import { useState, useEffect, useRef } from 'react'; import axios from 'axios'; import { IconPlus, IconSearch, IconEdit, IconTrash, IconCheck, IconX, IconDatabase, IconRefresh, IconUpload, IconDownload, IconDeviceFloppy } from '@tabler/icons-react'; const API_URL = '/api'; function IPRangesManager() { const [items, setItems] = useState([]); const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); const [newInvalid, setNewInvalid] = useState({ ipRange: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [editingIp, setEditingIp] = useState(null); const [editingValue, setEditingValue] = useState(''); const [loading, setLoading] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [sortField, setSortField] = useState('ipRange'); const [sortOrder, setSortOrder] = useState('asc'); const [filterCommunity, setFilterCommunity] = useState(''); const editInputRef = useRef(null); const pageSize = 10; const isValidIPv4 = (ip) => { const octets = ip.split('.'); if (octets.length !== 4) return false; return octets.every(o => /^\d{1,3}$/.test(o) && Number(o) >= 0 && Number(o) <= 255); }; const isValidCidr = (value) => { const v = String(value).trim(); const parts = v.split('/'); if (parts.length !== 2) return false; const [ip, mask] = parts; if (!isValidIPv4(ip)) return false; if (!/^\d{1,2}$/.test(mask)) return false; const m = Number(mask); return m >= 0 && m <= 32; }; const isValidCommunity = (value) => /^[0-9]+$/.test(String(value).trim()); useEffect(() => { fetchItems(); }, []); const fetchItems = async () => { setLoading(true); try { const response = await axios.get(`${API_URL}/ip-ranges`); setItems(response.data); setError(''); } catch (error) { console.error('Error fetching ip-ranges:', error); setError('Не удалось загрузить IP-диапазоны. Проверьте, запущен ли бэкенд.'); } finally { setLoading(false); } }; const handleAddItem = () => { const cidrOk = isValidCidr(newItem.ipRange); const communityOk = isValidCommunity(newItem.community); setNewInvalid({ ipRange: !cidrOk, community: !communityOk }); if (!cidrOk || !communityOk) { setError('Введите корректный IP-диапазон в формате CIDR (например 192.168.1.0/24) и числовой community.'); return; } setError(''); setItems([...items, { ipRange: newItem.ipRange.trim(), community: String(newItem.community).trim() }]); setNewItem({ ipRange: '', community: '' }); setNewInvalid({ ipRange: false, community: false }); }; const handleEdit = (item) => { setEditingIp(item.ipRange); setEditingValue(item.community); }; const handleSaveEdit = (ipRange) => { if (!isValidCommunity(editingValue)) { setError('Community должен быть числом.'); return; } const updatedItems = items.map(i => i.ipRange === ipRange ? { ...i, community: String(editingValue).trim() } : i ); setItems(updatedItems); setEditingIp(null); }; const handleCancelEdit = () => { setEditingIp(null); }; const handleDeleteItem = (ipRangeToDelete) => { setItems(items.filter(i => i.ipRange !== ipRangeToDelete)); }; const confirmDelete = (item) => { setItemToDelete(item); setShowDeleteModal(true); }; const executeDelete = () => { if (itemToDelete) { handleDeleteItem(itemToDelete.ipRange); setShowDeleteModal(false); setItemToDelete(null); } }; const handleSaveChanges = async () => { setLoading(true); try { const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)); await axios.post(`${API_URL}/ip-ranges`, { ipRanges: valid }); setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { console.error('Error saving changes:', error); setError('Не удалось сохранить изменения.'); } finally { setLoading(false); } }; const handleImport = () => { const text = window.prompt('Вставьте строки: CIDR ПРОБЕЛ COMMUNITY (по одной записи на строку)'); if (!text) return; const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); const parsed = lines.map(l => { const [cidr, c] = l.split(/\s+/); return { ipRange: cidr || '', community: c || '' }; }); setItems(prev => [...prev, ...parsed]); }; const handleExport = () => { const header = ['ipRange', 'community']; const csv = [header, ...items.map(i => [i.ipRange, i.community])] .map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `ip_ranges_${new Date().toISOString().split('T')[0]}.csv`; a.click(); URL.revokeObjectURL(url); }; const clearInvalid = () => { setItems(prev => prev.filter(i => i.ipRange || i.community) .filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)) ); }; // Сортировка const sortedItems = [...items].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; }); // Фильтрация по community const filteredByCommunity = filterCommunity ? sortedItems.filter(i => i.community === filterCommunity) : sortedItems; // Поиск const filteredItems = filteredByCommunity.filter(i => i.ipRange.toLowerCase().includes(searchTerm.toLowerCase()) ); // Пагинация const totalPages = Math.ceil(filteredItems.length / pageSize); const paginatedItems = filteredItems.slice((currentPage - 1) * pageSize, currentPage * pageSize); // Для фильтра - список всех уникальных community const allCommunities = Array.from(new Set(items.map(i => i.community))); // Сортировка по клику const handleSort = (field) => { if (sortField === field) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortField(field); setSortOrder('asc'); } }; // Улучшенный инлайн-редакт useEffect(() => { if (editingIp && editInputRef.current) { editInputRef.current.focus(); } }, [editingIp]); const handleEditKeyDown = (e, ipRange) => { if (e.key === 'Enter') handleSaveEdit(ipRange); if (e.key === 'Escape') handleCancelEdit(); }; return ( <> {error && (
{error}
)} {success && (
{success}
)}
{/* Add New Item Card */}

Добавить новый IP-диапазон

{ e.preventDefault(); handleAddItem(); }}>
setNewItem({ ...newItem, ipRange: e.target.value })} /> {newInvalid.ipRange &&
Некорректный CIDR
}
setNewItem({ ...newItem, community: e.target.value })} /> {newInvalid.community &&
Только цифры
}
{/* Actions Card */}

Действия

Список IP-диапазонов {items.length}

{/* Фильтр по community */} {/* Поиск */}
{ setSearchTerm(e.target.value); setCurrentPage(1); }} />
{paginatedItems.map((item) => ( ))}
handleSort('ipRange')}> IP-диапазон {sortField === 'ipRange' && ( {sortOrder === 'asc' ? '▲' : '▼'} )} handleSort('community')}> Community {sortField === 'community' && ( {sortOrder === 'asc' ? '▲' : '▼'} )}
{item.ipRange} {item.community} {editingIp === item.ipRange ? ( <> setEditingValue(e.target.value)} onKeyDown={e => handleEditKeyDown(e, item.ipRange)} style={{maxWidth: 120}} /> ) : ( <> )}
{/* Пагинация */} {totalPages > 1 && (
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length}
  • {(() => { const pages = []; const maxToShow = 7; // 1 .. 5 .. N let start = Math.max(1, currentPage - 2); let end = Math.min(totalPages, currentPage + 2); if (currentPage <= 3) { end = Math.min(totalPages, 5); } if (currentPage >= totalPages - 2) { start = Math.max(1, totalPages - 4); } if (start > 1) pages.push('start-ellipsis'); for (let p = start; p <= end; p++) pages.push(p); if (end < totalPages) pages.push('end-ellipsis'); const renderItem = (p) => { if (p === 'start-ellipsis' || p === 'end-ellipsis') { return (
  • ); } return (
  • ); }; return pages.map(renderItem); })()}
)}
{/* Delete Confirmation Modal */} {showDeleteModal && (

Удалить IP-диапазон?

Вы уверены, что хотите удалить "{itemToDelete?.ipRange}"? Это действие необратимо.
)} ); } export default IPRangesManager;