feat: Добавить модальные окна для импорта данных и компонент QuickAddBar в менеджерах ASNs, Domains и IPRanges, улучшив пользовательский интерфейс и упрощая процесс добавления записей
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
function ImportModal({
|
||||
show,
|
||||
title = 'Импорт',
|
||||
description = 'Вставьте текст или перетащите файл TXT/CSV. Формат: VALUE COMMUNITY',
|
||||
parseLine,
|
||||
validateItem,
|
||||
onConfirm,
|
||||
onClose,
|
||||
sampleHeader = ['value', 'community'],
|
||||
placeholder = 'value community\nvalue community',
|
||||
}) {
|
||||
const [text, setText] = useState('');
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [parsed, setParsed] = useState({ items: [], invalid: [] });
|
||||
const [fileName, setFileName] = useState('');
|
||||
const textAreaRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">
|
||||
<div className="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div className="modal-content">
|
||||
<button type="button" className="btn-close" onClick={onClose}></button>
|
||||
<div className="modal-header">
|
||||
<h3 className="modal-title">{title}</h3>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="mb-2 text-muted">{description}</div>
|
||||
<div
|
||||
className={`mb-3 border-dashed rounded p-3 ${dragOver ? 'bg-blue-lt' : ''}`}
|
||||
onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
|
||||
onDragLeave={() => setDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="d-flex align-items-center justify-content-between">
|
||||
<div className="me-3 text-muted">
|
||||
{fileName ? `Файл: ${fileName}` : 'Перетащите TXT/CSV сюда или вставьте текст ниже'}
|
||||
</div>
|
||||
<label className="btn btn-outline-primary mb-0">
|
||||
Выбрать файл
|
||||
<input type="file" accept=".txt,.csv,.log" hidden onChange={async (e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (!f) return;
|
||||
setFileName(f.name);
|
||||
const t = await f.text();
|
||||
setText(t);
|
||||
parseText(t);
|
||||
}} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textAreaRef}
|
||||
className="form-control"
|
||||
rows={8}
|
||||
value={text}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
<div className="row mt-3 g-3">
|
||||
<div className="col-md-6">
|
||||
<div className="card"><div className="card-body">
|
||||
<div className="text-muted">Готово к импорту</div>
|
||||
<div className="h2 m-0">{parsed.items.length}</div>
|
||||
</div></div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="card"><div className="card-body">
|
||||
<div className="text-muted">Пропущено (ошибки)</div>
|
||||
<div className="h2 m-0">{parsed.invalid.length}</div>
|
||||
</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-text mt-2">Проверьте предпросмотр и нажмите «Импортировать».</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn btn-outline-secondary" onClick={downloadSample}>Шаблон CSV</button>
|
||||
<button className="btn" onClick={onClose}>Отмена</button>
|
||||
<button className="btn btn-primary" disabled={parsed.items.length === 0} onClick={handleConfirm}>Импортировать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportModal;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user