From 24d4e743b5454ee316aea1b7503e7dc02c4c3cd0 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Mon, 14 Jul 2025 20:36:10 +0700 Subject: [PATCH] feat: Integrate ASNsNewManager component for ASN management in routing --- frontend/src/ASNsNewManager.jsx | 429 ++++++++++++++++++++++++++++++++ frontend/src/App.jsx | 9 +- 2 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 frontend/src/ASNsNewManager.jsx diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx new file mode 100644 index 0000000..b0eec48 --- /dev/null +++ b/frontend/src/ASNsNewManager.jsx @@ -0,0 +1,429 @@ +import { useState, useEffect, useRef } from 'react'; +import axios from 'axios'; +import { + IconPlus, + IconSearch, + IconEdit, + IconTrash, + IconCheck, + IconX, + IconDatabase, + IconRefresh +} from '@tabler/icons-react'; + +const API_URL = '/api'; + +function ASNsNewManager() { + const [items, setItems] = useState([]); + const [newItem, setNewItem] = useState({ asn: '', community: '' }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [editingAsn, setEditingAsn] = 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('asn'); + const [sortOrder, setSortOrder] = useState('asc'); + const [filterCommunity, setFilterCommunity] = useState(''); + const editInputRef = useRef(null); + const pageSize = 10; + + useEffect(() => { + fetchItems(); + }, []); + + const fetchItems = async () => { + setLoading(true); + try { + const response = await axios.get(`${API_URL}/asns`); + setItems(response.data.map(item => ({ asn: item.domain, community: item.type }))); + setError(''); + } catch (error) { + console.error('Error fetching ASNs:', error); + setError('Не удалось загрузить список ASN. Проверьте, запущен ли бэкенд.'); + } finally { + setLoading(false); + } + }; + + const handleAddItem = () => { + if (newItem.asn.trim() === '') { + setError('Номер AS не может быть пустым.'); + return; + } + if (newItem.community.trim() === '') { + setError('Community не может быть пустым.'); + return; + } + setError(''); + setItems([...items, newItem]); + setNewItem({ asn: '', community: '' }); + }; + + const handleEdit = (item) => { + setEditingAsn(item.asn); + setEditingValue(item.community); + }; + + const handleSaveEdit = (asn) => { + const updatedItems = items.map(i => + i.asn === asn ? { ...i, community: editingValue } : i + ); + setItems(updatedItems); + setEditingAsn(null); + }; + + const handleCancelEdit = () => { + setEditingAsn(null); + }; + + const handleDeleteItem = (asnToDelete) => { + setItems(items.filter(i => i.asn !== asnToDelete)); + }; + + const confirmDelete = (item) => { + setItemToDelete(item); + setShowDeleteModal(true); + }; + + const executeDelete = () => { + if (itemToDelete) { + handleDeleteItem(itemToDelete.asn); + setShowDeleteModal(false); + setItemToDelete(null); + } + }; + + const handleSaveChanges = async () => { + setLoading(true); + try { + // API ожидает domains: [{domain, type}] + await axios.post(`${API_URL}/asns`, { domains: items.map(i => ({ domain: i.asn, type: i.community })) }); + setSuccess('Изменения успешно сохранены!'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error saving changes:', error); + setError('Не удалось сохранить изменения.'); + } finally { + setLoading(false); + } + }; + + // Сортировка + 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.asn.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 (editingAsn && editInputRef.current) { + editInputRef.current.focus(); + } + }, [editingAsn]); + + const handleEditKeyDown = (e, asn) => { + if (e.key === 'Enter') handleSaveEdit(asn); + if (e.key === 'Escape') handleCancelEdit(); + }; + + return ( + <> + {error && ( +
+
+
+ +
+
+ {error} +
+
+ setError('')}> +
+ )} + + {success && ( +
+
+
+ +
+
+ {success} +
+
+ setSuccess('')}> +
+ )} + +
+
+ {/* Add New ASN Card */} +
+
+

+ + Добавить новый ASN +

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

+ + Действия +

+
+
+
+ + +
+
+
+
+ +
+
+
+

Список ASN

+
+ {/* Фильтр по community */} + + {/* Поиск */} +
+ + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} + /> +
+
+
+
+ + + + + + + + + + {paginatedItems.map((item) => ( + + + + + + ))} + +
handleSort('asn')}> + Номер AS + {sortField === 'asn' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + handleSort('community')}> + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} +
{item.asn}{item.community} + {editingAsn === item.asn ? ( + <> + setEditingValue(e.target.value)} + onKeyDown={e => handleEditKeyDown(e, item.asn)} + style={{maxWidth: 120}} + /> + + + + ) : ( + <> + + + + )} +
+
+ {/* Пагинация */} + {totalPages > 1 && ( +
+
    +
  • + +
  • + {Array.from({length: totalPages}, (_, i) => i + 1).map(page => ( +
  • + +
  • + ))} +
  • + +
  • +
+
+ )} +
+
+
+ + {/* Delete Confirmation Modal */} + {showDeleteModal && ( +
+
+
+ +
+
+ +

Удалить ASN?

+
+ Вы уверены, что хотите удалить "{itemToDelete?.asn}"? Это действие необратимо. +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+ )} + + ); +} + +export default ASNsNewManager; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 80155bd..6ee4289 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,6 +24,7 @@ import ServerManager from './ServerManager'; import FilterManager from './FilterManager'; import DomainsNewManager from './DomainsNewManager'; import IPRangesManager from './IPRangesManager'; +import ASNsNewManager from './ASNsNewManager'; import './App.css'; import axios from 'axios'; import { @@ -101,13 +102,7 @@ function MainLayout() { } /> } /> } /> - - } /> + } /> } /> } /> } />