feat: Добавить кэширование имен ASN и улучшить обработку данных в менеджере ASNs, включая обновление фильтрации и отображения метаданных
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s

This commit is contained in:
2025-08-27 19:12:24 +07:00
parent 960f68b8ec
commit 10903400e7
2 changed files with 117 additions and 5 deletions
+31 -5
View File
@@ -32,6 +32,7 @@ import { notifyMutationSuccess } from './components/NotifyProvider.jsx';
import ImportModal from './components/ImportModal.jsx'; import ImportModal from './components/ImportModal.jsx';
import QuickAddBar from './components/QuickAddBar.jsx'; import QuickAddBar from './components/QuickAddBar.jsx';
import AccordionCard from './components/AccordionCard.jsx'; import AccordionCard from './components/AccordionCard.jsx';
import { getAsnName, getAsnNameSync } from './lib/asn.js';
const API_URL = '/api'; const API_URL = '/api';
@@ -60,6 +61,7 @@ function ASNsNewManager() {
const [filterCommunity, setFilterCommunity] = useState(''); const [filterCommunity, setFilterCommunity] = useState('');
const editInputRef = useRef(null); const editInputRef = useRef(null);
const pageSize = 10; const pageSize = 10;
const [asnNameMap, setAsnNameMap] = useState({});
// Справочник community для подсказок // Справочник community для подсказок
const [communities, setCommunities] = useState([]); const [communities, setCommunities] = useState([]);
@@ -74,6 +76,20 @@ function ASNsNewManager() {
})(); })();
}, []); }, []);
// Подгружаем имена ASN из кэша/внешних API
useEffect(() => {
const visible = paginatedItems.map(i => i.asn);
visible.forEach(async (asn) => {
const cached = getAsnNameSync(asn);
if (cached !== null && typeof cached !== 'undefined') {
setAsnNameMap(prev => ({ ...prev, [asn]: cached }));
return;
}
const name = await getAsnName(asn);
if (name !== null) setAsnNameMap(prev => ({ ...prev, [asn]: name }));
});
}, [JSON.stringify(paginatedItems)]);
const renderCommunityBadge = (value) => { const renderCommunityBadge = (value) => {
const v = String(value ?? '').trim(); const v = String(value ?? '').trim();
if (!v) return <span className="badge bg-blue-lt text-blue">—</span>; if (!v) return <span className="badge bg-blue-lt text-blue">—</span>;
@@ -105,7 +121,7 @@ function ASNsNewManager() {
try { try {
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, 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 payload = Array.isArray(response.data?.items) ? response.data.items : [];
const mapped = payload.map(item => ({ asn: item.domain, community: item.type })); const mapped = payload.map(item => ({ asn: String(item.domain), community: String(item.type) }));
const total = mapped.length; const total = mapped.length;
setItems(mapped); setItems(mapped);
setOriginalItems(mapped); setOriginalItems(mapped);
@@ -367,7 +383,7 @@ function ASNsNewManager() {
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize); const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
// Для фильтра - список всех уникальных community // Для фильтра - список всех уникальных community
const allCommunities = Array.from(new Set(items.map(i => i.community))); const allCommunities = Array.from(new Set(items.map(i => String(i.community))));
// Сортировка по клику // Сортировка по клику
const handleSort = (field) => { const handleSort = (field) => {
@@ -577,7 +593,7 @@ function ASNsNewManager() {
value={filterCommunity} value={filterCommunity}
onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }} onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }}
title={(() => { title={(() => {
const meta = communities.find(c => c.value === filterCommunity); const meta = communities.find(c => String(c.value) === String(filterCommunity));
if (!meta) return 'Все community'; if (!meta) return 'Все community';
const name = meta.name ? meta.name + ' — ' : ''; const name = meta.name ? meta.name + ' — ' : '';
const desc = meta.description || ''; const desc = meta.description || '';
@@ -587,11 +603,11 @@ function ASNsNewManager() {
> >
<option value="">Все community</option> <option value="">Все community</option>
{allCommunities.map(community => { {allCommunities.map(community => {
const meta = communities.find(c => c.value === community); const meta = communities.find(c => String(c.value) === String(community));
const label = meta && meta.name ? `${community} — ${meta.name}` : community; const label = meta && meta.name ? `${community} — ${meta.name}` : community;
const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : community; const title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : community;
return ( return (
<option key={community} value={community} title={title}>{label}</option> <option key={community} value={String(community)} title={title}>{label}</option>
); );
})} })}
</select> </select>
@@ -632,6 +648,9 @@ function ASNsNewManager() {
</span> </span>
)} )}
</th> </th>
<th>
Имя AS
</th>
<th className="cursor-pointer" onClick={() => handleSort('community')}> <th className="cursor-pointer" onClick={() => handleSort('community')}>
Community Community
{sortField === 'community' && ( {sortField === 'community' && (
@@ -647,6 +666,13 @@ function ASNsNewManager() {
{paginatedItems.map((item) => ( {paginatedItems.map((item) => (
<tr key={item.asn} className={editingAsn === item.asn ? 'table-info' : ''}> <tr key={item.asn} className={editingAsn === item.asn ? 'table-info' : ''}>
<td className="font-monospace" title="ASN">{item.asn}</td> <td className="font-monospace" title="ASN">{item.asn}</td>
<td title={asnNameMap[item.asn] || ''}>
{asnNameMap[item.asn] ? (
<span>{asnNameMap[item.asn]}</span>
) : (
<span className="text-muted">—</span>
)}
</td>
<td>{renderCommunityBadge(item.community)}</td> <td>{renderCommunityBadge(item.community)}</td>
<td className="text-end"> <td className="text-end">
{editingAsn === item.asn ? ( {editingAsn === item.asn ? (
+86
View File
@@ -0,0 +1,86 @@
// Утилиты для получения названия ASN с кэшем в localStorage
const CACHE_KEY = 'asnNameCache.v1';
const DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 дней
function readCache() {
try {
const raw = localStorage.getItem(CACHE_KEY);
if (!raw) return {};
const data = JSON.parse(raw);
return typeof data === 'object' && data ? data : {};
} catch {
return {};
}
}
function writeCache(cache) {
try {
localStorage.setItem(CACHE_KEY, JSON.stringify(cache));
} catch {}
}
function getCached(asn) {
const cache = readCache();
const entry = cache[String(asn)];
if (!entry) return null;
if (Date.now() > (entry.ts || 0)) return null;
return entry.name || null;
}
function setCached(asn, name, ttlMs = DEFAULT_TTL_MS) {
const cache = readCache();
cache[String(asn)] = { name: String(name || '').trim(), ts: Date.now() + ttlMs };
writeCache(cache);
}
async function fetchFromBGPView(asn) {
try {
const url = `https://api.bgpview.io/asn/AS${asn}`;
const res = await fetch(url, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const name = json?.data?.name || json?.data?.description || '';
return String(name || '').trim() || null;
} catch {
return null;
}
}
async function fetchFromRIPE(asn) {
try {
const url = `https://stat.ripe.net/data/as-overview/data.json?resource=AS${asn}`;
const res = await fetch(url, { method: 'GET' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const name = json?.data?.holder || json?.data?.name || '';
return String(name || '').trim() || null;
} catch {
return null;
}
}
export async function getAsnName(asn, { ttlMs = DEFAULT_TTL_MS } = {}) {
const key = String(asn).trim();
if (!/^[0-9]+$/.test(key)) return null;
const cached = getCached(key);
if (cached) return cached;
// попытаемся получить из провайдеров
const providers = [fetchFromBGPView, fetchFromRIPE];
for (const p of providers) {
const name = await p(key);
if (name) {
setCached(key, name, ttlMs);
return name;
}
}
// Закэшируем пустой ответ на короткое время, чтобы не долбить API
setCached(key, '', 60 * 60 * 1000); // 1 час
return null;
}
export function getAsnNameSync(asn) {
return getCached(asn);
}