From cd3f7a84e3476cd3470470b8175dc71530c24a3d Mon Sep 17 00:00:00 2001 From: shats Date: Wed, 27 Aug 2025 17:39:04 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B1=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B8=D1=80=D0=BE=D0=B2=D0=BE=D0=BA=20=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B2=20?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=B4=D0=B6=D0=B5=D1=80=D0=B0=D1=85=20?= =?UTF-8?q?ASNs,=20Domains=20=D0=B8=20IPRanges,=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B8=D0=B2=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20=D0=B8=20=D0=BF=D0=B0=D0=B3=D0=B8=D0=BD?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/ASNsNewManager.jsx | 43 ++++--------- frontend/src/DomainsNewManager.jsx | 43 ++++--------- frontend/src/IPRangesManager.jsx | 43 ++++--------- frontend/src/components/LockBanner.jsx | 85 +------------------------- 4 files changed, 37 insertions(+), 177 deletions(-) diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 8310ad9..fc36d91 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -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))); diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index c1de0bb..233cab3 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -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))); diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 92ccd9d..161250e 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -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))); diff --git a/frontend/src/components/LockBanner.jsx b/frontend/src/components/LockBanner.jsx index fdffa4a..5dde365 100644 --- a/frontend/src/components/LockBanner.jsx +++ b/frontend/src/components/LockBanner.jsx @@ -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 ( -
-
- {status.locked ? ( - myLock ? ( - <>Вы удерживаете блокировку ресурса {resource}{until ? ` до ${until}` : ''} - ) : ( - <>Ресурс {resource} редактирует {status.owner || 'другой пользователь'}{until ? ` до ${until}` : ''} - ) - ) : ( - <>Ресурс {resource} свободен - )} -
-
- - - {myLock && ( - - )} -
-
- ); -} - -export default LockBanner; - - +export default function LockBanner() { return null }