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 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 ? (
|
||||||
|
|||||||
@@ -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