diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index fc36d91..3b205f4 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -29,6 +29,8 @@ import ConfirmDiffModal from './components/ConfirmDiffModal.jsx'; import HistoryModal from './components/HistoryModal.jsx'; import ErrorAlert from './components/ErrorAlert.jsx'; import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; +import ImportModal from './components/ImportModal.jsx'; +import QuickAddBar from './components/QuickAddBar.jsx'; const API_URL = '/api'; @@ -269,22 +271,7 @@ 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 || '').trim(), community: (c || '').trim() }; - }).filter(i => isValidAsn(i.asn) && isValidCommunity(i.community)); - setItems(prev => { - const merged = [...prev, ...parsed]; - const map = new Map(); - for (const it of merged) { - const key = String(it.asn).trim(); - if (!map.has(key)) map.set(key, { asn: key, community: String(it.community).trim() }); - } - return Array.from(map.values()); - }); + setImportOpen(true); }; const handleExport = async () => { @@ -309,7 +296,7 @@ function ASNsNewManager() { } }; - // Drag&Drop импорт + // Drag&Drop импорт (быстрый путь) const onDropImport = async (e) => { e.preventDefault(); const file = e.dataTransfer?.files?.[0]; @@ -337,6 +324,26 @@ function ASNsNewManager() { ); }; + // ===== Импорт через модалку ===== + const [importOpen, setImportOpen] = useState(false); + const parseLine = (line) => { + const [a, c] = String(line).split(/\s+/); + return { asn: (a || '').trim(), community: (c || '').trim() }; + }; + const validateItem = (obj) => isValidAsn(obj.asn) && isValidCommunity(obj.community); + const applyImportedItems = (list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.asn).trim(); + if (!map.has(key)) map.set(key, { asn: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + setImportOpen(false); + }; + // Сортировка const sortedItems = [...items].sort((a, b) => { let valA = a[sortField] || ''; @@ -427,6 +434,8 @@ function ASNsNewManager() { actions={(
- {/* Add New ASN Card */} -
-
-

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

-
-
-
{ e.preventDefault(); handleAddItem(); }}> -
- - setNewItem({ ...newItem, asn: e.target.value })} - /> - {newInvalid.asn &&
Только цифры
} -
Формат: только цифры, например 12345
-
-
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} -
Формат: N или N:N (например 65000:100)
- {(() => { - const match = communities.find(c => c.value === String(newItem.community).trim()); - if (!match) return null; - return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} -
- ); - })()} -
-
- -
-
-
-
+ { const [a, c] = String(line).split(/\s+/); return { asn: (a||'').trim(), community: (c||'').trim() }; }} + validateItem={(obj) => isValidAsn(obj.asn) && isValidCommunity(obj.community)} + onApply={(list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.asn).trim(); + if (!map.has(key)) map.set(key, { asn: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + }} + /> {/* Drag & Drop импорт */}
setConfirmSaveOpen(false)} /> + setImportOpen(false)} + sampleHeader={['asn','community']} + placeholder={'12345 65000:100'} + /> { - 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 || '').trim() }; - }).filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)); - // merge and dedup - setItems(prev => { - const merged = [...prev, ...parsed]; - const map = new Map(); - for (const it of merged) { - const key = String(it.domain).trim().toLowerCase(); - if (!map.has(key)) map.set(key, { domain: key, community: String(it.community).trim() }); - } - return Array.from(map.values()); - }); + setImportOpen(true); }; const handleExport = async () => { @@ -343,6 +329,26 @@ function DomainsNewManager() { ); }; + // ===== Импорт через модалку ===== + const [importOpen, setImportOpen] = useState(false); + const parseLine = (line) => { + const [d, c] = String(line).split(/\s+/); + return { domain: (d || '').toLowerCase(), community: (c || '').trim() }; + }; + const validateItem = (obj) => isValidDomain(obj.domain) && isValidCommunity(obj.community); + const applyImportedItems = (list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.domain).trim().toLowerCase(); + if (!map.has(key)) map.set(key, { domain: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + setImportOpen(false); + }; + // Сортировка const sortedItems = [...items].sort((a, b) => { let valA = a[sortField] || ''; @@ -433,6 +439,8 @@ function DomainsNewManager() { actions={(
- {/* Add New Item Card */} -
-
-

- - Добавить новый домен -

-
-
-
{ e.preventDefault(); handleAddItem(); }}> -
- - setNewItem({ ...newItem, domain: e.target.value })} - /> - {newInvalid.domain &&
Неверный формат домена
} -
Без http/https, только доменное имя
-
Формат: sub.domain.tld (например foo.example.org)
-
-
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} -
Формат: N или N:N (например 65000:100)
- {(() => { - const match = communities.find(c => c.value === String(newItem.community).trim()); - if (!match) return null; - return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} -
- ); - })()} -
-
- -
-
-
-
+ { const [d, c] = String(line).split(/\s+/); return { domain: (d||'').toLowerCase(), community: (c||'').trim() }; }} + validateItem={(obj) => isValidDomain(obj.domain) && isValidCommunity(obj.community)} + onApply={(list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.domain).trim().toLowerCase(); + if (!map.has(key)) map.set(key, { domain: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + }} + /> {/* Карточка действий больше не нужна — действия перенесены в page-header */} {/* Drag & Drop импорт */} @@ -722,6 +690,17 @@ function DomainsNewManager() { onConfirm={performSave} onClose={() => setConfirmSaveOpen(false)} /> + setImportOpen(false)} + sampleHeader={['domain','community']} + placeholder={'example.com 65000:100'} + /> { - const text = window.prompt('Вставьте строки: CIDR ПРОБЕЛ COMMUNITY (по одной записи на строку)'); - if (!text) return; - const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); - const parsed = lines.map(l => { - const [cidr, c] = l.split(/\s+/); - return { ipRange: (cidr || '').trim(), community: (c || '').trim() }; - }).filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)); - setItems(prev => { - const merged = [...prev, ...parsed]; - const map = new Map(); - for (const it of merged) { - const key = String(it.ipRange).trim(); - if (!map.has(key)) map.set(key, { ipRange: key, community: String(it.community).trim() }); - } - return Array.from(map.values()); - }); + setImportOpen(true); }; const handleExport = async () => { @@ -328,7 +315,7 @@ function IPRangesManager() { } }; - // Drag&Drop импорт + // Drag&Drop импорт (оставим как быстрый путь) const onDropImport = async (e) => { e.preventDefault(); const file = e.dataTransfer?.files?.[0]; @@ -356,6 +343,26 @@ function IPRangesManager() { ); }; + // ===== Импорт через модалку ===== + const [importOpen, setImportOpen] = useState(false); + const parseLine = (line) => { + const [cidr, c] = String(line).split(/\s+/); + return { ipRange: (cidr || '').trim(), community: (c || '').trim() }; + }; + const validateItem = (obj) => isValidCidr(obj.ipRange) && isValidCommunity(obj.community); + const applyImportedItems = (list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.ipRange).trim(); + if (!map.has(key)) map.set(key, { ipRange: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + setImportOpen(false); + }; + // Сортировка const sortedItems = [...items].sort((a, b) => { let valA = a[sortField] || ''; @@ -684,6 +691,8 @@ function IPRangesManager() { actions={(
- {/* Add New Item Card */} -
-
-

- - Добавить новый IP-диапазон -

-
-
-
{ e.preventDefault(); handleAddItem(); }}> -
- - setNewItem({ ...newItem, ipRange: e.target.value })} - /> - {newInvalid.ipRange &&
Некорректный CIDR
} -
Формат: A.B.C.D/Маска, например 10.0.0.0/24
-
-
- - setNewItem({ ...newItem, community: v })} - communities={communities} - placeholder="Начните вводить номер или имя" - className={`form-control${newInvalid.community ? ' is-invalid' : ''}`} - /> - {newInvalid.community &&
Только цифры
} - {(() => { - const match = communities.find(c => c.value === String(newItem.community).trim()); - if (!match) return null; - return ( -
- {match.name && (<>{match.name} — )} - {match.description || ''} - {match.tags && match.tags.length > 0 && ( - <> ({match.tags.join(', ')}) - )} -
- ); - })()} -
-
- -
-
-
-
+ { const [cidr, c] = String(line).split(/\s+/); return { ipRange: (cidr||'').trim(), community: (c||'').trim() }; }} + validateItem={(obj) => isValidCidr(obj.ipRange) && isValidCommunity(obj.community)} + onApply={(list) => { + setItems(prev => { + const merged = [...prev, ...list]; + const map = new Map(); + for (const it of merged) { + const key = String(it.ipRange).trim(); + if (!map.has(key)) map.set(key, { ipRange: key, community: String(it.community).trim() }); + } + return Array.from(map.values()); + }); + }} + /> {/* Drag & Drop импорт */}
setConfirmSaveOpen(false)} /> + setImportOpen(false)} + sampleHeader={['ipRange','community']} + placeholder={'192.168.1.0/24 65000:100'} + /> { + if (show) { + setText(''); + setParsed({ items: [], invalid: [] }); + setFileName(''); + setDragOver(false); + setTimeout(() => textAreaRef.current?.focus(), 0); + } + }, [show]); + + if (!show) return null; + + const parseText = (raw) => { + const lines = String(raw || '') + .split(/\r?\n/) + .map(l => l.trim()) + .filter(Boolean); + const items = []; + const invalid = []; + for (const line of lines) { + const obj = parseLine(line); + if (obj && validateItem(obj)) items.push(obj); else invalid.push(line); + } + setParsed({ items, invalid }); + }; + + const handleDrop = async (e) => { + e.preventDefault(); + setDragOver(false); + const file = e.dataTransfer?.files?.[0]; + if (!file) return; + setFileName(file.name); + const content = await file.text(); + setText(content); + parseText(content); + }; + + const handleChange = (value) => { + setText(value); + parseText(value); + }; + + const handleConfirm = () => { + if (parsed.items.length === 0) return; + onConfirm?.(parsed.items); + }; + + const downloadSample = () => { + const csv = [sampleHeader, ['example', '65000:100']] + .map(r => r.map(x => `"${String(x ?? '').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 = 'sample.csv'; + a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+
+
+ +
+

{title}

+
+
+
{description}
+
{ e.preventDefault(); setDragOver(true); }} + onDragLeave={() => setDragOver(false)} + onDrop={handleDrop} + > +
+
+ {fileName ? `Файл: ${fileName}` : 'Перетащите TXT/CSV сюда или вставьте текст ниже'} +
+ +
+
+