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
+53 -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';
@@ -193,6 +196,7 @@ function ASNsNewManager() {
const [showDiff, setShowDiff] = useState(false);
const [diff, setDiff] = useState({ added: [], removed: [], changed: [] });
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
const handlePreviewDiff = () => {
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
@@ -203,9 +207,17 @@ function ASNsNewManager() {
};
const handleSaveChanges = async () => {
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
.map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() }));
const unique = deduplicate(valid);
setDiff(computeDiff(originalItems, unique));
setConfirmSaveOpen(true);
};
const performSave = async () => {
setConfirmSaveOpen(false);
setLoading(true);
try {
// API ожидает domains: [{domain, type}]
const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
.map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() }));
const unique = deduplicate(valid);
@@ -262,6 +274,28 @@ function ASNsNewManager() {
URL.revokeObjectURL(url);
};
// 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 [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());
});
};
const clearInvalid = () => {
setItems(prev => prev.filter(i => i.asn || i.community)
.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community))
@@ -350,6 +384,7 @@ function ASNsNewManager() {
<div className="row">
<div className="col-lg-3">
<LockBanner resource="asns" className="mb-3" />
{/* Add New ASN Card */}
<div className="card card-md">
<div className="card-header">
@@ -404,6 +439,16 @@ function ASNsNewManager() {
</form>
</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 сюда для импорта (формат: ASN community)
</div>
</div>
{/* Actions Card */}
<div className="card card-md">
@@ -501,6 +546,7 @@ function ASNsNewManager() {
</div>
</div>
</div>
<S3MetaBar keys={["asns"]} />
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
@@ -657,6 +703,12 @@ function ASNsNewManager() {
</div>
</div>
)}
<ConfirmDiffModal
show={confirmSaveOpen}
diff={diff}
onConfirm={performSave}
onClose={() => setConfirmSaveOpen(false)}
/>
{/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
</>
);