Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m1s
380 lines
13 KiB
React
380 lines
13 KiB
React
import { useState, useEffect } from 'react';
|
|
import { Container } from 'react-bootstrap';
|
|
import {
|
|
IconBrandTabler,
|
|
IconWorld,
|
|
IconNetwork,
|
|
IconSettings,
|
|
IconHome,
|
|
IconFileText,
|
|
IconCloud,
|
|
IconShield,
|
|
IconActivity,
|
|
IconBell,
|
|
IconChevronDown,
|
|
IconUser,
|
|
IconDatabase,
|
|
IconAlertTriangle,
|
|
IconAlertCircle,
|
|
IconServer,
|
|
IconFilter,
|
|
IconDownload,
|
|
IconCreditCard,
|
|
IconMoon,
|
|
IconHeart,
|
|
IconCode
|
|
} from '@tabler/icons-react';
|
|
import DataManager from './DataManager';
|
|
import ServerManager from './ServerManager';
|
|
import FilterManager from './FilterManager';
|
|
import DomainsNewManager from './DomainsNewManager';
|
|
import IPRangesManager from './IPRangesManager';
|
|
import ASNsNewManager from './ASNsNewManager';
|
|
import AutoUrlManager from './AutoUrlManager';
|
|
import BillingManager from './BillingManager';
|
|
import './App.css';
|
|
import axios from 'axios';
|
|
import {
|
|
BrowserRouter as Router,
|
|
Routes,
|
|
Route,
|
|
Link,
|
|
useLocation,
|
|
Navigate
|
|
} from 'react-router-dom';
|
|
|
|
function App() {
|
|
return (
|
|
<Router>
|
|
<MainLayout />
|
|
</Router>
|
|
);
|
|
}
|
|
|
|
function MainLayout() {
|
|
const location = useLocation();
|
|
const [notifications] = useState([
|
|
{ id: 1, title: 'Новый домен добавлен', message: 'example.com был добавлен в список', time: '2 мин назад' },
|
|
{ id: 2, title: 'Изменения сохранены', message: 'Список AS обновлен в S3', time: '5 мин назад' }
|
|
]);
|
|
|
|
// Состояние для управления выпадающими меню
|
|
const [dropdownStates, setDropdownStates] = useState({
|
|
data: false,
|
|
management: false,
|
|
tools: false,
|
|
settings: false
|
|
});
|
|
|
|
const toggleDropdown = (dropdown) => {
|
|
setDropdownStates(prev => ({
|
|
...prev,
|
|
[dropdown]: !prev[dropdown]
|
|
}));
|
|
};
|
|
|
|
const navCategories = [
|
|
{
|
|
id: 'home',
|
|
title: 'Главная',
|
|
icon: IconHome,
|
|
path: '/',
|
|
single: true
|
|
},
|
|
{
|
|
id: 'data',
|
|
title: 'Данные',
|
|
icon: IconDatabase,
|
|
items: [
|
|
{ id: 'domains', title: 'Домены', path: '/domains', icon: IconWorld },
|
|
{ id: 'domains-new', title: 'Домены New', path: '/domains-new', icon: IconWorld },
|
|
{ id: 'ip-ranges', title: 'IP-диапазоны', path: '/ip-ranges', icon: IconNetwork },
|
|
{ id: 'asns', title: 'AS', path: '/asns', icon: IconNetwork }
|
|
]
|
|
},
|
|
{
|
|
id: 'management',
|
|
title: 'Управление',
|
|
icon: IconServer,
|
|
items: [
|
|
{ id: 'servers', title: 'Серверы', path: '/servers', icon: IconServer },
|
|
{ id: 'filters', title: 'Фильтры', path: '/filters', icon: IconFilter },
|
|
{ id: 'billing', title: 'Биллинг', path: '/billing', icon: IconCreditCard }
|
|
]
|
|
},
|
|
{
|
|
id: 'tools',
|
|
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: 'settings',
|
|
title: 'Настройки',
|
|
icon: IconSettings,
|
|
path: '/settings',
|
|
single: true
|
|
}
|
|
];
|
|
|
|
// Определяем активную вкладку по адресу
|
|
const getActiveTab = () => {
|
|
for (const category of navCategories) {
|
|
if (category.single && location.pathname === category.path) {
|
|
return category.id;
|
|
}
|
|
if (category.items) {
|
|
for (const item of category.items) {
|
|
if (location.pathname.startsWith(item.path)) {
|
|
return item.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 'home';
|
|
};
|
|
|
|
const activeTab = getActiveTab();
|
|
|
|
return (
|
|
<div className="page">
|
|
{/* Верхнее меню Tabler Admin */}
|
|
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
|
<div className="container-xl">
|
|
{/* Логотип */}
|
|
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
|
<Link to="/" className="navbar-brand d-flex align-items-center">
|
|
<IconBrandTabler className="navbar-brand-icon me-2" />
|
|
<span className="fw-bold">S3 Lists Manager</span>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Основное меню */}
|
|
<div className="collapse navbar-collapse" id="navbar-menu">
|
|
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
|
|
<ul className="navbar-nav">
|
|
{navCategories.map(category => (
|
|
<li key={category.id} className="nav-item dropdown">
|
|
{category.single ? (
|
|
<Link
|
|
to={category.path}
|
|
className={`nav-link${activeTab === category.id ? ' active' : ''}`}
|
|
>
|
|
<category.icon className="icon icon-tabler" />
|
|
<span className="nav-link-title">{category.title}</span>
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<a
|
|
className={`nav-link dropdown-toggle${activeTab.startsWith(category.id) ? ' active' : ''}`}
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
toggleDropdown(category.id);
|
|
}}
|
|
>
|
|
<category.icon className="icon icon-tabler" />
|
|
<span className="nav-link-title">{category.title}</span>
|
|
</a>
|
|
{dropdownStates[category.id] && (
|
|
<div className="dropdown-menu show">
|
|
<div className="dropdown-menu-columns">
|
|
<div className="dropdown-menu-column">
|
|
{category.items.map(item => (
|
|
<Link
|
|
key={item.id}
|
|
to={item.path}
|
|
className={`dropdown-item${activeTab === item.id ? ' active' : ''}`}
|
|
>
|
|
<item.icon className="icon icon-tabler" />
|
|
{item.title}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Правая часть навбара */}
|
|
<div className="navbar-nav flex-row order-md-last">
|
|
{/* Иконки */}
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Source code">
|
|
<IconCode className="icon icon-tabler" />
|
|
</a>
|
|
</div>
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Sponsor">
|
|
<IconHeart className="icon icon-tabler" />
|
|
</a>
|
|
</div>
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Dark mode">
|
|
<IconMoon className="icon icon-tabler" />
|
|
</a>
|
|
</div>
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Notifications">
|
|
<IconBell className="icon icon-tabler" />
|
|
</a>
|
|
</div>
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Apps">
|
|
<IconActivity className="icon icon-tabler" />
|
|
</a>
|
|
</div>
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="Theme settings">
|
|
<IconSettings className="icon icon-tabler" />
|
|
<span className="badge bg-red"></span>
|
|
</a>
|
|
</div>
|
|
|
|
{/* Пользователь */}
|
|
<div className="nav-item dropdown">
|
|
<a href="#" className="nav-link d-flex lh-1 text-reset p-0 ms-3" aria-label="User menu">
|
|
<span className="avatar avatar-sm" style={{backgroundImage: 'url(./static/avatars/000m.jpg)'}}></span>
|
|
<div className="d-none d-xl-block ps-2">
|
|
<div>Admin User</div>
|
|
<div className="mt-1 small text-muted">Administrator</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<div className="container-xl mt-4">
|
|
<Routes>
|
|
<Route path="/domains" element={
|
|
<DataManager
|
|
entityName="домен"
|
|
entityKey="domains"
|
|
placeholder="example.com"
|
|
/>
|
|
} />
|
|
<Route path="/domains-new" element={<DomainsNewManager />} />
|
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
|
<Route path="/asns" element={<ASNsNewManager />} />
|
|
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
|
<Route path="/servers" element={<ServerManager />} />
|
|
<Route path="/billing" element={<BillingManager />} />
|
|
<Route path="/filters" element={<FilterManager />} />
|
|
<Route path="/" element={<Navigate to="/domains" 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;
|