import { useState, useEffect, useRef } from 'react'; import api from './lib/api.js'; import ConfirmDialog from './components/ConfirmDialog.jsx'; import FormModal from './components/FormModal.jsx'; import ConfirmModal from './components/ConfirmModal.jsx'; import FormField from './components/FormField.jsx'; import { IconPlus, IconSearch, IconEdit, IconTrash, IconCheck, IconX, IconDatabase, IconRefresh, IconArrowsExchange, IconDownload, IconUpload, IconAlertTriangle, IconServer, IconChevronDown, IconLink, IconFilter, IconMapPin, IconWorld } from '@tabler/icons-react'; import GraphView from './GraphView'; const API_URL = '/api'; function ServerManager() { const [servers, setServers] = useState([]); const [newServer, setNewServer] = useState({ ip: '', dns: '', country: '', provider: '', tunnel: 'GRE', gateway: '' }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [editingServer, setEditingServer] = useState(null); const [editingValues, setEditingValues] = useState({}); const [bulkFrom, setBulkFrom] = useState('GRE'); const [bulkTo, setBulkTo] = useState('IPSec'); const [loading, setLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [serverToDelete, setServerToDelete] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [sortField, setSortField] = useState('ip'); const [sortOrder, setSortOrder] = useState('asc'); const [filterTunnel, setFilterTunnel] = useState(''); const [filterCountry, setFilterCountry] = useState(''); const [filterProvider, setFilterProvider] = useState(''); const editInputRef = useRef(null); const pageSize = 10; const [editModalOpen, setEditModalOpen] = useState(false); const [editModalServer, setEditModalServer] = useState(null); // Состояние для генератора ссылок const [linkGeneratorOpen, setLinkGeneratorOpen] = useState(false); const [linkGeneratorServer, setLinkGeneratorServer] = useState(null); const [urlSettings, setUrlSettings] = useState({ baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: 'SWE-IHOR', fastly_gateway: 'SWE-IHOR', telegram_gateway: '94.142.140.1', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' }); const presets = { default: { baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: 'SWE-IHOR', fastly_gateway: 'SWE-IHOR', telegram_gateway: '94.142.140.1', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' }, cloudflareSWE: { baseUrl: 'https://cf.example/api', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '', type: 'routes', version: 'v4.rsc' }, hetznerDE: { baseUrl: 'https://hetzner.example/api', cloudflare_gateway: '', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' } }; // Состояние для мониторинга сети const [networkStatus, setNetworkStatus] = useState({}); const [monitoringEnabled, setMonitoringEnabled] = useState(false); // Состояние для модального окна добавления сервера const [addServerModalOpen, setAddServerModalOpen] = useState(false); const [customProvider, setCustomProvider] = useState(''); const [connections, setConnections] = useState([ // Пример связи для теста // { from: '192.168.1.1', to: '192.168.1.2', tunnelType: 'GRE', ipA: '10.10.100.1', ipB: '10.10.100.2' } ]); const [newConnection, setNewConnection] = useState({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); // Фильтры графа const [graphTunnelFilters, setGraphTunnelFilters] = useState({ GRE: true, IPSec: true, WireGuard: true, OpenVPN: true }); const [graphCountryFilter, setGraphCountryFilter] = useState(''); const [graphProviderFilter, setGraphProviderFilter] = useState(''); const [confirmState, setConfirmState] = useState({ open: false, text: '', onConfirm: null }); useEffect(() => { fetchServers(); }, []); // Загружаем настройки URL из localStorage useEffect(() => { const savedSettings = localStorage.getItem('urlSettings'); if (savedSettings) { try { const parsed = JSON.parse(savedSettings); setUrlSettings(prev => ({ ...prev, ...parsed })); } catch (e) { console.error('Ошибка при загрузке настроек URL:', e); } } }, []); const fetchServers = async () => { setLoading(true); try { const response = await api.get(`/servers`); setServers(response.data); setError(''); } catch (error) { console.error('Error fetching servers:', error); setError('Не удалось загрузить серверы. Проверьте, запущен ли бэкенд.'); } finally { setLoading(false); } }; const handleAddServer = () => { if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim()) { setError('Все поля должны быть заполнены.'); return; } setError(''); setServers([...servers, { ...newServer }]); setNewServer({ ip: '', dns: '', country: '', provider: '', tunnel: 'GRE' }); }; // Функции для модального окна добавления сервера const handleOpenAddServerModal = () => { setAddServerModalOpen(true); }; const handleCloseAddServerModal = () => { setAddServerModalOpen(false); setNewServer({ ip: '', dns: '', country: '', provider: '', tunnel: 'GRE' }); setCustomProvider(''); setError(''); }; const handleAddServerFromModal = () => { if (!newServer.ip.trim() || !newServer.dns.trim() || !newServer.country.trim() || !newServer.provider.trim() || !newServer.tunnel.trim() || !newServer.gateway.trim()) { setError('Все поля должны быть заполнены.'); return; } // Проверяем, если выбран "Другой" провайдер, то customProvider должен быть заполнен if (newServer.provider === 'Другой' && !customProvider.trim()) { setError('Пожалуйста, введите название провайдера.'); return; } setError(''); // Используем customProvider если выбран "Другой" const finalProvider = newServer.provider === 'Другой' ? customProvider : newServer.provider; const serverToAdd = { ...newServer, provider: finalProvider }; setServers([...servers, serverToAdd]); setNewServer({ ip: '', dns: '', country: '', provider: '', tunnel: 'GRE' }); setCustomProvider(''); setAddServerModalOpen(false); setSuccess('Сервер успешно добавлен!'); setTimeout(() => setSuccess(''), 3000); }; const handleEdit = (server) => { setEditModalServer({ ...server }); setEditModalOpen(true); }; const handleEditModalChange = (updatedServer) => { setEditModalServer(updatedServer); }; const handleEditModalSave = (updatedServer) => { setServers(servers.map(s => s.ip === updatedServer.ip ? { ...updatedServer } : s)); setEditModalOpen(false); setEditModalServer(null); }; const handleEditModalClose = () => { setEditModalOpen(false); setEditModalServer(null); }; // Функции для генератора ссылок const handleGenerateLink = (server) => { setLinkGeneratorServer(server); setLinkGeneratorOpen(true); }; const handleLinkGeneratorClose = () => { setLinkGeneratorOpen(false); setLinkGeneratorServer(null); }; const generateServerUrl = (server) => { const params = new URLSearchParams(); // Добавляем все параметры из настроек Object.entries(urlSettings).forEach(([key, value]) => { if (key !== 'baseUrl' && value) { params.append(key, value); } }); // Добавляем IP сервера как параметр server params.append('server', server.ip); return `${urlSettings.baseUrl}?${params.toString()}`; }; const applyPreset = (name) => { const p = presets[name]; if (!p) return; setUrlSettings(prev => ({ ...prev, ...p })); localStorage.setItem('urlSettings', JSON.stringify({ ...urlSettings, ...p })); setSuccess('Профиль применён'); setTimeout(() => setSuccess(''), 2000); }; // Функция резервного копирования для http const fallbackCopyToClipboard = (text) => { const textArea = document.createElement('textarea'); textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); setSuccess('Ссылка скопирована в буфер обмена!'); setTimeout(() => setSuccess(''), 3000); } catch (err) { setError('Не удалось скопировать ссылку'); } document.body.removeChild(textArea); }; const copyToClipboard = async (text) => { if (navigator.clipboard) { try { await navigator.clipboard.writeText(text); setSuccess('Ссылка скопирована в буфер обмена!'); setTimeout(() => setSuccess(''), 3000); return; } catch (err) { // fallback ниже } } fallbackCopyToClipboard(text); }; const quickCopyLink = async (server) => { const url = generateServerUrl(server); await copyToClipboard(url); }; const exportAllLinks = () => { const filtered = servers .filter(s => (!filterTunnel || s.tunnel === filterTunnel)) .filter(s => (!filterCountry || s.country === filterCountry)) .filter(s => (!filterProvider || s.provider === filterProvider)); const csvContent = [ ['IP', 'DNS', 'Country', 'Provider', 'Tunnel', 'Generated URL'], ...filtered.map(server => [ server.ip, server.dns, server.country, server.provider, server.tunnel, generateServerUrl(server) ]) ].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `server_links_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; const handleCancelEdit = () => { setEditingServer(null); }; const handleDeleteServer = (serverIp) => { setServers(servers.filter(s => s.ip !== serverIp)); }; const confirmDelete = (server) => { setServerToDelete(server); setShowDeleteModal(true); }; const executeDelete = () => { if (serverToDelete) { handleDeleteServer(serverToDelete.ip); setShowDeleteModal(false); setServerToDelete(null); } }; const handleSaveChanges = async () => { setLoading(true); try { await api.post(`/servers`, { domains: servers }); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); } finally { setLoading(false); } }; const handleBulkUpdate = () => { if (!bulkFrom || !bulkTo) { setError('Оба поля для массового обновления должны быть заполнены.'); return; } setConfirmState({ open: true, text: `Вы уверены, что хотите заменить все туннели "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`, onConfirm: () => { const updatedServers = servers.map(s => s.tunnel === bulkFrom ? { ...s, tunnel: bulkTo } : s); setServers(updatedServers); setSuccess(`Туннели "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`); setConfirmState({ open: false, text: '', onConfirm: null }); }, }); }; // Сортировка const sortedServers = [...servers].sort((a, b) => { let valA = a[sortField] || ''; let valB = b[sortField] || ''; if (typeof valA === 'string') valA = valA.toLowerCase(); if (typeof valB === 'string') valB = valB.toLowerCase(); if (valA < valB) return sortOrder === 'asc' ? -1 : 1; if (valA > valB) return sortOrder === 'asc' ? 1 : -1; return 0; }); // Фильтрация let filteredServers = sortedServers; if (filterTunnel) { filteredServers = filteredServers.filter(s => s.tunnel === filterTunnel); } if (filterCountry) { filteredServers = filteredServers.filter(s => s.country === filterCountry); } if (filterProvider) { filteredServers = filteredServers.filter(s => s.provider === filterProvider); } // Поиск filteredServers = filteredServers.filter(s => s.ip.toLowerCase().includes(searchTerm.toLowerCase()) || s.dns.toLowerCase().includes(searchTerm.toLowerCase()) || s.country.toLowerCase().includes(searchTerm.toLowerCase()) || s.provider.toLowerCase().includes(searchTerm.toLowerCase()) ); // Пагинация const totalPages = Math.ceil(filteredServers.length / pageSize); const paginatedServers = filteredServers.slice((currentPage - 1) * pageSize, currentPage * pageSize); // Для фильтров - списки всех уникальных значений const allTunnels = Array.from(new Set(servers.map(s => s.tunnel))); const allCountries = Array.from(new Set(servers.map(s => s.country))); const allProviders = Array.from(new Set(servers.map(s => s.provider))); // Сортировка по клику const handleSort = (field) => { if (sortField === field) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortOrder('asc'); } }; // Улучшенный инлайн-редакт useEffect(() => { if (editingServer && editInputRef.current) { editInputRef.current.focus(); } }, [editingServer]); const handleEditKeyDown = (e, serverIp) => { if (e.key === 'Enter') handleSaveEdit(serverIp); if (e.key === 'Escape') handleCancelEdit(); }; // Функция для преобразования кода страны в emoji-флаг function countryToFlag(isoCode) { if (!isoCode) return ''; return isoCode .toUpperCase() .replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt()) ); } // Добавление новой связи const handleAddConnection = () => { if (!newConnection.from || !newConnection.to || !newConnection.tunnelType || !newConnection.ipA || !newConnection.ipB) { setError('Все поля для связи должны быть заполнены.'); return; } if (newConnection.from === newConnection.to) { setError('Сервер A и сервер B не могут быть одинаковыми.'); return; } setConnections([...connections, newConnection]); setNewConnection({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); setError(''); setSuccess('Связь успешно добавлена!'); setTimeout(() => setSuccess(''), 3000); }; // Удаление связи const handleDeleteConnection = (index) => { setConnections(connections.filter((_, i) => i !== index)); setSuccess('Связь удалена!'); setTimeout(() => setSuccess(''), 3000); }; return ( <> {error && (
{error}
)} {success && (
{success}
)} {/* Page header */}

Серверы

Главная / Управление / Серверы
{Object.keys(presets).map(key => ( ))}
{/* Таблица серверов на всю ширину */}

Список серверов {filteredServers.length}

{/* Фильтры и поиск */}
{ setSearchTerm(e.target.value); setCurrentPage(1); }} />
{paginatedServers.length === 0 ? ( ) : paginatedServers.map((server) => ( ))}
handleSort('ip')}> IP адрес {sortField === 'ip' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('dns')}> DNS имя {sortField === 'dns' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('country')}> Страна {sortField === 'country' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('provider')}> Провайдер {sortField === 'provider' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('tunnel')}> Туннель {sortField === 'tunnel' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('gateway')}> Основной шлюз {sortField === 'gateway' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} Статус
Ничего не найдено. Измените фильтры или поиск.
{server.ip} {server.dns} {countryToFlag(server.country)} {server.country} {server.provider} {server.tunnel} {server.gateway} Онлайн
{/* Пагинация */} {totalPages > 1 && (
Показано {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length}
)}
{/* Delete Modal - New unified component */} setShowDeleteModal(false)} onConfirm={executeDelete} title="Подтверждение удаления" message={ serverToDelete ? ( <>

Вы уверены, что хотите удалить сервер {serverToDelete.ip}?

Это действие нельзя отменить.

) : null } confirmLabel="Удалить" variant="danger" icon={IconTrash} /> {/* Edit Server Modal */} {/* Link Generator Modal */} {/* Add Server Modal - New unified component */} {error && (
{error}
)} setNewServer({ ...newServer, ip: val })} placeholder="192.168.1.1" required icon={IconServer} helpText="Введите IPv4 адрес сервера" /> setNewServer({ ...newServer, dns: val })} placeholder="srv1.shx.su" required icon={IconWorld} helpText="Полное доменное имя сервера" /> setNewServer({ ...newServer, country: val })} required icon={IconMapPin} options={[ { value: '', label: 'Выберите страну', disabled: true }, { value: 'RU', label: 'Россия (RU)' }, { value: 'SWE', label: 'Швеция (SWE)' }, { value: 'US', label: 'США (US)' }, { value: 'DE', label: 'Германия (DE)' }, { value: 'NL', label: 'Нидерланды (NL)' }, { value: 'SG', label: 'Сингапур (SG)' } ]} /> setNewServer({ ...newServer, provider: val })} required options={[ { value: '', label: 'Выберите провайдера', disabled: true }, { value: 'Yandex', label: 'Yandex Cloud' }, { value: 'AWS', label: 'Amazon Web Services' }, { value: 'Google', label: 'Google Cloud' }, { value: 'Azure', label: 'Microsoft Azure' }, { value: 'Hetzner', label: 'Hetzner' }, { value: 'OVH', label: 'OVH' }, { value: 'DigitalOcean', label: 'DigitalOcean' }, { value: 'Vultr', label: 'Vultr' }, { value: 'Linode', label: 'Linode' }, { value: 'VPSVILLE', label: 'VPSVILLE' }, { value: 'HIPHOST', label: 'HIPHOST' }, { value: 'Другой', label: 'Другой' } ]} /> {newServer.provider === 'Другой' && ( )} setNewServer({ ...newServer, tunnel: val })} required options={[ { value: 'GRE', label: 'GRE (Generic Routing Encapsulation)' }, { value: 'IPSec', label: 'IPSec' }, { value: 'WireGuard', label: 'WireGuard' }, { value: 'OpenVPN', label: 'OpenVPN' } ]} /> setNewServer({ ...newServer, gateway: val })} placeholder="Cloudflare, Hetzner, Мой шлюз и т.д." required helpText="Имя основного шлюза для маршрутизации" />

Граф связей между серверами

Поддержка: GRE, IPSec, WireGuard, OpenVPN
Фильтры графа:
{['GRE','IPSec','WireGuard','OpenVPN'].map(t => ( ))}
(!graphCountryFilter || s.country === graphCountryFilter) && (!graphProviderFilter || s.provider === graphProviderFilter))} connections={connections.filter(c => graphTunnelFilters[c.tunnelType])} onCreateConnection={({ from, to }) => { // Открываем модал добавления связи и проставляем выбранные узлы setNewConnection(prev => ({ ...prev, from, to })); const el = document.querySelector('#add-connection-anchor'); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }} />
GRE
IPSec
WireGuard
OpenVPN

Добавить связь между серверами

setNewConnection({ ...newConnection, ipA: e.target.value })} placeholder="10.10.100.1" />
setNewConnection({ ...newConnection, ipB: e.target.value })} placeholder="10.10.100.2" />
{/* Список существующих связей */} {connections.length > 0 && (

Существующие связи ({connections.length})

{connections.map((connection, index) => { const serverA = servers.find(s => s.ip === connection.from); const serverB = servers.find(s => s.ip === connection.to); return ( ); })}
Сервер A Сервер B Тип туннеля IP адреса туннеля Действия
{connection.from}
{serverA?.dns || 'Неизвестный сервер'}
{connection.to}
{serverB?.dns || 'Неизвестный сервер'}
{connection.tunnelType} {connection.ipA} ⇄ {connection.ipB}
)} setConfirmState({ open: false, text: '', onConfirm: null })} onConfirm={() => confirmState.onConfirm?.()} /> ); } // Модальное окно для редактирования сервера с backdrop и анимацией function EditServerModal({ show, server, onChange, onSave, onClose }) { const modalRef = useRef(null); // Локальное состояние для формы const [localServer, setLocalServer] = useState(server || {}); // Синхронизируем локальное состояние с пропсами useEffect(() => { setLocalServer(server || {}); }, [server]); useEffect(() => { if (window.Tabler && window.Tabler.Modal && modalRef.current) { const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current); if (show) { modalInstance.show(); } else { modalInstance.hide(); } // Закрытие по событию Tabler const handler = () => onClose && onClose(); modalRef.current.addEventListener('hide.bs.modal', handler); return () => { if (modalRef.current) { modalRef.current.removeEventListener('hide.bs.modal', handler); } }; } }, [show, onClose]); const handleChange = (field, value) => { const updated = { ...localServer, [field]: value }; setLocalServer(updated); onChange && onChange(updated); }; return (
Редактировать сервер
{ e.preventDefault(); onSave && onSave(localServer); }}>
handleChange('ip', e.target.value)} />
handleChange('dns', e.target.value)} />
handleChange('country', e.target.value)} />
handleChange('provider', e.target.value)} />
handleChange('tunnel', e.target.value)} />
); } function DeleteServerModal({ show, server, onDelete, onClose }) { const modalRef = useRef(null); useEffect(() => { if (window.Tabler && window.Tabler.Modal && modalRef.current) { const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current); if (show) { modalInstance.show(); } else { modalInstance.hide(); } const handler = () => onClose && onClose(); modalRef.current.addEventListener('hide.bs.modal', handler); return () => { if (modalRef.current) { modalRef.current.removeEventListener('hide.bs.modal', handler); } }; } }, [show, onClose]); return (
Подтверждение удаления

Вы уверены, что хотите удалить сервер {server?.ip}?

Это действие нельзя отменить.

); } // Модальное окно для генератора ссылок function LinkGeneratorModal({ show, server, urlSettings, onUrlSettingsChange, onGenerateUrl, onCopyToClipboard, onClose }) { const modalRef = useRef(null); const [localUrlSettings, setLocalUrlSettings] = useState(urlSettings); // Синхронизируем локальное состояние с пропсами useEffect(() => { setLocalUrlSettings(urlSettings); }, [urlSettings]); useEffect(() => { if (window.Tabler && window.Tabler.Modal && modalRef.current) { const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current); if (show) { modalInstance.show(); } else { modalInstance.hide(); } const handler = () => onClose && onClose(); modalRef.current.addEventListener('hide.bs.modal', handler); return () => { if (modalRef.current) { modalRef.current.removeEventListener('hide.bs.modal', handler); } }; } }, [show, onClose]); const handleSettingChange = (key, value) => { const updated = { ...localUrlSettings, [key]: value }; setLocalUrlSettings(updated); onUrlSettingsChange(updated); }; const handleSaveSettings = () => { onUrlSettingsChange(localUrlSettings); // Сохраняем в localStorage localStorage.setItem('urlSettings', JSON.stringify(localUrlSettings)); }; const generatedUrl = server ? onGenerateUrl(server) : ''; return (
Генератор ссылок для {server?.ip}
Настройки URL
handleSettingChange('baseUrl', e.target.value)} placeholder="https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo" />
handleSettingChange('cloudflare_gateway', e.target.value)} placeholder="SWE-IHOR или IP" />
handleSettingChange('bunny_gateway', e.target.value)} placeholder="SWE-IHOR или IP" />
handleSettingChange('fastly_gateway', e.target.value)} placeholder="SWE-IHOR или IP" />
handleSettingChange('telegram_gateway', e.target.value)} placeholder="IP адрес" />
handleSettingChange('hetzner_gateway', e.target.value)} placeholder="IP адрес" />
handleSettingChange('type', e.target.value)} placeholder="routes" />
handleSettingChange('version', e.target.value)} placeholder="v4.rsc" />
Сгенерированная ссылка
); } // Модальное окно для добавления нового сервера function AddServerModal({ show, newServer, customProvider, onNewServerChange, onCustomProviderChange, onAddServer, onClose, error }) { const modalRef = useRef(null); useEffect(() => { if (window.Tabler && window.Tabler.Modal && modalRef.current) { const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current); if (show) { modalInstance.show(); } else { modalInstance.hide(); } const handler = () => onClose && onClose(); modalRef.current.addEventListener('hide.bs.modal', handler); return () => { if (modalRef.current) { modalRef.current.removeEventListener('hide.bs.modal', handler); } }; } }, [show, onClose]); const handleChange = (field, value) => { onNewServerChange({ ...newServer, [field]: value }); }; const handleSubmit = (e) => { e.preventDefault(); onAddServer(); }; return (
Добавить новый сервер
{error && (
{error}
)}
handleChange('ip', e.target.value)} required />
handleChange('dns', e.target.value)} required />
{newServer.provider === 'Другой' && ( onCustomProviderChange(e.target.value)} /> )}
handleChange('gateway', e.target.value)} required />
); } export default ServerManager;