feat: Add language and theme context providers to App component, implement global search functionality in Dashboard, and enhance ASNs, Domains, and IPRanges managers with drag-and-drop import feature and confirmation modals for improved user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m48s

This commit is contained in:
2025-08-10 23:44:17 +07:00
parent ca56b52672
commit 1fb53c3467
8 changed files with 460 additions and 25 deletions
+55 -1
View File
@@ -17,6 +17,9 @@ import {
IconClock,
IconFileText
} from '@tabler/icons-react';
import LockBanner from './components/LockBanner.jsx';
import S3MetaBar from './components/S3MetaBar.jsx';
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
const API_URL = '/api';
@@ -197,6 +200,7 @@ function DomainsNewManager() {
const [showDiff, setShowDiff] = useState(false);
const [diff, setDiff] = useState({ added: [], removed: [], changed: [] });
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
const handlePreviewDiff = () => {
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
@@ -207,9 +211,18 @@ function DomainsNewManager() {
};
const handleSaveChanges = async () => {
// подготовим diff и спросим подтверждение
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
const unique = deduplicate(valid);
setDiff(computeDiff(originalItems, unique));
setConfirmSaveOpen(true);
};
const performSave = async () => {
setConfirmSaveOpen(false);
setLoading(true);
try {
// сохраняем только валидные строки + дедупликация
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
const unique = deduplicate(valid);
@@ -234,6 +247,28 @@ function DomainsNewManager() {
}
};
// Drag&Drop импорт
const onDropImport = async (e) => {
e.preventDefault();
const file = e.dataTransfer?.files?.[0];
if (!file) return;
const text = await file.text();
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));
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());
});
};
const handleImport = () => {
const text = window.prompt('Вставьте строки: DOMAIN ПРОБЕЛ COMMUNITY (по одной записи на строку)');
if (!text) return;
@@ -356,6 +391,7 @@ function DomainsNewManager() {
<div className="row">
<div className="col-lg-3">
<LockBanner resource="domains-new" className="mb-3" />
{/* Add New Item Card */}
<div className="card card-md">
<div className="card-header">
@@ -479,6 +515,16 @@ function DomainsNewManager() {
</div>
</div>
</div>
{/* Drag & Drop импорт */}
<div
className="card card-md border-dashed"
onDragOver={(e) => { e.preventDefault(); }}
onDrop={onDropImport}
>
<div className="card-body text-center text-muted">
Перетащите файл TXT/CSV сюда для импорта (формат: domain community)
</div>
</div>
</div>
<div className="col-lg-9">
@@ -508,6 +554,7 @@ function DomainsNewManager() {
</div>
</div>
</div>
<S3MetaBar keys={["domainsNew"]} />
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
@@ -664,6 +711,13 @@ function DomainsNewManager() {
</div>
</div>
)}
{/* Confirm Save Modal */}
<ConfirmDiffModal
show={confirmSaveOpen}
diff={diff}
onConfirm={performSave}
onClose={() => setConfirmSaveOpen(false)}
/>
{/* Diff Modal */}
{showDiff && (
<div className="modal modal-blur fade show" style={{display: 'block'}} tabIndex="-1">