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 QuickAddBar from './components/QuickAddBar.jsx';
import AccordionCard from './components/AccordionCard.jsx';
import { getAsnName, getAsnNameSync } from './lib/asn.js';
const API_URL = '/api';
@@ -60,6 +61,7 @@ function ASNsNewManager() {
const [filterCommunity, setFilterCommunity] = useState('');
const editInputRef = useRef(null);
const pageSize = 10;
const [asnNameMap, setAsnNameMap] = useState({});
// Справочник community для подсказок
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 v = String(value ?? '').trim();
if (!v) return <span className="badge bg-blue-lt text-blue"></span>;
@@ -105,7 +121,7 @@ function ASNsNewManager() {
try {
const response = await api.get(`/asns`, { params: { offset: 0, limit: 0, format: 'std' } });
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;
setItems(mapped);
setOriginalItems(mapped);
@@ -367,7 +383,7 @@ function ASNsNewManager() {
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
// Для фильтра - список всех уникальных 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) => {
@@ -577,7 +593,7 @@ function ASNsNewManager() {
value={filterCommunity}
onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }}
title={(() => {
const meta = communities.find(c => c.value === filterCommunity);
const meta = communities.find(c => String(c.value) === String(filterCommunity));
if (!meta) return 'Все community';
const name = meta.name ? meta.name + ' — ' : '';
const desc = meta.description || '';
@@ -587,11 +603,11 @@ function ASNsNewManager() {
>
<option value="">Все community</option>
{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 title = meta ? `${meta.name ? meta.name + ' — ' : ''}${meta.description || ''}${meta.tags && meta.tags.length ? ' (' + meta.tags.join(', ') + ')' : ''}` : community;
return (
<option key={community} value={community} title={title}>{label}</option>
<option key={community} value={String(community)} title={title}>{label}</option>
);
})}
</select>
@@ -632,6 +648,9 @@ function ASNsNewManager() {
</span>
)}
</th>
<th>
Имя AS
</th>
<th className="cursor-pointer" onClick={() => handleSort('community')}>
Community
{sortField === 'community' && (
@@ -647,6 +666,13 @@ function ASNsNewManager() {
{paginatedItems.map((item) => (
<tr key={item.asn} className={editingAsn === item.asn ? 'table-info' : ''}>
<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 className="text-end">
{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);
}