diff --git a/backend/server.js b/backend/server.js index 4e57d56..b7f07b9 100644 --- a/backend/server.js +++ b/backend/server.js @@ -76,6 +76,56 @@ app.post('/api/domains', async (req, res) => { } }); +// --- ASNs Routes --- + +// Get ASNs from S3 +app.get('/api/asns', async (req, res) => { + const params = { + Bucket: BUCKET_NAME, + Key: 'asns.txt', + }; + + try { + const data = await s3.getObject(params).promise(); + const fileContent = data.Body.toString('utf-8'); + const asns = fileContent.split('\n').filter(line => line).map(line => { + const parts = line.trim().split(/\s+/); + const domain = parts[0] || ''; // Keep name 'domain' for consistency in component + const type = parts[1] || ''; + return { domain, type }; + }); + res.json(asns); + } catch (error) { + if (error.code === 'NoSuchKey') { + res.json([]); + } else { + console.error(error); + res.status(500).send('Error reading from S3'); + } + } +}); + +// Update ASNs in S3 +app.post('/api/asns', async (req, res) => { + const { domains: asns } = req.body; // Keep name 'domains' for consistency + const fileContent = asns.map(a => `${a.domain} ${a.type}`).join('\n'); + + const params = { + Bucket: BUCKET_NAME, + Key: 'asns.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'); + } +}); + // The "catchall" handler: for any request that doesn't // match one above, send back React's index.html file. app.get('*', (req, res) => { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d6bdf01..49d5c06 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,97 +1,7 @@ -import { useState, useEffect } from 'react' -import axios from 'axios' -import { - Container, Navbar, Table, Button, Form, InputGroup, Row, Col, Card, Alert -} from 'react-bootstrap' - -const API_URL = '/api' +import { Container, Navbar, Tabs, Tab } from 'react-bootstrap'; +import DataManager from './DataManager'; function App() { - const [domains, setDomains] = useState([]) - const [newDomain, setNewDomain] = useState({ domain: '', type: 'SharkFIN' }) - const [error, setError] = useState('') - const [success, setSuccess] = useState('') - const [searchTerm, setSearchTerm] = useState('') - const [editingDomain, setEditingDomain] = useState(null) - const [editingValue, setEditingValue] = useState('') - const [bulkFrom, setBulkFrom] = useState('SharkFIN') - const [bulkTo, setBulkTo] = useState('SharkAM') - - useEffect(() => { - fetchDomains() - }, []) - - const fetchDomains = async () => { - try { - const response = await axios.get(`${API_URL}/domains`) - setDomains(response.data) - } catch (error) { - console.error('Error fetching domains:', error) - setError('Не удалось загрузить домены. Проверьте, запущен ли бэкенд.') - } - } - - const handleAddDomain = () => { - if (newDomain.domain.trim() === '') { - setError('Имя домена не может быть пустым.') - return - } - setError('') - setDomains([...domains, newDomain]) - setNewDomain({ domain: '', type: 'SharkFIN' }) - } - - const handleEdit = (domain) => { - setEditingDomain(domain.domain) - setEditingValue(domain.type) - } - - const handleSaveEdit = (domainName) => { - const updatedDomains = domains.map(d => - d.domain === domainName ? { ...d, type: editingValue } : d - ) - setDomains(updatedDomains) - setEditingDomain(null) - } - - const handleCancelEdit = () => { - setEditingDomain(null) - } - - const handleDeleteDomain = (domainNameToDelete) => { - setDomains(domains.filter(d => d.domain !== domainNameToDelete)) - } - - const handleSaveChanges = async () => { - try { - await axios.post(`${API_URL}/domains`, { domains }) - setSuccess('Изменения успешно сохранены!') - setTimeout(() => setSuccess(''), 3000) - } catch (error) { - console.error('Error saving changes:', error) - setError('Не удалось сохранить изменения.') - } - } - - const handleBulkUpdate = () => { - if (!bulkFrom || !bulkTo) { - setError('Оба поля для массового обновления должны быть заполнены.') - return - } - const confirm = window.confirm(`Вы уверены, что хотите заменить все шлюзы "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`) - if (confirm) { - const updatedDomains = domains.map(d => - d.type === bulkFrom ? { ...d, type: bulkTo } : d - ) - setDomains(updatedDomains) - setSuccess(`Шлюзы "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`) - } - } - - const filteredDomains = domains.filter(d => - d.domain.toLowerCase().includes(searchTerm.toLowerCase()) - ) - return ( <> @@ -101,149 +11,31 @@ function App() { - S3 Менеджер Доменов + S3 Lists Manager - {error && setError('')} dismissible>{error}} - {success && setSuccess('')} dismissible>{success}} - - - Добавить новый домен - -
{ e.preventDefault(); handleAddDomain(); }}> - - - - Имя домена - setNewDomain({ ...newDomain, domain: e.target.value })} - /> - - - - - Шлюз - setNewDomain({ ...newDomain, type: e.target.value })} - /> - - - - - - -
-
-
- - - Массовое обновление шлюзов - - - - - Заменить с - setBulkFrom(e.target.value)} /> - - - - - Заменить на - setBulkTo(e.target.value)} /> - - - - - - - - - - - -
-
Список доменов
- -
-
- - setSearchTerm(e.target.value)} + + + - - - - - - - - - - - {filteredDomains.map((d, index) => ( - - - - - - - ))} - -
#ДоменШлюзДействия
{index + 1}{d.domain} - {editingDomain === d.domain ? ( - setEditingValue(e.target.value)} - autoFocus - /> - ) : ( - d.type - )} - - {editingDomain === d.domain ? ( - <> - - - - ) : ( - <> - - - - )} -
- {domains.length > 0 && filteredDomains.length === 0 && ( -

По вашему запросу ничего не найдено.

- )} - {domains.length === 0 &&

Домены не найдены. Добавьте новый домен в форме выше.

} -
-
+ + + + +
- ) + ); } -export default App +export default App; diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx new file mode 100644 index 0000000..b92e333 --- /dev/null +++ b/frontend/src/DataManager.jsx @@ -0,0 +1,224 @@ +import { useState, useEffect } from 'react'; +import axios from 'axios'; +import { Table, Button, Form, Row, Col, Card, Alert } from 'react-bootstrap'; + +const API_URL = '/api'; + +function DataManager({ entityName, entityKey, placeholder }) { + const [items, setItems] = useState([]); + const [newItem, setNewItem] = useState({ domain: '', type: 'SharkFIN' }); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); + const [editingDomain, setEditingDomain] = useState(null); + const [editingValue, setEditingValue] = useState(''); + const [bulkFrom, setBulkFrom] = useState('SharkFIN'); + const [bulkTo, setBulkTo] = useState('SharkAM'); + + useEffect(() => { + fetchItems(); + }, []); + + const fetchItems = async () => { + try { + const response = await axios.get(`${API_URL}/${entityKey}`); + setItems(response.data); + } catch (error) { + console.error(`Error fetching ${entityKey}:`, error); + setError(`Не удалось загрузить ${entityName}. Проверьте, запущен ли бэкенд.`); + } + }; + + const handleAddItem = () => { + if (newItem.domain.trim() === '') { + setError(`Имя ${entityName} не может быть пустым.`); + return; + } + setError(''); + setItems([...items, newItem]); + setNewItem({ domain: '', type: 'SharkFIN' }); + }; + + const handleEdit = (item) => { + setEditingDomain(item.domain); + setEditingValue(item.type); + }; + + const handleSaveEdit = (domainName) => { + const updatedItems = items.map(i => + i.domain === domainName ? { ...i, type: editingValue } : i + ); + setItems(updatedItems); + setEditingDomain(null); + }; + + const handleCancelEdit = () => { + setEditingDomain(null); + }; + + const handleDeleteItem = (domainNameToDelete) => { + setItems(items.filter(i => i.domain !== domainNameToDelete)); + }; + + const handleSaveChanges = async () => { + try { + // The backend expects a payload with a 'domains' key for both endpoints. + await axios.post(`${API_URL}/${entityKey}`, { domains: items }); + setSuccess('Изменения успешно сохранены!'); + setTimeout(() => setSuccess(''), 3000); + } catch (error) { + console.error('Error saving changes:', error); + setError('Не удалось сохранить изменения.'); + } + }; + + const handleBulkUpdate = () => { + if (!bulkFrom || !bulkTo) { + setError('Оба поля для массового обновления должны быть заполнены.'); + return; + } + const confirm = window.confirm(`Вы уверены, что хотите заменить все шлюзы "${bulkFrom}" на "${bulkTo}"? Это действие необратимо.`); + if (confirm) { + const updatedItems = items.map(i => + i.type === bulkFrom ? { ...i, type: bulkTo } : i + ); + setItems(updatedItems); + setSuccess(`Шлюзы "${bulkFrom}" были успешно заменены на "${bulkTo}". Не забудьте сохранить изменения.`); + } + }; + + const filteredItems = items.filter(i => + i.domain.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( + <> + {error && setError('')} dismissible>{error}} + {success && setSuccess('')} dismissible>{success}} + + + + + Добавить новый {entityName} + +
{ e.preventDefault(); handleAddItem(); }}> + + Имя {entityName} + setNewItem({ ...newItem, domain: e.target.value })} + /> + + + Шлюз + setNewItem({ ...newItem, type: e.target.value })} + /> + + +
+
+
+ + + Массовое обновление + + + Заменить с + setBulkFrom(e.target.value)} /> + + + Заменить на + setBulkTo(e.target.value)} /> + + + + + + + + + +
+
Список: {entityName}
+ +
+
+ + setSearchTerm(e.target.value)} + /> + + + + + + + + + + + {filteredItems.map((item, index) => ( + + + + + + + ))} + +
#{entityName}ШлюзДействия
{index + 1}{item.domain} + {editingDomain === item.domain ? ( + setEditingValue(e.target.value)} + autoFocus + /> + ) : ( + item.type + )} + + {editingDomain === item.domain ? ( + <> + + + + ) : ( + <> + + + + )} +
+ {items.length > 0 && filteredItems.length === 0 && ( +

По вашему запросу ничего не найдено.

+ )} + {items.length === 0 &&

Список пуст. Добавьте новый элемент.

} +
+
+ +
+ + ); +} + +export default DataManager; \ No newline at end of file