feat: Enhance ASNsNewManager with input validation, import/export functionality, and improved UI feedback for better user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m32s

This commit is contained in:
2025-08-08 10:56:31 +07:00
parent 3c185509b2
commit 6d7ed80646
4 changed files with 122 additions and 62 deletions
+90 -23
View File
@@ -1,14 +1,17 @@
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
IconRefresh,
IconUpload,
IconDownload,
IconDeviceFloppy
} from '@tabler/icons-react';
const API_URL = '/api';
@@ -16,6 +19,7 @@ const API_URL = '/api';
function ASNsNewManager() {
const [items, setItems] = useState([]);
const [newItem, setNewItem] = useState({ asn: '', community: '' });
const [newInvalid, setNewInvalid] = useState({ asn: false, community: false });
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const [searchTerm, setSearchTerm] = useState('');
@@ -31,6 +35,9 @@ function ASNsNewManager() {
const editInputRef = useRef(null);
const pageSize = 10;
const isValidAsn = (value) => /^[0-9]+$/.test(String(value).trim());
const isValidCommunity = (value) => /^[0-9]+$/.test(String(value).trim());
useEffect(() => {
fetchItems();
}, []);
@@ -50,17 +57,17 @@ function ASNsNewManager() {
};
const handleAddItem = () => {
if (newItem.asn.trim() === '') {
setError('Номер AS не может быть пустым.');
return;
}
if (newItem.community.trim() === '') {
setError('Community не может быть пустым.');
const asnOk = isValidAsn(newItem.asn);
const communityOk = isValidCommunity(newItem.community);
setNewInvalid({ asn: !asnOk, community: !communityOk });
if (!asnOk || !communityOk) {
setError('Введите корректный номер ASN и числовой community.');
return;
}
setError('');
setItems([...items, newItem]);
setItems([...items, { asn: String(newItem.asn).trim(), community: String(newItem.community).trim() }]);
setNewItem({ asn: '', community: '' });
setNewInvalid({ asn: false, community: false });
};
const handleEdit = (item) => {
@@ -69,8 +76,12 @@ function ASNsNewManager() {
};
const handleSaveEdit = (asn) => {
if (!isValidCommunity(editingValue)) {
setError('Community должен быть числом.');
return;
}
const updatedItems = items.map(i =>
i.asn === asn ? { ...i, community: editingValue } : i
i.asn === asn ? { ...i, community: String(editingValue).trim() } : i
);
setItems(updatedItems);
setEditingAsn(null);
@@ -101,7 +112,8 @@ function ASNsNewManager() {
setLoading(true);
try {
// API ожидает domains: [{domain, type}]
await axios.post(`${API_URL}/asns`, { domains: items.map(i => ({ domain: i.asn, type: i.community })) });
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community));
await axios.post(`${API_URL}/asns`, { domains: valid.map(i => ({ domain: i.asn, type: i.community })) });
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
} catch (error) {
@@ -112,6 +124,37 @@ function ASNsNewManager() {
}
};
const handleImport = () => {
const text = window.prompt('Вставьте строки: ASN ПРОБЕЛ COMMUNITY (по одной записи на строку)');
if (!text) return;
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
const parsed = lines.map(l => {
const [a, c] = l.split(/\s+/);
return { asn: a || '', community: c || '' };
});
setItems(prev => [...prev, ...parsed]);
};
const handleExport = () => {
const header = ['asn', 'community'];
const csv = [header, ...items.map(i => [i.asn, 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 = `asns_${new Date().toISOString().split('T')[0]}.csv`;
a.click();
URL.revokeObjectURL(url);
};
const clearInvalid = () => {
setItems(prev => prev.filter(i => i.asn || i.community)
.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
);
};
// Сортировка
const sortedItems = [...items].sort((a, b) => {
let valA = a[sortField] || '';
@@ -208,21 +251,23 @@ function ASNsNewManager() {
<label className="form-label">Номер AS</label>
<input
type="text"
className="form-control"
className={`form-control${newInvalid.asn ? ' is-invalid' : ''}`}
placeholder="12345"
value={newItem.asn}
onChange={(e) => setNewItem({ ...newItem, asn: e.target.value })}
/>
{newInvalid.asn && <div className="invalid-feedback">Только цифры</div>}
</div>
<div className="mb-3">
<label className="form-label">Community</label>
<input
type="text"
className="form-control"
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
placeholder="112"
value={newItem.community}
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
/>
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
</div>
<div className="form-footer">
<button type="submit" className="btn btn-primary w-100">
@@ -256,7 +301,7 @@ function ASNsNewManager() {
</>
) : (
<>
<IconDatabase className="icon me-2" />
<IconDeviceFloppy className="icon me-2" />
Сохранить в S3
</>
)}
@@ -269,6 +314,28 @@ function ASNsNewManager() {
<IconRefresh className="icon me-2" />
Обновить
</button>
<button
className="btn btn-outline-primary"
onClick={handleImport}
>
<IconUpload className="icon me-2" />
Импорт из буфера
</button>
<button
className="btn btn-outline-primary"
onClick={handleExport}
disabled={items.length === 0}
>
<IconDownload className="icon me-2" />
Экспорт CSV
</button>
<button
className="btn btn-outline-secondary"
onClick={clearInvalid}
disabled={items.length === 0}
>
Очистить пустые/невалидные
</button>
</div>
</div>
</div>
@@ -277,7 +344,7 @@ function ASNsNewManager() {
<div className="col-lg-9">
<div className="card">
<div className="card-header d-flex justify-content-between align-items-center">
<h3 className="card-title mb-0">Список ASN</h3>
<h3 className="card-title mb-0">Список ASN <span className="badge bg-blue-lt text-blue ms-2">{items.length}</span></h3>
<div className="d-flex gap-2 w-50">
{/* Фильтр по community */}
<select className="form-select w-auto" value={filterCommunity} onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }}>
@@ -334,14 +401,14 @@ function ASNsNewManager() {
<>
<input
type="text"
className="form-control d-inline-block w-auto me-2"
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
value={editingValue}
ref={editInputRef}
onChange={e => setEditingValue(e.target.value)}
onKeyDown={e => handleEditKeyDown(e, item.asn)}
style={{maxWidth: 120}}
/>
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.asn)}><IconCheck size={18} /></button>
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.asn)} disabled={!isValidCommunity(editingValue)}><IconCheck size={18} /></button>
<button className="btn btn-secondary btn-icon" onClick={handleCancelEdit}><IconX size={18} /></button>
</>
) : (