diff --git a/backend/server.js b/backend/server.js index c589344..990d2ef 100644 --- a/backend/server.js +++ b/backend/server.js @@ -126,6 +126,56 @@ app.post('/api/asns', async (req, res) => { } }); +// --- Domains New Routes --- + +// Get domains-new from S3 +app.get('/api/domains-new', async (req, res) => { + const params = { + Bucket: BUCKET_NAME, + Key: 'domains_comunity.txt', + }; + + try { + const data = await s3.getObject(params).promise(); + const fileContent = data.Body.toString('utf-8'); + const domains = fileContent.split('\n').filter(line => line).map(line => { + const parts = line.trim().split(/\s+/); + const domain = parts[0] || ''; + const community = parts[1] || ''; + return { domain, community }; + }); + res.json(domains); + } catch (error) { + if (error.code === 'NoSuchKey') { + res.json([]); // Return empty array if file does not exist + } else { + console.error(error); + res.status(500).send('Error reading from S3'); + } + } +}); + +// Update domains-new in S3 +app.post('/api/domains-new', async (req, res) => { + const { domains } = req.body; + const fileContent = domains.map(d => `${d.domain} ${d.community}`).join('\n'); + + const params = { + Bucket: BUCKET_NAME, + Key: 'domains_comunity.txt', + Body: fileContent, + ContentType: 'text/plain', + }; + + try { + await s3.putObject(params).promise(); + res.send('File updated successfully'); + } catch (error) { + console.error(error); + res.status(500).send('Error writing to S3'); + } +}); + // --- Servers Routes (JSON format) --- // Get servers from S3 @@ -267,14 +317,16 @@ app.post('/api/filters', async (req, res) => { // Новый эндпоинт для получения дат последнего изменения файлов S3 app.get('/api/s3/last-modified', async (req, res) => { try { - const [domainsHead, asnsHead, serversHead, filtersHead] = await Promise.all([ + const [domainsHead, domainsNewHead, asnsHead, serversHead, filtersHead] = await Promise.all([ s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains.txt' }).promise(), + s3.headObject({ Bucket: BUCKET_NAME, Key: 'domains_comunity.txt' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'asns.txt' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise(), s3.headObject({ Bucket: BUCKET_NAME, Key: 'filters.json' }).promise() ]); res.json({ domainsLastModified: domainsHead.LastModified ? domainsHead.LastModified.toISOString() : null, + domainsNewLastModified: domainsNewHead.LastModified ? domainsNewHead.LastModified.toISOString() : null, asnsLastModified: asnsHead.LastModified ? asnsHead.LastModified.toISOString() : null, serversLastModified: serversHead.LastModified ? serversHead.LastModified.toISOString() : null, filtersLastModified: filtersHead.LastModified ? filtersHead.LastModified.toISOString() : null diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 15a2069..c86e861 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -22,6 +22,7 @@ import { import DataManager from './DataManager'; import ServerManager from './ServerManager'; import FilterManager from './FilterManager'; +import DomainsNewManager from './DomainsNewManager'; import './App.css'; import axios from 'axios'; import { @@ -50,6 +51,7 @@ function MainLayout() { const navItems = [ { id: 'domains', title: 'Домены', icon: IconWorld, path: '/domains' }, + { id: 'domains-new', title: 'Домены New', icon: IconWorld, path: '/domains-new' }, { id: 'asns', title: 'AS', icon: IconNetwork, path: '/asns' }, { id: 'servers', title: 'Серверы', icon: IconServer, path: '/servers' }, { id: 'filters', title: 'Фильтры', icon: IconFilter, path: '/filters' }, @@ -73,7 +75,7 @@ function MainLayout() { S3 Lists Manager - {navItems.slice(0, 4).map(item => ( + {navItems.slice(0, 5).map(item => ( } /> + } /> { + fetchItems(); + }, []); + + const fetchItems = async () => { + setLoading(true); + try { + const response = await axios.get(`${API_URL}/domains-new`); + setItems(response.data); + setError(''); + } catch (error) { + console.error('Error fetching domains-new:', error); + setError('Не удалось загрузить домены. Проверьте, запущен ли бэкенд.'); + } finally { + setLoading(false); + } + }; + + const handleAddItem = () => { + if (newItem.domain.trim() === '') { + setError('Имя домена не может быть пустым.'); + return; + } + if (newItem.community.trim() === '') { + setError('Community не может быть пустым.'); + return; + } + setError(''); + setItems([...items, newItem]); + setNewItem({ domain: '', community: '' }); + }; + + const handleEdit = (item) => { + setEditingDomain(item.domain); + setEditingValue(item.community); + }; + + const handleSaveEdit = (domainName) => { + const updatedItems = items.map(i => + i.domain === domainName ? { ...i, community: editingValue } : i + ); + setItems(updatedItems); + setEditingDomain(null); + }; + + const handleCancelEdit = () => { + setEditingDomain(null); + }; + + const handleDeleteItem = (domainNameToDelete) => { + setItems(items.filter(i => i.domain !== domainNameToDelete)); + }; + + const confirmDelete = (item) => { + setItemToDelete(item); + setShowDeleteModal(true); + }; + + const executeDelete = () => { + if (itemToDelete) { + handleDeleteItem(itemToDelete.domain); + setShowDeleteModal(false); + setItemToDelete(null); + } + }; + + const handleSaveChanges = async () => { + setLoading(true); + try { + await axios.post(`${API_URL}/domains-new`, { domains: items }); + 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.domain.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 (editingDomain && editInputRef.current) { + editInputRef.current.focus(); + } + }, [editingDomain]); + + const handleEditKeyDown = (e, domainName) => { + if (e.key === 'Enter') handleSaveEdit(domainName); + if (e.key === 'Escape') handleCancelEdit(); + }; + + return ( + <> + {error && ( + + + + + + + {error} + + + setError('')}> + + )} + + {success && ( + + + + + + + {success} + + + setSuccess('')}> + + )} + + + + {/* Add New Item Card */} + + + + + Добавить новый домен + + + + { e.preventDefault(); handleAddItem(); }}> + + Домен + setNewItem({ ...newItem, domain: e.target.value })} + /> + + + Community + setNewItem({ ...newItem, community: e.target.value })} + /> + + + + + Добавить + + + + + + + {/* Actions Card */} + + + + + Действия + + + + + + {loading ? ( + <> + + Сохранение... + > + ) : ( + <> + + Сохранить в S3 + > + )} + + + + Обновить + + + + + + + + + + Список доменов + + {/* Фильтр по community */} + { setFilterCommunity(e.target.value); setCurrentPage(1); }}> + Все community + {allCommunities.map(community => ( + {community} + ))} + + {/* Поиск */} + + + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} + /> + + + + + + + + handleSort('domain')}> + Домен + {sortField === 'domain' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + + handleSort('community')}> + Community + {sortField === 'community' && ( + + {sortOrder === 'asc' ? '▲' : '▼'} + + )} + + + + + + {paginatedItems.map((item) => ( + + {item.domain} + {item.community} + + {editingDomain === item.domain ? ( + <> + setEditingValue(e.target.value)} + onKeyDown={e => handleEditKeyDown(e, item.domain)} + style={{maxWidth: 120}} + /> + handleSaveEdit(item.domain)}> + + > + ) : ( + <> + handleEdit(item)}> + confirmDelete(item)}> + > + )} + + + ))} + + + + {/* Пагинация */} + {totalPages > 1 && ( + + + + setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>« + + {Array.from({length: totalPages}, (_, i) => i + 1).map(page => ( + + setCurrentPage(page)}>{page} + + ))} + + setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>» + + + + )} + + + + + {/* Delete Confirmation Modal */} + {showDeleteModal && ( + + + + setShowDeleteModal(false)} + > + + + + Удалить домен? + + Вы уверены, что хотите удалить "{itemToDelete?.domain}"? Это действие необратимо. + + + + + + + setShowDeleteModal(false)} + > + Отмена + + + + + Удалить + + + + + + + + + )} + > + ); +} + +export default DomainsNewManager; \ No newline at end of file