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

This commit is contained in:
2025-08-05 21:26:23 +07:00
parent f5d86d9e1b
commit 7c6ceb66bc
2 changed files with 302 additions and 102 deletions
+5 -102
View File
@@ -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;
+297
View File
@@ -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;