Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m35s
604 lines
28 KiB
React
604 lines
28 KiB
React
import { useState, useEffect, createContext, useContext } from 'react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import {
|
|
BrowserRouter as Router,
|
|
Routes,
|
|
Route,
|
|
Link,
|
|
useLocation,
|
|
Navigate
|
|
} from 'react-router-dom';
|
|
import {
|
|
IconBrandTabler,
|
|
IconWorld,
|
|
IconNetwork,
|
|
IconSettings,
|
|
IconHome,
|
|
IconDatabase,
|
|
IconServer,
|
|
IconFilter,
|
|
IconDownload,
|
|
IconCreditCard,
|
|
IconMenu2,
|
|
IconArrowsExchange,
|
|
IconLayoutSidebarLeftExpand,
|
|
IconLayoutNavbar,
|
|
IconChartPie,
|
|
IconChartBar,
|
|
IconClock,
|
|
IconClockPlay,
|
|
IconShield
|
|
} from '@tabler/icons-react';
|
|
import ServerManager from './ServerManager';
|
|
import FilterManager from './FilterManager';
|
|
import EasySwitchManager from './EasySwitchManager';
|
|
import DomainsNewManager from './DomainsNewManager';
|
|
import IPRangesManager from './IPRangesManager';
|
|
import ASNsNewManager from './ASNsNewManager';
|
|
import AutoUrlManager from './AutoUrlManager';
|
|
import BillingManager from './BillingManager';
|
|
import CommunitiesManager from './CommunitiesManager';
|
|
import NetworkConfigManager from './NetworkConfigManager';
|
|
import MikrotikTools from './MikrotikTools.jsx';
|
|
import InterfaceSpeedTest from './InterfaceSpeedTest.jsx';
|
|
import Dashboard from './Dashboard';
|
|
import TrafficDashboard from './TrafficDashboard.jsx';
|
|
import NetworkMapDashboard from './NetworkMapDashboard.jsx';
|
|
import NetworkMapSchedulerPage from './NetworkMapSchedulerPage.jsx';
|
|
import UptimeMonitorPage from './UptimeMonitorPage.jsx';
|
|
import MikrotikBackupsManager from './MikrotikBackupsManager.jsx';
|
|
import PingServicesManager from './PingServicesManager.jsx';
|
|
import FirewallPage from './FirewallPage.jsx';
|
|
import SettingsPage from './SettingsPage.jsx';
|
|
import ResourceStatsPage from './ResourceStatsPage.jsx';
|
|
import './App.css';
|
|
import { NotifyProvider } from './components/NotifyProvider.jsx';
|
|
import ToastContainer from './components/ToastContainer.jsx';
|
|
import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx';
|
|
import ErrorBoundary from './components/ErrorBoundary.jsx';
|
|
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
|
|
import { PingProvider } from './contexts/PingContext.jsx';
|
|
|
|
// --- Simple i18n (RU/EN) ---
|
|
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
|
|
|
|
function LanguageProvider({ children }) {
|
|
const [lang, setLang] = useState(() => localStorage.getItem('lang') || 'ru');
|
|
useEffect(() => { localStorage.setItem('lang', lang); }, [lang]);
|
|
const dict = {
|
|
ru: {
|
|
home: 'Главная', data: 'Данные', management: 'Управление', tools: 'Инструменты',
|
|
dashboard: 'Панель', trafficTraffic: 'Расход трафика', resourceStats: 'Статистика ресурсов', networkMap: 'Карта сети', uptimeMonitor: 'Uptime Monitor', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
|
|
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
|
|
easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы', pingServices: 'Пинг сервисов', firewall: 'Firewall',
|
|
light: 'Светлая', dark: 'Тёмная',
|
|
layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню'
|
|
},
|
|
en: {
|
|
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
|
|
dashboard: 'Dashboard', trafficTraffic: 'Traffic Usage', resourceStats: 'Resource Stats', networkMap: 'Network Map', uptimeMonitor: 'Uptime Monitor', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
|
|
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
|
|
easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups', pingServices: 'Ping Services', firewall: 'Firewall',
|
|
light: 'Light', dark: 'Dark',
|
|
layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu'
|
|
}
|
|
};
|
|
const t = (key) => (dict[lang] && key in dict[lang] ? dict[lang][key] : key);
|
|
return (
|
|
<LanguageContext.Provider value={{ lang, setLang, t }}>
|
|
{children}
|
|
</LanguageContext.Provider>
|
|
);
|
|
}
|
|
|
|
// --- Theme provider (light/dark) ---
|
|
const ThemeContext = createContext({ theme: 'light', setTheme: () => {} });
|
|
|
|
function ThemeProvider({ children }) {
|
|
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
|
|
useEffect(() => {
|
|
localStorage.setItem('theme', theme);
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
}, [theme]);
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
// Единственный экземпляр QueryClient для всего приложения
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
refetchOnWindowFocus: false,
|
|
staleTime: 30_000,
|
|
}
|
|
}
|
|
});
|
|
|
|
function App() {
|
|
return (
|
|
<ErrorBoundary>
|
|
<Router>
|
|
<QueryClientProvider client={queryClient}>
|
|
<LanguageProvider>
|
|
<ThemeProvider>
|
|
<PingProvider>
|
|
<ToastContainer>
|
|
<NotifyProvider>
|
|
<NetworkErrorHandler />
|
|
<MainLayout />
|
|
</NotifyProvider>
|
|
</ToastContainer>
|
|
</PingProvider>
|
|
</ThemeProvider>
|
|
</LanguageProvider>
|
|
</QueryClientProvider>
|
|
</Router>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|
|
|
|
const LAYOUT_FLUID = 'fluid';
|
|
const LAYOUT_HORIZONTAL = 'horizontal';
|
|
|
|
function MainLayout() {
|
|
const location = useLocation();
|
|
const { lang, setLang, t } = useContext(LanguageContext);
|
|
const { theme, setTheme } = useContext(ThemeContext);
|
|
const [layout, setLayout] = useState(() => localStorage.getItem('layout') || LAYOUT_FLUID);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('layout', layout);
|
|
}, [layout]);
|
|
|
|
// Состояние для управления выпадающими меню
|
|
const [dropdownStates, setDropdownStates] = useState({
|
|
data: false,
|
|
management: false,
|
|
tools: false
|
|
});
|
|
|
|
// Закрытие выпадающих меню только при клике в сайдбаре вне открытого меню (клики по странице не закрывают)
|
|
useEffect(() => {
|
|
const handleClickOutside = (event) => {
|
|
const inSidebar = event.target.closest('.navbar-vertical');
|
|
const inDropdown = event.target.closest('.nav-item.dropdown');
|
|
if (inSidebar && !inDropdown) {
|
|
setDropdownStates({
|
|
data: false,
|
|
management: false,
|
|
tools: false
|
|
});
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('click', handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
const toggleDropdown = (dropdown) => {
|
|
setDropdownStates(prev => {
|
|
// Закрываем все выпадающие меню
|
|
const newState = {
|
|
data: false,
|
|
management: false,
|
|
tools: false
|
|
};
|
|
// Открываем только выбранное меню, если оно было закрыто
|
|
if (!prev[dropdown]) {
|
|
newState[dropdown] = true;
|
|
}
|
|
return newState;
|
|
});
|
|
};
|
|
|
|
const navCategories = [
|
|
{
|
|
id: 'home',
|
|
title: t('home'),
|
|
icon: IconHome,
|
|
items: [
|
|
{ id: 'dashboard', title: t('dashboard'), path: '/dashboard', icon: IconHome },
|
|
{ id: 'traffic', title: t('trafficTraffic'), path: '/traffic', icon: IconChartPie },
|
|
{ id: 'resource-stats', title: t('resourceStats'), path: '/resource-stats', icon: IconChartBar },
|
|
{ id: 'network-map', title: t('networkMap'), path: '/network-map', icon: IconNetwork },
|
|
{ id: 'uptime-monitor', title: t('uptimeMonitor'), path: '/uptime-monitor', icon: IconClock }
|
|
]
|
|
},
|
|
{
|
|
id: 'data',
|
|
title: t('data'),
|
|
icon: IconDatabase,
|
|
items: [
|
|
{ id: 'domains', title: t('domains'), path: '/domains', icon: IconWorld },
|
|
{ id: 'ip-ranges', title: t('ipRanges'), path: '/ip-ranges', icon: IconNetwork },
|
|
{ id: 'asns', title: t('asns'), path: '/asns', icon: IconNetwork },
|
|
{ id: 'communities', title: t('communities'), path: '/communities', icon: IconFilter }
|
|
]
|
|
},
|
|
{
|
|
id: 'management',
|
|
title: t('management'),
|
|
icon: IconServer,
|
|
items: [
|
|
{ id: 'servers', title: t('servers'), path: '/servers', icon: IconServer },
|
|
{ id: 'filters', title: t('filters'), path: '/filters', icon: IconFilter },
|
|
{ id: 'network-config', title: t('networkConfig'), path: '/network-config', icon: IconNetwork },
|
|
{ id: 'easy-switch', title: t('easySwitch'), path: '/easy-switch', icon: IconArrowsExchange },
|
|
{ id: 'billing', title: t('billing'), path: '/billing', icon: IconCreditCard }
|
|
]
|
|
},
|
|
{
|
|
id: 'tools',
|
|
title: t('tools'),
|
|
icon: IconSettings,
|
|
items: [
|
|
{ id: 'auto-urls', title: t('autoUrls'), path: '/auto-urls', icon: IconDownload },
|
|
{ id: 'mikrotik-backups', title: t('mikrotikBackups'), path: '/mikrotik-backups', icon: IconDatabase },
|
|
{ id: 'mikrotik-tools', title: 'MikroTik Инструменты', path: '/mikrotik-tools', icon: IconNetwork },
|
|
{ id: 'firewall', title: t('firewall'), path: '/firewall', icon: IconShield },
|
|
{ id: 'interface-speed', title: 'Скорость интерфейсов', path: '/interface-speed', icon: IconNetwork },
|
|
{ id: 'ping-services', title: t('pingServices'), path: '/ping-services', icon: IconNetwork },
|
|
{ id: 'uptime-monitor', title: t('uptimeMonitor'), path: '/uptime-monitor', icon: IconClock },
|
|
{ id: 'scheduler', title: 'Планировщик карты сети', path: '/scheduler', icon: IconClockPlay }
|
|
]
|
|
},
|
|
{
|
|
id: 'settings',
|
|
title: 'Настройки',
|
|
icon: IconSettings,
|
|
single: true,
|
|
path: '/settings'
|
|
}
|
|
];
|
|
|
|
// Определяем активную вкладку по адресу
|
|
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();
|
|
|
|
// Старый вид: один верхний навбар с горизонтальным меню
|
|
if (layout === LAYOUT_HORIZONTAL) {
|
|
return (
|
|
<div className="page d-flex flex-column min-vh-100">
|
|
<a href="#main-content" className="skip-link">Перейти к основному содержанию</a>
|
|
<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>
|
|
<button
|
|
className="navbar-toggler d-md-none"
|
|
type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#navbar-menu-horizontal"
|
|
aria-controls="navbar-menu-horizontal"
|
|
aria-expanded="false"
|
|
aria-label="Toggle navigation"
|
|
>
|
|
<IconMenu2 />
|
|
</button>
|
|
<div className="collapse navbar-collapse" id="navbar-menu-horizontal">
|
|
<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();
|
|
e.stopPropagation();
|
|
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" onClick={(e) => e.stopPropagation()}>
|
|
<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 me-1">
|
|
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
|
|
{layout === LAYOUT_FLUID ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutNavbar size={20} />}
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_FLUID ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_FLUID); }}>
|
|
<IconLayoutSidebarLeftExpand className="me-2" size={18} /> {t('layoutSidebar')}
|
|
</a>
|
|
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_HORIZONTAL ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_HORIZONTAL); }}>
|
|
<IconLayoutNavbar className="me-2" size={18} /> {t('layoutHorizontal')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item dropdown me-1">
|
|
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
|
|
<span className="badge">{lang.toUpperCase()}</span>
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('ru'); }}>Русский</a>
|
|
<a href="#" className={`dropdown-item ${lang === 'en' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('en'); }}>English</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item dropdown me-1">
|
|
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
|
|
{theme === 'dark' ? '🌙' : '☀️'}
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item ${theme === 'light' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('light'); }}>☀️ {t('light')}</a>
|
|
<a href="#" className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('dark'); }}>🌙 {t('dark')}</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item me-1">
|
|
<KeyboardShortcutsButton className="nav-link px-2" />
|
|
</div>
|
|
<div className="nav-item">
|
|
<Link to="/settings" className="nav-link px-2" title="Настройки">
|
|
<IconSettings size={20} />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
|
|
<Routes>
|
|
<Route path="/dashboard" element={<Dashboard />} />
|
|
<Route path="/traffic" element={<TrafficDashboard />} />
|
|
<Route path="/resource-stats" element={<ResourceStatsPage />} />
|
|
<Route path="/network-map" element={<NetworkMapDashboard />} />
|
|
<Route path="/uptime-monitor" element={<UptimeMonitorPage />} />
|
|
<Route path="/domains" element={<DomainsNewManager />} />
|
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
|
<Route path="/asns" element={<ASNsNewManager />} />
|
|
<Route path="/communities" element={<CommunitiesManager />} />
|
|
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
|
<Route path="/servers" element={<ServerManager />} />
|
|
<Route path="/billing" element={<BillingManager />} />
|
|
<Route path="/filters" element={<FilterManager />} />
|
|
<Route path="/network-config" element={<NetworkConfigManager />} />
|
|
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
|
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
|
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
|
<Route path="/firewall" element={<FirewallPage />} />
|
|
<Route path="/interface-speed" element={<InterfaceSpeedTest />} />
|
|
<Route path="/ping-services" element={<PingServicesManager />} />
|
|
<Route path="/scheduler" element={<NetworkMapSchedulerPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
</Routes>
|
|
</main>
|
|
<footer className="footer mt-auto py-2">
|
|
<div className="container-xl text-muted small">Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}</div>
|
|
</footer>
|
|
<CommandPalette />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Fluid Vertical: вертикальный сайдбар + верхний навбар + page-wrapper
|
|
return (
|
|
<div className="page">
|
|
{/* Skip link для доступности */}
|
|
<a href="#main-content" className="skip-link">
|
|
Перейти к основному содержанию
|
|
</a>
|
|
|
|
{/* Левая вертикальная навигация — всегда тёмная (Tabler navbar-vertical) */}
|
|
<header className="navbar navbar-vertical navbar-expand-md d-print-none" data-bs-theme="dark">
|
|
<div className="container-fluid">
|
|
<Link to="/" className="navbar-brand navbar-brand-autodark">
|
|
<IconBrandTabler className="navbar-brand-icon" />
|
|
<span className="fw-bold">S3 Lists Manager</span>
|
|
</Link>
|
|
<div className="navbar-nav flex-row d-md-none">
|
|
<button
|
|
className="navbar-toggler"
|
|
type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#sidebar-menu"
|
|
aria-controls="sidebar-menu"
|
|
aria-expanded="false"
|
|
aria-label="Открыть меню"
|
|
>
|
|
<IconMenu2 />
|
|
</button>
|
|
</div>
|
|
<div className="collapse navbar-collapse" id="sidebar-menu">
|
|
<ul className="navbar-nav pt-lg-3">
|
|
{navCategories.map(category => (
|
|
<li key={category.id} className={`nav-item${category.items ? ' dropdown' : ''}${(category.single && activeTab === category.id) || (category.items && category.items.some(i => activeTab === i.id)) ? ' active' : ''}`}>
|
|
{category.single ? (
|
|
<Link
|
|
to={category.path}
|
|
className="nav-link"
|
|
>
|
|
<span className="nav-link-icon">
|
|
<category.icon className="icon" />
|
|
</span>
|
|
<span className="nav-link-title">{category.title}</span>
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<a
|
|
className="nav-link dropdown-toggle"
|
|
href="#"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
toggleDropdown(category.id);
|
|
}}
|
|
aria-expanded={dropdownStates[category.id]}
|
|
>
|
|
<span className="nav-link-icon">
|
|
<category.icon className="icon" />
|
|
</span>
|
|
<span className="nav-link-title">{category.title}</span>
|
|
</a>
|
|
<div className={`dropdown-menu${dropdownStates[category.id] ? ' show' : ''}`}>
|
|
{category.items.map(item => (
|
|
<Link
|
|
key={item.id}
|
|
to={item.path}
|
|
className={`dropdown-item d-flex align-items-center${activeTab === item.id ? ' active' : ''}`}
|
|
>
|
|
<span className="nav-link-icon me-2">
|
|
<item.icon className="icon" />
|
|
</span>
|
|
{item.title}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
{/* Горизонтальный ряд значков в самом низу сайдбара */}
|
|
<div className="navbar-sidebar-footer border-top mt-auto">
|
|
<div className="d-flex align-items-center justify-content-center gap-1 py-2 px-2">
|
|
<div className="nav-item dropdown dropup">
|
|
<a href="#" className="nav-link btn-icon rounded" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()} title="Язык">
|
|
<span className="badge bg-secondary">{lang.toUpperCase()}</span>
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('ru'); }}>Русский</a>
|
|
<a href="#" className={`dropdown-item ${lang === 'en' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('en'); }}>English</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item dropdown dropup">
|
|
<a href="#" className="nav-link btn-icon rounded" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()} title={t('layoutSidebar')}>
|
|
{layout === LAYOUT_FLUID ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutNavbar size={20} />}
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_FLUID ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_FLUID); }}>
|
|
<IconLayoutSidebarLeftExpand className="me-2" size={18} /> {t('layoutSidebar')}
|
|
</a>
|
|
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_HORIZONTAL ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_HORIZONTAL); }}>
|
|
<IconLayoutNavbar className="me-2" size={18} /> {t('layoutHorizontal')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item dropdown dropup">
|
|
<a href="#" className="nav-link btn-icon rounded" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()} title={t('light') + ' / ' + t('dark')}>
|
|
{theme === 'dark' ? '🌙' : '☀️'}
|
|
</a>
|
|
<div className="dropdown-menu dropdown-menu-end">
|
|
<a href="#" className={`dropdown-item ${theme === 'light' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('light'); }}>☀️ {t('light')}</a>
|
|
<a href="#" className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('dark'); }}>🌙 {t('dark')}</a>
|
|
</div>
|
|
</div>
|
|
<div className="nav-item">
|
|
<KeyboardShortcutsButton className="nav-link btn-icon rounded" title="Горячие клавиши" />
|
|
</div>
|
|
<div className="nav-item">
|
|
<Link to="/settings" className="nav-link btn-icon rounded" title="Настройки">
|
|
<IconSettings size={20} />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="page-wrapper">
|
|
<div className="page-body">
|
|
<div className="container-fluid">
|
|
<main id="main-content" tabIndex="-1">
|
|
<Routes>
|
|
<Route path="/dashboard" element={<Dashboard />} />
|
|
<Route path="/traffic" element={<TrafficDashboard />} />
|
|
<Route path="/resource-stats" element={<ResourceStatsPage />} />
|
|
<Route path="/network-map" element={<NetworkMapDashboard />} />
|
|
<Route path="/uptime-monitor" element={<UptimeMonitorPage />} />
|
|
<Route path="/domains" element={<DomainsNewManager />} />
|
|
<Route path="/ip-ranges" element={<IPRangesManager />} />
|
|
<Route path="/asns" element={<ASNsNewManager />} />
|
|
<Route path="/communities" element={<CommunitiesManager />} />
|
|
<Route path="/auto-urls" element={<AutoUrlManager />} />
|
|
<Route path="/servers" element={<ServerManager />} />
|
|
<Route path="/billing" element={<BillingManager />} />
|
|
<Route path="/filters" element={<FilterManager />} />
|
|
<Route path="/network-config" element={<NetworkConfigManager />} />
|
|
<Route path="/easy-switch" element={<EasySwitchManager />} />
|
|
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
|
|
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
|
|
<Route path="/firewall" element={<FirewallPage />} />
|
|
<Route path="/interface-speed" element={<InterfaceSpeedTest />} />
|
|
<Route path="/ping-services" element={<PingServicesManager />} />
|
|
<Route path="/scheduler" element={<NetworkMapSchedulerPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
</Routes>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<footer className="footer footer-transparent d-print-none">
|
|
<div className="container-fluid">
|
|
<ul className="list-inline list-inline-dots mb-0 text-muted small">
|
|
<li className="list-inline-item">Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}</li>
|
|
</ul>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<CommandPalette />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
export default App;
|