Files
router-lists-ui/frontend/src/Dashboard.jsx
T

339 lines
12 KiB
React

import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import api from './lib/api.js';
import { formatDateTime } from './lib/datetime.js';
import {
IconWorld,
IconNetwork,
IconServer,
IconClock,
IconMapPin,
IconCloud,
IconShield,
IconAlertTriangle,
IconRefresh,
IconFilter,
IconDownload,
IconCreditCard,
IconSearch
} from '@tabler/icons-react';
import PageHeader from './components/PageHeader.jsx';
import Breadcrumbs from './components/Breadcrumbs.jsx';
import TopNStats from './components/TopNStats.jsx';
import TrendIndicator from './components/TrendIndicator.jsx';
import LastSaved from './components/LastSaved.jsx';
import Tooltip from './components/Tooltip.jsx';
function StatCard({ icon: Icon, color, value, title, subtitle, to, trend, previousValue }) {
return (
<div className="card h-100 position-relative card-hover">
<div className="card-body d-flex align-items-center">
<span className={`avatar avatar-lg me-3 bg-${color}-lt text-${color} border-0`}>
<Icon size={32} />
</span>
<div className="flex-grow-1">
<div className="d-flex align-items-baseline gap-2 mb-1">
<div className="h3 mb-0 fw-bold">{value}</div>
{trend && previousValue !== undefined && (
<TrendIndicator
value={typeof value === 'number' ? value : 0}
previousValue={previousValue}
format="number"
/>
)}
</div>
<div className="text-muted small">{title}</div>
{subtitle && (
<div className="text-muted small opacity-75">{subtitle}</div>
)}
</div>
</div>
{to && (
<Link to={to} className="stretched-link" aria-label={`Перейти к ${title}`}></Link>
)}
</div>
);
}
function MetricCard({ title, value, icon: Icon, color, description }) {
return (
<div className="card h-100 position-relative">
<div className="card-body d-flex align-items-center">
<span className={`avatar avatar-lg me-3 bg-${color}-lt text-${color} border-0`}>
<Icon size={32} />
</span>
<div className="flex-grow-1">
<div className="h3 mb-0 fw-bold">
{value} <span className="fs-5 fw-normal">{title}</span>
</div>
{description && (
<div className="text-muted lh-1">{description}</div>
)}
</div>
</div>
</div>
);
}
function Dashboard() {
// Глобальный поиск удалён по требованию UX
const [stats, setStats] = useState({
domainsCount: null,
ipRangesCount: null,
asnsCount: null,
serversCount: null,
lastModified: null,
countriesCount: null,
providersCount: null,
onlineServers: null,
totalServers: null
});
const [previousStats, setPreviousStats] = useState(null);
const [raw, setRaw] = useState({ domains: [], ipRanges: [], asns: [], servers: [] });
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [lastFetchTime, setLastFetchTime] = useState(null);
useEffect(() => {
async function fetchStats() {
setLoading(true);
setError(null);
try {
// Загружаем данные с обработкой ошибок для каждого endpoint
const results = await Promise.allSettled([
api.get('/domains-new', { params: { countOnly: true } }),
api.get('/ip-ranges', { params: { countOnly: true } }),
api.get('/asns', { params: { countOnly: true } }),
api.get('/servers'),
api.get('/servers/availability', { params: { ttlSeconds: 60 } }),
api.get('/s3/last-modified')
]);
// Обрабатываем результаты
const domainsRes = results[0];
const ipRangesRes = results[1];
const asnsRes = results[2];
const serversRes = results[3];
const availabilityRes = results[4];
const s3Res = results[5];
// Получаем данные серверов для подсчета дополнительной статистики
const domains = [];
const ipRanges = [];
const asns = [];
const serversRaw = serversRes.status === 'fulfilled' ? serversRes.value.data : [];
const servers = Array.isArray(serversRaw) ? serversRaw : [];
setRaw({ domains, ipRanges, asns, servers });
const countries = new Set(servers.map(server => server.country).filter(Boolean));
const providers = new Set(servers.map(server => server.provider).filter(Boolean));
const onlineServers = availabilityRes.status === 'fulfilled' ? (availabilityRes.value.data?.online || 0) : (servers.filter(server => server.status === 'Онлайн').length);
// Получаем дату последнего обновления
// Новый формат: объект с ключами { domainsNew, asns, servers, filters, ipRanges }
const lmRaw = s3Res.status === 'fulfilled' ? s3Res.value.data?.domainsNew?.lastModified : null;
const lastModified = lmRaw ? formatDateTime(lmRaw) : formatDateTime(new Date());
const domainsCount = domainsRes.status === 'fulfilled' && typeof domainsRes.value.data?.total === 'number'
? domainsRes.value.data.total
: 0;
const ipRangesCount = ipRangesRes.status === 'fulfilled' && typeof ipRangesRes.value.data?.total === 'number'
? ipRangesRes.value.data.total
: 0;
const asnsCount = asnsRes.status === 'fulfilled' && typeof asnsRes.value.data?.total === 'number'
? asnsRes.value.data.total
: 0;
const newStats = {
domainsCount,
ipRangesCount,
asnsCount,
serversCount: servers.length,
lastModified,
countriesCount: countries.size,
providersCount: providers.size,
onlineServers,
totalServers: servers.length
};
// Сохраняем предыдущие значения перед обновлением
setStats(prevStats => {
if (prevStats.domainsCount !== null) {
setPreviousStats(prevStats);
}
return newStats;
});
setLastFetchTime(new Date().toISOString());
// Проверяем, есть ли ошибки
const errors = results.filter(result => result.status === 'rejected');
if (errors.length > 0) {
console.warn('Некоторые API недоступны:', errors.map(e => e.reason?.message));
}
} catch (e) {
console.error('Критическая ошибка загрузки статистики:', e);
setError('Не удалось загрузить статистику');
setStats({
domainsCount: null,
ipRangesCount: null,
asnsCount: null,
serversCount: null,
lastModified: null,
countriesCount: null,
providersCount: null,
onlineServers: null,
totalServers: null
});
} finally {
setLoading(false);
}
}
fetchStats();
}, []);
if (error) {
return (
<div className="alert alert-danger" role="alert">
<IconAlertTriangle className="me-2" />
{error}
</div>
);
}
return (
<div>
{/* Глобальный поиск удалён */}
{/* Заголовок страницы */}
<PageHeader
title="Панель"
pretitle={(
<Breadcrumbs items={[
{ label: 'Главная' },
]} />
)}
actions={(
<div className="d-flex align-items-center gap-3">
{lastFetchTime && (
<LastSaved timestamp={lastFetchTime} variant="compact" />
)}
<Tooltip content="Обновить данные" shortcut="⌘R">
<button className="btn btn-outline-primary" onClick={() => window.location.reload()}>
<IconRefresh className="me-2" /> Обновить
</button>
</Tooltip>
</div>
)}
/>
{/* Основные метрики */}
<div className="row g-3 mb-4">
<div className="col-md-3">
<div className="animate-in">
<StatCard
icon={IconWorld}
color="blue"
value={loading ? '...' : (stats.domainsCount ?? '—')}
title="Доменов"
subtitle="Всего доменов в системе"
to="/domains"
trend={!loading && previousStats}
previousValue={previousStats?.domainsCount}
/>
</div>
</div>
<div className="col-md-3">
<div className="animate-in" style={{animationDelay: '0.1s'}}>
<StatCard
icon={IconNetwork}
color="green"
value={loading ? '...' : (stats.ipRangesCount ?? '—')}
title="IP-диапазонов"
subtitle="Всего IP диапазонов"
to="/ip-ranges"
trend={!loading && previousStats}
previousValue={previousStats?.ipRangesCount}
/>
</div>
</div>
<div className="col-md-3">
<div className="animate-in" style={{animationDelay: '0.2s'}}>
<StatCard
icon={IconNetwork}
color="purple"
value={loading ? '...' : (stats.asnsCount ?? '—')}
title="AS"
subtitle="Всего Autonomous Systems"
to="/asns"
trend={!loading && previousStats}
previousValue={previousStats?.asnsCount}
/>
</div>
</div>
<div className="col-md-3">
<div className="animate-in" style={{animationDelay: '0.3s'}}>
<StatCard
icon={IconServer}
color="orange"
value={loading ? '...' : (stats.serversCount ?? '—')}
title="Серверов"
subtitle="Всего серверов"
to="/servers"
trend={!loading && previousStats}
previousValue={previousStats?.serversCount}
/>
</div>
</div>
</div>
{/* Дополнительные метрики */}
<div className="row g-3 mb-4 justify-content-center">
<div className="col-sm-6 col-md-3">
<MetricCard
title="Стран"
value={loading ? '...' : stats.countriesCount ?? '—'}
icon={IconMapPin}
color="blue"
description="Географическое покрытие"
/>
</div>
<div className="col-sm-6 col-md-3">
<MetricCard
title="Провайдеров"
value={loading ? '...' : stats.providersCount ?? '—'}
icon={IconCloud}
color="green"
description="Облачные провайдеры"
/>
</div>
<div className="col-sm-6 col-md-3">
<MetricCard
title="Онлайн серверов"
value={loading ? '...' : `${stats.onlineServers ?? '—'}/${stats.totalServers ?? '—'}`}
icon={IconServer}
color="success"
description="Активные серверы"
/>
</div>
<div className="col-sm-6 col-md-3">
<MetricCard
title="Последнее обновление"
value={loading ? '...' : (stats.lastModified ? stats.lastModified : '—')}
icon={IconClock}
color="orange"
description="S3 синхронизация"
/>
</div>
</div>
{/* Top-N статистика */}
<div className="mb-4">
<h3 className="mb-3">Топ статистика</h3>
<TopNStats data={{ servers: raw.servers }} loading={loading} />
</div>
{/* Убрали быстрые действия, карточки выше кликабельны */}
</div>
);
}
export default Dashboard;