feat: Добавить кэширование имен ASN и улучшить обработку данных в менеджере ASNs, включая обновление фильтрации и отображения метаданных
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user