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
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';
@@ -208,6 +211,7 @@ function IPRangesManager() {
const [showDiff, setShowDiff] = useState(false);
const [diff, setDiff] = useState({ added: [], removed: [], changed: [] });
const [confirmSaveOpen, setConfirmSaveOpen] = useState(false);
const handlePreviewDiff = () => {
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
@@ -218,6 +222,15 @@ function IPRangesManager() {
};
const handleSaveChanges = async () => {
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
.map(i => ({ ipRange: i.ipRange.trim(), 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 => isValidCidr(i.ipRange) && isValidCommunity(i.community))
@@ -277,6 +290,28 @@ function IPRangesManager() {
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 [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());
});
};
const clearInvalid = () => {
setItems(prev => prev.filter(i => i.ipRange || i.community)
.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
@@ -365,6 +400,7 @@ function IPRangesManager() {
<div className="row">
<div className="col-lg-3">
<LockBanner resource="ip-ranges" className="mb-3" />
{/* Add New Item Card */}
<div className="card card-md">
<div className="card-header">
@@ -419,6 +455,16 @@ function IPRangesManager() {
</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 сюда для импорта (формат: CIDR community)
</div>
</div>
{/* Actions Card */}
<div className="card card-md">
@@ -516,6 +562,7 @@ function IPRangesManager() {
</div>
</div>
</div>
<S3MetaBar keys={["ipRanges"]} />
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
@@ -677,6 +724,12 @@ function IPRangesManager() {
</div>
</div>
)}
<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">