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 ASNsNewManager from './ASNsNewManager';
|
||||||
import AutoUrlManager from './AutoUrlManager';
|
import AutoUrlManager from './AutoUrlManager';
|
||||||
import BillingManager from './BillingManager';
|
import BillingManager from './BillingManager';
|
||||||
|
import Dashboard from './Dashboard';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {
|
import {
|
||||||
@@ -107,7 +108,7 @@ function MainLayout() {
|
|||||||
id: 'home',
|
id: 'home',
|
||||||
title: 'Главная',
|
title: 'Главная',
|
||||||
icon: IconHome,
|
icon: IconHome,
|
||||||
path: '/',
|
path: '/dashboard',
|
||||||
single: true
|
single: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -136,11 +137,7 @@ function MainLayout() {
|
|||||||
title: 'Инструменты',
|
title: 'Инструменты',
|
||||||
icon: IconSettings,
|
icon: IconSettings,
|
||||||
items: [
|
items: [
|
||||||
{ id: 'auto-urls', title: 'Авто URL', path: '/auto-urls', icon: IconDownload },
|
{ 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 }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -288,6 +285,7 @@ function MainLayout() {
|
|||||||
</header>
|
</header>
|
||||||
<div className="container-xl mt-4">
|
<div className="container-xl mt-4">
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/domains" element={
|
<Route path="/domains" element={
|
||||||
<DataManager
|
<DataManager
|
||||||
entityName="домен"
|
entityName="домен"
|
||||||
@@ -302,108 +300,13 @@ function MainLayout() {
|
|||||||
<Route path="/servers" element={<ServerManager />} />
|
<Route path="/servers" element={<ServerManager />} />
|
||||||
<Route path="/billing" element={<BillingManager />} />
|
<Route path="/billing" element={<BillingManager />} />
|
||||||
<Route path="/filters" element={<FilterManager />} />
|
<Route path="/filters" element={<FilterManager />} />
|
||||||
<Route path="/" element={<Navigate to="/domains" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
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