feat: Удалить логику блокировок и обновить логику загрузки данных в менеджерах ASNs, Domains и IPRanges, улучшив фильтрацию и пагинацию для локального отображения
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m55s
This commit is contained in:
@@ -91,33 +91,19 @@ function ASNsNewManager() {
|
||||
const [wsUrl, setWsUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Acquire soft lock
|
||||
const owner = localStorage.getItem('uiOwner') || `ui-${Math.random().toString(36).slice(2,8)}`;
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'asns';
|
||||
const acquire = async () => {
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
// Load WS URL once
|
||||
(async () => {
|
||||
try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {}
|
||||
})();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||
const response = await api.get(`/asns`, { params: { q: effectiveQ, offset: Math.max(0, (currentPage - 1) * pageSize), limit: pageSize, format: 'std' } });
|
||||
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
|
||||
const total = Number(response.data?.total ?? (Array.isArray(response.data) ? response.data.length : 0));
|
||||
const mapped = payload.map(item => ({ asn: item.domain, community: item.type }));
|
||||
const total = mapped.length;
|
||||
setItems(mapped);
|
||||
setOriginalItems(mapped);
|
||||
setTotalItems(Number.isFinite(total) ? total : 0);
|
||||
@@ -134,13 +120,7 @@ function ASNsNewManager() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!didInit.current) { didInit.current = true; fetchItems(); return; }
|
||||
if (searchTimer.current) clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => { fetchItems(); }, 350);
|
||||
return () => { if (searchTimer.current) clearTimeout(searchTimer.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, searchTerm, filterCommunity]);
|
||||
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const asnOk = isValidAsn(newItem.asn);
|
||||
@@ -368,14 +348,15 @@ function ASNsNewManager() {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Фильтрация по community
|
||||
const filteredByCommunity = filterCommunity
|
||||
? sortedItems.filter(i => i.community === filterCommunity)
|
||||
: sortedItems;
|
||||
|
||||
// Пагинация теперь серверная
|
||||
const paginatedItems = filteredByCommunity;
|
||||
const totalPages = Math.max(1, Math.ceil((totalItems || paginatedItems.length) / pageSize));
|
||||
// Локальная фильтрация и пагинация
|
||||
const filtered = sortedItems.filter(i => {
|
||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(i.asn).toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
||||
|
||||
// Для фильтра - список всех уникальных community
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
|
||||
@@ -96,29 +96,15 @@ function DomainsNewManager() {
|
||||
const [wsUrl, setWsUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Acquire soft lock
|
||||
const owner = localStorage.getItem('uiOwner') || `ui-${Math.random().toString(36).slice(2,8)}`;
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'domains-new';
|
||||
const acquire = async () => {
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
(async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: Math.max(0, (currentPage - 1) * pageSize), limit: pageSize, format: 'std' } });
|
||||
const response = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
|
||||
const total = Number(response.data?.total ?? (Array.isArray(response.data) ? response.data.length : 0));
|
||||
const total = payload.length;
|
||||
setItems(payload);
|
||||
setOriginalItems(payload);
|
||||
setTotalItems(Number.isFinite(total) ? total : 0);
|
||||
@@ -136,13 +122,7 @@ function DomainsNewManager() {
|
||||
};
|
||||
|
||||
// Подгрузка при изменении страницы/поиска/фильтра
|
||||
useEffect(() => {
|
||||
if (!didInit.current) { didInit.current = true; fetchItems(); return; }
|
||||
if (searchTimer.current) clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => { fetchItems(); }, 350);
|
||||
return () => { if (searchTimer.current) clearTimeout(searchTimer.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, searchTerm, filterCommunity]);
|
||||
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const domainOk = isValidDomain(newItem.domain);
|
||||
@@ -374,14 +354,15 @@ function DomainsNewManager() {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Дополнительная локальная фильтрация по community только если одновременно задан поиск (сервер фильтрует по q)
|
||||
const filteredByCommunity = searchTerm && filterCommunity
|
||||
? sortedItems.filter(i => i.community === filterCommunity)
|
||||
: sortedItems;
|
||||
|
||||
// Пагинация теперь серверная: на странице уже items
|
||||
const paginatedItems = filteredByCommunity;
|
||||
const totalPages = Math.max(1, Math.ceil((totalItems || paginatedItems.length) / pageSize));
|
||||
// Локальная фильтрация и пагинация
|
||||
const filtered = sortedItems.filter(i => {
|
||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(i.domain).toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
||||
|
||||
// Для фильтра - список всех уникальных community
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
|
||||
@@ -107,29 +107,15 @@ function IPRangesManager() {
|
||||
const [wsUrl, setWsUrl] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// Acquire soft lock
|
||||
const owner = localStorage.getItem('uiOwner') || `ui-${Math.random().toString(36).slice(2,8)}`;
|
||||
localStorage.setItem('uiOwner', owner);
|
||||
const resource = 'ip-ranges';
|
||||
const acquire = async () => {
|
||||
try { await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 }); } catch {}
|
||||
};
|
||||
acquire();
|
||||
const interval = setInterval(acquire, 60_000);
|
||||
(async () => { try { const r = await api.get('/ws/url'); setWsUrl(String(r.data?.url || '')); } catch {} })();
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
api.delete(`/locks/${resource}`).catch(() => {});
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const effectiveQ = searchTerm ? searchTerm : (filterCommunity ? filterCommunity : '');
|
||||
const response = await api.get(`/ip-ranges`, { params: { q: effectiveQ, offset: Math.max(0, (currentPage - 1) * pageSize), limit: pageSize, format: 'std' } });
|
||||
const response = await api.get(`/ip-ranges`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const payload = Array.isArray(response.data?.items) ? response.data.items : [];
|
||||
const total = Number(response.data?.total ?? (Array.isArray(response.data) ? response.data.length : 0));
|
||||
const total = payload.length;
|
||||
setItems(payload);
|
||||
setOriginalItems(payload);
|
||||
setTotalItems(Number.isFinite(total) ? total : 0);
|
||||
@@ -146,13 +132,7 @@ function IPRangesManager() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!didInit.current) { didInit.current = true; fetchItems(); return; }
|
||||
if (searchTimer.current) clearTimeout(searchTimer.current);
|
||||
searchTimer.current = setTimeout(() => { fetchItems(); }, 350);
|
||||
return () => { if (searchTimer.current) clearTimeout(searchTimer.current); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentPage, searchTerm, filterCommunity]);
|
||||
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
|
||||
|
||||
const handleAddItem = () => {
|
||||
const cidrOk = isValidCidr(newItem.ipRange);
|
||||
@@ -387,14 +367,15 @@ function IPRangesManager() {
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Фильтрация по community
|
||||
const filteredByCommunity = filterCommunity
|
||||
? sortedItems.filter(i => i.community === filterCommunity)
|
||||
: sortedItems;
|
||||
|
||||
// Пагинация теперь серверная
|
||||
const paginatedItems = filteredByCommunity;
|
||||
const totalPages = Math.max(1, Math.ceil((totalItems || paginatedItems.length) / pageSize));
|
||||
// Локальная фильтрация и пагинация
|
||||
const filtered = sortedItems.filter(i => {
|
||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(i.ipRange).toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
||||
|
||||
// Для фильтра - список всех уникальных community
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
|
||||
@@ -1,84 +1 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import api from '../lib/api.js';
|
||||
|
||||
function LockBanner({ resource, className = '' }) {
|
||||
const [status, setStatus] = useState({ locked: false });
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const owner = (() => {
|
||||
const existing = localStorage.getItem('uiOwner');
|
||||
if (existing) return existing;
|
||||
const gen = `ui-${Math.random().toString(36).slice(2, 8)}`;
|
||||
localStorage.setItem('uiOwner', gen);
|
||||
return gen;
|
||||
})();
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const res = await api.get(`/locks/${resource}`);
|
||||
setStatus(res.data || { locked: false });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus();
|
||||
const id = setInterval(fetchStatus, 30_000);
|
||||
return () => clearInterval(id);
|
||||
}, [resource]);
|
||||
|
||||
const acquireOrRefresh = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await api.post(`/locks/${resource}`, { owner, ttlSeconds: 180 });
|
||||
setStatus(res.data || { locked: true, owner, expiresAt: Date.now() + 180_000 });
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const release = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.delete(`/locks/${resource}`);
|
||||
setStatus({ locked: false });
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const myLock = status.locked && status.owner === owner;
|
||||
const until = status.expiresAt ? new Date(status.expiresAt).toLocaleTimeString() : null;
|
||||
|
||||
return (
|
||||
<div className={`alert ${status.locked ? (myLock ? 'alert-success' : 'alert-warning') : 'alert-secondary'} d-flex align-items-center justify-content-between ${className}`} role="alert">
|
||||
<div>
|
||||
{status.locked ? (
|
||||
myLock ? (
|
||||
<>Вы удерживаете блокировку ресурса <code>{resource}</code>{until ? ` до ${until}` : ''}</>
|
||||
) : (
|
||||
<>Ресурс <code>{resource}</code> редактирует <strong>{status.owner || 'другой пользователь'}</strong>{until ? ` до ${until}` : ''}</>
|
||||
)
|
||||
) : (
|
||||
<>Ресурс <code>{resource}</code> свободен</>
|
||||
)}
|
||||
</div>
|
||||
<div className="btn-list m-0">
|
||||
<button className="btn btn-outline-primary btn-sm" onClick={fetchStatus} disabled={loading}>Обновить</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={acquireOrRefresh} disabled={loading}>{myLock ? 'Продлить' : 'Захватить'}</button>
|
||||
{myLock && (
|
||||
<button className="btn btn-outline-danger btn-sm" onClick={release} disabled={loading}>Освободить</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LockBanner;
|
||||
|
||||
|
||||
export default function LockBanner() { return null }
|
||||
|
||||
Reference in New Issue
Block a user