diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 52731ae..0d04f72 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -1,41 +1,35 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import axios from 'axios'; -import { - IconPlus, - IconSearch, - IconEdit, - IconTrash, - IconCheck, - IconX, +import { + IconPlus, + IconSearch, + IconEdit, + IconTrash, + IconCheck, + IconX, IconDatabase, - IconRefresh, - IconDownload, - IconUpload, - IconAlertTriangle, - IconFilter, - IconChevronDown, - IconLink, - IconCopy, - IconEye, - IconServer, - IconSettings, - IconDeviceFloppy, - IconHash, - IconRocket, - IconWorld, - IconNetwork + IconRefresh } from '@tabler/icons-react'; const API_URL = '/api'; function IPRangesManager() { const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); + const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); - const [editingIndex, setEditingIndex] = useState(null); - const [editingItem, setEditingItem] = useState({ ipRange: '', community: '' }); + 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; useEffect(() => { fetchItems(); @@ -49,314 +43,311 @@ function IPRangesManager() { setError(''); } catch (error) { console.error('Error fetching ip-ranges:', error); - setItems([]); + setError('Не удалось загрузить IP-диапазоны. Проверьте, запущен ли бэкенд.'); } finally { setLoading(false); } }; - const handleAdd = () => { - setEditingIndex(null); - setEditingItem({ ipRange: '', community: '' }); - }; - - const handleEdit = (index) => { - setEditingIndex(index); - setEditingItem({ ...items[index] }); - }; - - const handleSave = () => { - if (!editingItem.ipRange.trim() || !editingItem.community.trim()) { - setError('IP-диапазон и Community обязательны для заполнения.'); + const handleAddItem = () => { + if (newItem.ipRange.trim() === '') { + setError('IP-диапазон не может быть пустым.'); return; } - - const updatedItems = [...items]; - if (editingIndex !== null) { - updatedItems[editingIndex] = { ...editingItem }; - } else { - updatedItems.push({ ...editingItem }); + if (newItem.community.trim() === '') { + setError('Community не может быть пустым.'); + return; } - + setError(''); + setItems([...items, newItem]); + setNewItem({ ipRange: '', community: '' }); + }; + + const handleEdit = (item) => { + setEditingIp(item.ipRange); + setEditingValue(item.community); + }; + + const handleSaveEdit = (ipRange) => { + const updatedItems = items.map(i => + i.ipRange === ipRange ? { ...i, community: editingValue } : i + ); setItems(updatedItems); - setEditingIndex(null); - setEditingItem({ ipRange: '', community: '' }); + setEditingIp(null); }; - const handleCancel = () => { - setEditingIndex(null); - setEditingItem({ ipRange: '', community: '' }); + const handleCancelEdit = () => { + setEditingIp(null); }; - const handleDelete = (index) => { - const updatedItems = items.filter((_, i) => i !== index); - setItems(updatedItems); + const handleDeleteItem = (ipRangeToDelete) => { + setItems(items.filter(i => i.ipRange !== ipRangeToDelete)); }; - const handleSaveToS3 = async () => { + const confirmDelete = (item) => { + setItemToDelete(item); + setShowDeleteModal(true); + }; + + const executeDelete = () => { + if (itemToDelete) { + handleDeleteItem(itemToDelete.ipRange); + setShowDeleteModal(false); + setItemToDelete(null); + } + }; + + const handleSaveChanges = async () => { setLoading(true); try { await axios.post(`${API_URL}/ip-ranges`, { ipRanges: items }); - setSuccess('IP-диапазоны успешно сохранены!'); + setSuccess('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } catch (error) { - console.error('Error saving ip-ranges:', error); - setError('Не удалось сохранить IP-диапазоны.'); + console.error('Error saving changes:', error); + setError('Не удалось сохранить изменения.'); } finally { setLoading(false); } }; - const handleUpload = async (event) => { - const file = event.target.files[0]; - if (!file) return; + // Сортировка + 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; + }); - const reader = new FileReader(); - reader.onload = (e) => { - const content = e.target.result; - const lines = content.split('\n').filter(line => line.trim()); - const parsedItems = lines.map(line => { - const parts = line.trim().split(/\s+/); - return { - ipRange: parts[0] || '', - community: parts[1] || '' - }; - }); - setItems(parsedItems); - setSuccess(`Загружено ${parsedItems.length} IP-диапазонов!`); - setTimeout(() => setSuccess(''), 3000); - }; - reader.readAsText(file); - }; + // Фильтрация по community + const filteredByCommunity = filterCommunity + ? sortedItems.filter(i => i.community === filterCommunity) + : sortedItems; - const handleDownload = () => { - const content = items.map(item => `${item.ipRange} ${item.community}`).join('\n'); - const blob = new Blob([content], { type: 'text/plain' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'ips.txt'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const filteredItems = items.filter(item => - item.ipRange.toLowerCase().includes(searchTerm.toLowerCase()) || - item.community.toLowerCase().includes(searchTerm.toLowerCase()) + // Поиск + const filteredItems = filteredByCommunity.filter(i => + i.ipRange.toLowerCase().includes(searchTerm.toLowerCase()) ); - return ( -
-
-
-
-

Управление IP-диапазонами

-
Главная / IP-диапазоны
-
-
-
- - - -
-
-
-
+ // Пагинация + 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} - +
+
+ +
+
+ {error} +
+
+ setError('')}>
)} + {success && (
- - {success} - +
+
+ +
+
+ {success} +
+
+ setSuccess('')}>
)} - {/* Панель поиска и сохранения */} -
-
-
-
-
- - - - setSearchTerm(e.target.value)} - /> -
+
+
+ {/* Add New Item Card */} +
+
+

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

-
-
+
+
{ e.preventDefault(); handleAddItem(); }}> +
+ + setNewItem({ ...newItem, ipRange: e.target.value })} + /> +
+
+ + setNewItem({ ...newItem, community: e.target.value })} + /> +
+
+ +
+
+
+
+ + {/* Actions Card */} +
+
+

+ + Действия +

+
+
+
+ -
-
- {/* Таблица IP-диапазонов */} -
-
-

- - IP-диапазоны ({filteredItems.length} из {items.length}) -

-
-
- {loading ? ( -
-
- Загрузка... +
+
+
+

Список IP-диапазонов

+
+ {/* Фильтр по community */} + + {/* Поиск */} +
+ + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} + /> +
- Загрузка IP-диапазонов...
- ) : filteredItems.length === 0 ? ( -
- -

Нет IP-диапазонов

- -
- ) : (
- - - + + + - {filteredItems.map((item, index) => ( - - - + {paginatedItems.map((item) => ( + + + @@ -364,10 +355,73 @@ function IPRangesManager() {
IP-ДИАПАЗОНCOMMUNITYДЕЙСТВИЯ handleSort('ipRange')}> + IP-диапазон + {sortField === 'ipRange' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + handleSort('community')}> + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} +
- {editingIndex === index ? ( - setEditingItem({ ...editingItem, ipRange: e.target.value })} - placeholder="192.168.1.0/24" - /> - ) : ( -
- - - - {item.ipRange} -
- )} -
- {editingIndex === index ? ( - setEditingItem({ ...editingItem, community: e.target.value })} - placeholder="65001:100" - /> - ) : ( -
- - - - {item.community} -
- )} -
{item.ipRange}{item.community} - {editingIndex === index ? ( -
- - -
+ {editingIp === item.ipRange ? ( + <> + setEditingValue(e.target.value)} + onKeyDown={e => handleEditKeyDown(e, item.ipRange)} + style={{maxWidth: 120}} + /> + + + ) : ( -
- - -
+ <> + + + )}
- )} + {/* Пагинация */} + {totalPages > 1 && ( +
+
    +
  • + +
  • + {Array.from({length: totalPages}, (_, i) => i + 1).map(page => ( +
  • + +
  • + ))} +
  • + +
  • +
+
+ )} +
-
+ + {/* Delete Confirmation Modal */} + {showDeleteModal && ( +
+
+
+ +
+
+ +

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

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