feat: Enhance DomainsNewManager with validation for domain and community fields, add import/export functionality, and improve UI with new icons and feedback messages
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m8s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m8s
This commit is contained in:
@@ -8,7 +8,10 @@ import {
|
||||
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 DomainsNewManager() {
|
||||
const [items, setItems] = useState([]);
|
||||
const [newItem, setNewItem] = useState({ domain: '', community: '' });
|
||||
const [newInvalid, setNewInvalid] = useState({ domain: false, community: false });
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -31,6 +35,14 @@ function DomainsNewManager() {
|
||||
const editInputRef = useRef(null);
|
||||
const pageSize = 10;
|
||||
|
||||
const isValidDomain = (value) => {
|
||||
const v = String(value).trim().toLowerCase();
|
||||
// Простой валидатор FQDN: sub.domain.tld
|
||||
return /^([a-z0-9-]+\.)+[a-z]{2,}$/i.test(v);
|
||||
};
|
||||
|
||||
const isValidCommunity = (value) => /^[0-9]+$/.test(String(value).trim());
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, []);
|
||||
@@ -50,17 +62,17 @@ function DomainsNewManager() {
|
||||
};
|
||||
|
||||
const handleAddItem = () => {
|
||||
if (newItem.domain.trim() === '') {
|
||||
setError('Имя домена не может быть пустым.');
|
||||
return;
|
||||
}
|
||||
if (newItem.community.trim() === '') {
|
||||
setError('Community не может быть пустым.');
|
||||
const domainOk = isValidDomain(newItem.domain);
|
||||
const communityOk = isValidCommunity(newItem.community);
|
||||
setNewInvalid({ domain: !domainOk, community: !communityOk });
|
||||
if (!domainOk || !communityOk) {
|
||||
setError('Заполните корректный домен (example.com) и числовой community.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setItems([...items, newItem]);
|
||||
setItems([...items, { domain: newItem.domain.trim().toLowerCase(), community: String(newItem.community).trim() }]);
|
||||
setNewItem({ domain: '', community: '' });
|
||||
setNewInvalid({ domain: false, community: false });
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
@@ -69,8 +81,12 @@ function DomainsNewManager() {
|
||||
};
|
||||
|
||||
const handleSaveEdit = (domainName) => {
|
||||
if (!isValidCommunity(editingValue)) {
|
||||
setError('Community должен быть числом.');
|
||||
return;
|
||||
}
|
||||
const updatedItems = items.map(i =>
|
||||
i.domain === domainName ? { ...i, community: editingValue } : i
|
||||
i.domain === domainName ? { ...i, community: String(editingValue).trim() } : i
|
||||
);
|
||||
setItems(updatedItems);
|
||||
setEditingDomain(null);
|
||||
@@ -100,7 +116,9 @@ function DomainsNewManager() {
|
||||
const handleSaveChanges = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await axios.post(`${API_URL}/domains-new`, { domains: items });
|
||||
// сохраняем только валидные строки
|
||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community));
|
||||
await axios.post(`${API_URL}/domains-new`, { domains: valid });
|
||||
setSuccess('Изменения успешно сохранены!');
|
||||
setTimeout(() => setSuccess(''), 3000);
|
||||
} catch (error) {
|
||||
@@ -111,6 +129,37 @@ function DomainsNewManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
const text = window.prompt('Вставьте строки: DOMAIN ПРОБЕЛ COMMUNITY (по одной записи на строку)');
|
||||
if (!text) return;
|
||||
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
||||
const parsed = lines.map(l => {
|
||||
const [d, c] = l.split(/\s+/);
|
||||
return { domain: (d || '').toLowerCase(), community: c || '' };
|
||||
});
|
||||
setItems(prev => [...prev, ...parsed]);
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
const header = ['domain', 'community'];
|
||||
const csv = [header, ...items.map(i => [i.domain, 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 = `domains_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const clearInvalid = () => {
|
||||
setItems(prev => prev.filter(i => i.domain || i.community)
|
||||
.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||
);
|
||||
};
|
||||
|
||||
// Сортировка
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
let valA = a[sortField] || '';
|
||||
@@ -207,21 +256,24 @@ function DomainsNewManager() {
|
||||
<label className="form-label">Домен</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
className={`form-control${newInvalid.domain ? ' is-invalid' : ''}`}
|
||||
placeholder="example.com"
|
||||
value={newItem.domain}
|
||||
onChange={(e) => setNewItem({ ...newItem, domain: e.target.value })}
|
||||
/>
|
||||
{newInvalid.domain && <div className="invalid-feedback">Неверный формат домена</div>}
|
||||
<div className="form-text">Без http/https, только доменное имя</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="100"
|
||||
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">
|
||||
@@ -255,7 +307,7 @@ function DomainsNewManager() {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IconDatabase className="icon me-2" />
|
||||
<IconDeviceFloppy className="icon me-2" />
|
||||
Сохранить в S3
|
||||
</>
|
||||
)}
|
||||
@@ -268,6 +320,28 @@ function DomainsNewManager() {
|
||||
<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>
|
||||
@@ -276,7 +350,7 @@ function DomainsNewManager() {
|
||||
<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">Список доменов</h3>
|
||||
<h3 className="card-title mb-0">Список доменов <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); }}>
|
||||
@@ -333,14 +407,14 @@ function DomainsNewManager() {
|
||||
<>
|
||||
<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.domain)}
|
||||
style={{maxWidth: 120}}
|
||||
/>
|
||||
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.domain)}><IconCheck size={18} /></button>
|
||||
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.domain)} disabled={!isValidCommunity(editingValue)}><IconCheck size={18} /></button>
|
||||
<button className="btn btn-secondary btn-icon" onClick={handleCancelEdit}><IconX size={18} /></button>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user