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
-
-
-
-
+
{ 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 */}
-
-
-
-
- Добавить новый домен
-
-
-
-
+
{ 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-диапазон
-
-
-
-
+
{ 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 сюда или вставьте текст ниже'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ImportModal;
+
+
diff --git a/frontend/src/components/PageHeaderActions.jsx b/frontend/src/components/PageHeaderActions.jsx
index 5993cbc..7b62206 100644
--- a/frontend/src/components/PageHeaderActions.jsx
+++ b/frontend/src/components/PageHeaderActions.jsx
@@ -6,7 +6,8 @@ import {
IconPlayerPlay,
IconBolt,
IconEraser,
- IconDots
+ IconDots,
+ IconEye
} from '@tabler/icons-react';
import PortalDropdown from './PortalDropdown.jsx';
import { IconClock } from '@tabler/icons-react';
@@ -98,6 +99,11 @@ function PageHeaderActions({
+ {onPreview && (
+
+ )}
{onRefresh && (