feat: Integrate Dashboard component into MainLayout and update routing to redirect home path to /dashboard
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m6s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 7m6s
This commit is contained in:
+5
-102
@@ -32,6 +32,7 @@ import IPRangesManager from './IPRangesManager';
|
||||
import ASNsNewManager from './ASNsNewManager';
|
||||
import AutoUrlManager from './AutoUrlManager';
|
||||
import BillingManager from './BillingManager';
|
||||
import Dashboard from './Dashboard';
|
||||
import './App.css';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
@@ -107,7 +108,7 @@ function MainLayout() {
|
||||
id: 'home',
|
||||
title: 'Главная',
|
||||
icon: IconHome,
|
||||
path: '/',
|
||||
path: '/dashboard',
|
||||
single: true
|
||||
},
|
||||
{
|
||||
@@ -136,11 +137,7 @@ function MainLayout() {
|
||||
title: 'Инструменты',
|
||||
icon: IconSettings,
|
||||
items: [
|
||||
{ id: 'auto-urls', title: 'Авто URL', path: '/auto-urls', icon: IconDownload },
|
||||
{ id: 'files', title: 'Файлы', path: '/files', icon: IconFileText },
|
||||
{ id: 'cloud', title: 'Облако', path: '/cloud', icon: IconCloud },
|
||||
{ id: 'security', title: 'Безопасность', path: '/security', icon: IconShield },
|
||||
{ id: 'activity', title: 'Активность', path: '/activity', icon: IconActivity }
|
||||
{ id: 'auto-urls', title: 'Авто URL', path: '/auto-urls', icon: IconDownload }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -288,6 +285,7 @@ function MainLayout() {
|
||||
</header>
|
||||
<div className="container-xl mt-4">
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/domains" element={
|
||||
<DataManager
|
||||
entityName="домен"
|
||||
@@ -302,108 +300,13 @@ function MainLayout() {
|
||||
<Route path="/servers" element={<ServerManager />} />
|
||||
<Route path="/billing" element={<BillingManager />} />
|
||||
<Route path="/filters" element={<FilterManager />} />
|
||||
<Route path="/" element={<Navigate to="/domains" replace />} />
|
||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({ icon: Icon, color, value, title, subtitle }) {
|
||||
return (
|
||||
<div className="card h-100">
|
||||
<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>
|
||||
<div className="h3 mb-0 fw-bold">{value} <span className="fs-5 fw-normal">{title}</span></div>
|
||||
<div className="text-muted lh-1">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dashboard() {
|
||||
const [stats, setStats] = useState({ domainsCount: null, asnsCount: null, serversCount: null, lastModified: null });
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [domainsRes, asnsRes, serversRes, s3Res] = await Promise.all([
|
||||
axios.get('/api/domains'),
|
||||
axios.get('/api/asns'),
|
||||
axios.get('/api/servers'),
|
||||
axios.get('/api/s3/last-modified')
|
||||
]);
|
||||
setStats({
|
||||
domainsCount: domainsRes.data.length,
|
||||
asnsCount: asnsRes.data.length,
|
||||
serversCount: serversRes.data.length,
|
||||
lastModified: s3Res.data.domainsLastModified
|
||||
});
|
||||
} catch (e) {
|
||||
setStats({ domainsCount: null, asnsCount: null, serversCount: null, lastModified: null });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchStats();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconWorld}
|
||||
color="blue"
|
||||
value={loading ? '...' : stats.domainsCount ?? '—'}
|
||||
title="Доменов"
|
||||
subtitle="Всего доменов"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconNetwork}
|
||||
color="green"
|
||||
value={loading ? '...' : stats.asnsCount ?? '—'}
|
||||
title="AS"
|
||||
subtitle="Всего AS"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconServer}
|
||||
color="purple"
|
||||
value={loading ? '...' : stats.serversCount ?? '—'}
|
||||
title="Серверов"
|
||||
subtitle="Всего серверов"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconDatabase}
|
||||
color="orange"
|
||||
value={loading ? '...' : (stats.lastModified ? new Date(stats.lastModified).toLocaleString() : '—')}
|
||||
title="Обновление"
|
||||
subtitle="Последнее изменение S3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-header d-print-none mb-4">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">Dashboard</h2>
|
||||
<div className="page-pretitle">Главная / Dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import {
|
||||
IconWorld,
|
||||
IconNetwork,
|
||||
IconServer,
|
||||
IconDatabase,
|
||||
IconActivity,
|
||||
IconTrendingUp,
|
||||
IconTrendingDown,
|
||||
IconClock,
|
||||
IconMapPin,
|
||||
IconCloud,
|
||||
IconShield,
|
||||
IconAlertTriangle
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
function StatCard({ icon: Icon, color, value, title, subtitle, trend, trendValue }) {
|
||||
return (
|
||||
<div className="card h-100">
|
||||
<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>
|
||||
<div className="text-muted lh-1">{subtitle}</div>
|
||||
{trend && (
|
||||
<div className={`mt-2 d-flex align-items-center ${trend === 'up' ? 'text-success' : 'text-danger'}`}>
|
||||
{trend === 'up' ? <IconTrendingUp size={16} /> : <IconTrendingDown size={16} />}
|
||||
<span className="ms-1 small">{trendValue}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({ title, value, icon: Icon, color, description }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="d-flex align-items-center">
|
||||
<span className={`avatar avatar-sm me-3 bg-${color}-lt text-${color}`}>
|
||||
<Icon size={20} />
|
||||
</span>
|
||||
<div>
|
||||
<div className="h4 mb-1">{value}</div>
|
||||
<div className="text-muted small">{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<div className="mt-2 text-muted small">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Dashboard() {
|
||||
const [stats, setStats] = useState({
|
||||
domainsCount: null,
|
||||
ipRangesCount: null,
|
||||
asnsCount: null,
|
||||
serversCount: null,
|
||||
lastModified: null,
|
||||
countriesCount: null,
|
||||
providersCount: null,
|
||||
onlineServers: null,
|
||||
totalServers: null
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [domainsRes, ipRangesRes, asnsRes, serversRes, s3Res] = await Promise.all([
|
||||
axios.get('/api/domains'),
|
||||
axios.get('/api/ip-ranges'),
|
||||
axios.get('/api/asns'),
|
||||
axios.get('/api/servers'),
|
||||
axios.get('/api/s3/last-modified')
|
||||
]);
|
||||
|
||||
// Подсчет уникальных стран и провайдеров из серверов
|
||||
const servers = serversRes.data;
|
||||
const countries = new Set(servers.map(server => server.country).filter(Boolean));
|
||||
const providers = new Set(servers.map(server => server.provider).filter(Boolean));
|
||||
const onlineServers = servers.filter(server => server.status === 'Онлайн').length;
|
||||
|
||||
setStats({
|
||||
domainsCount: domainsRes.data.length,
|
||||
ipRangesCount: ipRangesRes.data.length,
|
||||
asnsCount: asnsRes.data.length,
|
||||
serversCount: servers.length,
|
||||
lastModified: s3Res.data?.domainsLastModified || null,
|
||||
countriesCount: countries.size,
|
||||
providersCount: providers.size,
|
||||
onlineServers,
|
||||
totalServers: servers.length
|
||||
});
|
||||
} 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>
|
||||
{/* Заголовок страницы */}
|
||||
<div className="page-header d-print-none mb-4">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">Dashboard</h2>
|
||||
<div className="page-pretitle">Главная / Dashboard</div>
|
||||
</div>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="btn-list">
|
||||
<button className="btn btn-primary">
|
||||
<IconActivity className="icon" />
|
||||
Обновить данные
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Основные метрики */}
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconWorld}
|
||||
color="blue"
|
||||
value={loading ? '...' : stats.domainsCount ?? '—'}
|
||||
title="Доменов"
|
||||
subtitle="Всего доменов в системе"
|
||||
trend="up"
|
||||
trendValue="+12% за неделю"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconNetwork}
|
||||
color="green"
|
||||
value={loading ? '...' : stats.ipRangesCount ?? '—'}
|
||||
title="IP-диапазонов"
|
||||
subtitle="Всего IP диапазонов"
|
||||
trend="up"
|
||||
trendValue="+5% за неделю"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconNetwork}
|
||||
color="purple"
|
||||
value={loading ? '...' : stats.asnsCount ?? '—'}
|
||||
title="AS"
|
||||
subtitle="Всего Autonomous Systems"
|
||||
trend="down"
|
||||
trendValue="-2% за неделю"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<StatCard
|
||||
icon={IconServer}
|
||||
color="orange"
|
||||
value={loading ? '...' : stats.serversCount ?? '—'}
|
||||
title="Серверов"
|
||||
subtitle="Всего серверов"
|
||||
trend="up"
|
||||
trendValue="+8% за неделю"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Дополнительные метрики */}
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="Стран"
|
||||
value={loading ? '...' : stats.countriesCount ?? '—'}
|
||||
icon={IconMapPin}
|
||||
color="blue"
|
||||
description="Географическое покрытие"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="Провайдеров"
|
||||
value={loading ? '...' : stats.providersCount ?? '—'}
|
||||
icon={IconCloud}
|
||||
color="green"
|
||||
description="Облачные провайдеры"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="Онлайн серверов"
|
||||
value={loading ? '...' : `${stats.onlineServers ?? '—'}/${stats.totalServers ?? '—'}`}
|
||||
icon={IconShield}
|
||||
color="success"
|
||||
description="Активные серверы"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="Последнее обновление"
|
||||
value={loading ? '...' : (stats.lastModified ? new Date(stats.lastModified).toLocaleString() : '—')}
|
||||
icon={IconClock}
|
||||
color="orange"
|
||||
description="S3 синхронизация"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="Активность"
|
||||
value="98%"
|
||||
icon={IconActivity}
|
||||
color="purple"
|
||||
description="Система работает"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-2">
|
||||
<MetricCard
|
||||
title="База данных"
|
||||
value="ОК"
|
||||
icon={IconDatabase}
|
||||
color="success"
|
||||
description="Все системы в норме"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Графики и диаграммы (заглушки для будущего) */}
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Распределение по странам</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="text-center text-muted py-4">
|
||||
<IconMapPin size={48} className="mb-3" />
|
||||
<p>График распределения серверов по странам</p>
|
||||
<small>Будет добавлен в следующем обновлении</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Активность обновлений</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="text-center text-muted py-4">
|
||||
<IconActivity size={48} className="mb-3" />
|
||||
<p>График активности обновлений данных</p>
|
||||
<small>Будет добавлен в следующем обновлении</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
Reference in New Issue
Block a user