refactor(PageHeader): standardize header component across various managers by adding icons for improved visual consistency and user experience
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m53s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m53s
This commit is contained in:
@@ -529,6 +529,7 @@ function ASNsNewManager() {
|
|||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="ASNs"
|
title="ASNs"
|
||||||
|
icon={<IconHash size={24} />}
|
||||||
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
||||||
actions={
|
actions={
|
||||||
<PageHeaderActions
|
<PageHeaderActions
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ function AutoUrlManager() {
|
|||||||
<div>
|
<div>
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Автоматические URL"
|
title="Автоматические URL"
|
||||||
|
icon={<IconLink size={24} />}
|
||||||
actions={(
|
actions={(
|
||||||
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||||
<div className="btn-list">
|
<div className="btn-list">
|
||||||
|
|||||||
@@ -576,6 +576,7 @@ function BillingManager() {
|
|||||||
{/* Заголовок */}
|
{/* Заголовок */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Биллинг"
|
title="Биллинг"
|
||||||
|
icon={<IconCreditCard size={24} />}
|
||||||
actions={
|
actions={
|
||||||
<PageHeaderActions
|
<PageHeaderActions
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
@@ -350,30 +350,22 @@ function CommunitiesManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrapper">
|
<div className="page-wrapper">
|
||||||
<div className="page-header d-print-none">
|
<PageHeader
|
||||||
<div className="container-xl">
|
title="Справочник Community"
|
||||||
<div className="row g-2 align-items-center">
|
icon={<IconFilter size={24} />}
|
||||||
<div className="col">
|
actions={
|
||||||
<h2 className="page-title">
|
<PageHeaderActions
|
||||||
<IconFilter className="icon me-2" />
|
loading={loading}
|
||||||
Справочник Community
|
onRefresh={fetchItems}
|
||||||
</h2>
|
disableRefresh={loading}
|
||||||
</div>
|
onImport={handleImport}
|
||||||
<div className="col-auto ms-auto d-print-none">
|
onExport={handleExport}
|
||||||
<PageHeaderActions
|
disableExport={items.length === 0}
|
||||||
loading={loading}
|
onSave={handleSave}
|
||||||
onRefresh={fetchItems}
|
disableSave={loading}
|
||||||
disableRefresh={loading}
|
/>
|
||||||
onImport={handleImport}
|
}
|
||||||
onExport={handleExport}
|
/>
|
||||||
disableExport={items.length === 0}
|
|
||||||
onSave={handleSave}
|
|
||||||
disableSave={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="page-body">
|
<div className="page-body">
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ function Dashboard() {
|
|||||||
{/* Заголовок страницы */}
|
{/* Заголовок страницы */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Панель"
|
title="Панель"
|
||||||
|
icon={<IconCloud size={24} />}
|
||||||
actions={(
|
actions={(
|
||||||
<div className="d-flex align-items-center gap-3">
|
<div className="d-flex align-items-center gap-3">
|
||||||
{lastFetchTime && (
|
{lastFetchTime && (
|
||||||
|
|||||||
@@ -518,6 +518,7 @@ function DomainsNewManager() {
|
|||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Домены"
|
title="Домены"
|
||||||
|
icon={<IconWorld size={24} />}
|
||||||
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
||||||
actions={(
|
actions={(
|
||||||
<PageHeaderActions
|
<PageHeaderActions
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronUp
|
IconChevronUp
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
|
|
||||||
// Функция для получения флага по коду страны
|
// Функция для получения флага по коду страны
|
||||||
const countryToFlag = (code) => {
|
const countryToFlag = (code) => {
|
||||||
@@ -495,18 +496,12 @@ function EasySwitchManager() {
|
|||||||
if (loading && servers.length === 0) {
|
if (loading && servers.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
<div className="page-header">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="Easy Switch"
|
||||||
<div className="col">
|
icon={<IconWorld size={24} />}
|
||||||
<h2 className="page-title">
|
meta="Быстрое переключение шлюзов"
|
||||||
<IconWorld className="me-2" />
|
/>
|
||||||
Easy Switch
|
|
||||||
</h2>
|
|
||||||
<div className="text-muted mt-1">Быстрое переключение шлюзов</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body text-center py-5">
|
<div className="card-body text-center py-5">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<div className="spinner-border text-primary" role="status">
|
||||||
@@ -521,41 +516,32 @@ function EasySwitchManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
{/* Заголовок */}
|
<PageHeader
|
||||||
<div className="page-header mb-3">
|
title="Easy Switch"
|
||||||
<div className="row align-items-center">
|
icon={<IconWorld size={24} />}
|
||||||
<div className="col">
|
meta={`Быстрое переключение шлюзов • ${servers.length} ${servers.length === 1 ? 'сервер' : 'серверов'} • ${communitiesDirectory.length} communities`}
|
||||||
<h2 className="page-title">
|
actions={
|
||||||
<IconWorld className="me-2" />
|
<div className="btn-list">
|
||||||
Easy Switch
|
{hasChanges && (
|
||||||
</h2>
|
<button
|
||||||
<div className="text-muted mt-1">
|
className="btn btn-success"
|
||||||
Быстрое переключение шлюзов • {servers.length} {servers.length === 1 ? 'сервер' : 'серверов'} • {communitiesDirectory.length} communities
|
onClick={handleApplyChanges}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto">
|
|
||||||
<div className="btn-list">
|
|
||||||
{hasChanges && (
|
|
||||||
<button
|
|
||||||
className="btn btn-success"
|
|
||||||
onClick={handleApplyChanges}
|
|
||||||
>
|
|
||||||
<IconDeviceFloppy size={18} className="me-2" />
|
|
||||||
Применить изменения
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleRefresh}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
>
|
||||||
<IconRefresh size={18} className="me-2" />
|
<IconDeviceFloppy size={18} className="me-2" />
|
||||||
Обновить
|
Применить изменения
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconRefresh size={18} className="me-2" />
|
||||||
|
Обновить
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Вкладки */}
|
{/* Вкладки */}
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|||||||
+114
-120
@@ -6,6 +6,7 @@ import FormModal from './components/FormModal.jsx';
|
|||||||
import ConfirmModal from './components/ConfirmModal.jsx';
|
import ConfirmModal from './components/ConfirmModal.jsx';
|
||||||
import FormField from './components/FormField.jsx';
|
import FormField from './components/FormField.jsx';
|
||||||
import Modal from './components/Modal.jsx';
|
import Modal from './components/Modal.jsx';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
import {
|
import {
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconSearch,
|
IconSearch,
|
||||||
@@ -1755,133 +1756,126 @@ function FilterManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrapper">
|
<div className="page-wrapper">
|
||||||
<div className="page-header d-print-none">
|
<PageHeader
|
||||||
<div className="container-xl">
|
title="Управление фильтрами frouting"
|
||||||
<div className="row g-2 align-items-center">
|
icon={<IconFilter size={24} />}
|
||||||
<div className="col">
|
actions={
|
||||||
<h2 className="page-title">Управление фильтрами frouting</h2>
|
<div className="btn-list">
|
||||||
</div>
|
<div className="dropdown">
|
||||||
<div className="col-auto ms-auto d-print-none">
|
<button
|
||||||
<div className="btn-list">
|
className="btn btn-primary dropdown-toggle"
|
||||||
{/* Dropdown выбора сервера */}
|
type="button"
|
||||||
<div className="dropdown">
|
data-bs-toggle="dropdown"
|
||||||
<button
|
aria-expanded="false"
|
||||||
className="btn btn-primary dropdown-toggle"
|
>
|
||||||
type="button"
|
<IconServer className="icon" />
|
||||||
data-bs-toggle="dropdown"
|
{selectedServer ? selectedServer.name : 'Выбрать сервер'}
|
||||||
aria-expanded="false"
|
</button>
|
||||||
>
|
<div className="dropdown-menu dropdown-menu-end" style={{ minWidth: '300px' }}>
|
||||||
<IconServer className="icon" />
|
<div className="px-3 py-2">
|
||||||
{selectedServer ? selectedServer.name : 'Выбрать сервер'}
|
<input
|
||||||
</button>
|
type="text"
|
||||||
<div className="dropdown-menu dropdown-menu-end" style={{ minWidth: '300px' }}>
|
className="form-control form-control-sm"
|
||||||
<div className="px-3 py-2">
|
placeholder="Поиск серверов..."
|
||||||
<input
|
value={serverSearch}
|
||||||
type="text"
|
onChange={(e) => setServerSearch(e.target.value)}
|
||||||
className="form-control form-control-sm"
|
onClick={(e) => e.stopPropagation()}
|
||||||
placeholder="Поиск серверов..."
|
/>
|
||||||
value={serverSearch}
|
|
||||||
onChange={(e) => setServerSearch(e.target.value)}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="dropdown-divider"></div>
|
|
||||||
{activeServers.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h6 className="dropdown-header">Активные серверы</h6>
|
|
||||||
{activeServers.map(server => (
|
|
||||||
<a
|
|
||||||
key={server.id}
|
|
||||||
className={`dropdown-item ${selectedServer?.id === server.id ? 'active' : ''}`}
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedServer(server);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="d-flex align-items-center">
|
|
||||||
<IconServer size={16} className="me-2" />
|
|
||||||
<span className="flex-grow-1">{server.name}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{inactiveServers.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="dropdown-divider"></div>
|
|
||||||
<h6 className="dropdown-header">Неактивные серверы</h6>
|
|
||||||
{inactiveServers.map(server => (
|
|
||||||
<a
|
|
||||||
key={server.id}
|
|
||||||
className={`dropdown-item ${selectedServer?.id === server.id ? 'active' : ''}`}
|
|
||||||
href="#"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSelectedServer(server);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="d-flex align-items-center text-muted">
|
|
||||||
<IconServer size={16} className="me-2" />
|
|
||||||
<span className="flex-grow-1">{server.name}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="dropdown-divider"></div>
|
|
||||||
<button
|
|
||||||
className="dropdown-item"
|
|
||||||
onClick={() => setServerListModalOpen(true)}
|
|
||||||
>
|
|
||||||
<IconSettings size={16} className="me-2" />
|
|
||||||
Управление серверами
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="dropdown-divider"></div>
|
||||||
{selectedServer && (
|
{activeServers.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<h6 className="dropdown-header">Активные серверы</h6>
|
||||||
className="btn btn-outline-primary"
|
{activeServers.map(server => (
|
||||||
onClick={handlePreviewConfig}
|
<a
|
||||||
disabled={serverFilters.length === 0}
|
key={server.id}
|
||||||
>
|
className={`dropdown-item ${selectedServer?.id === server.id ? 'active' : ''}`}
|
||||||
<IconEye className="icon" />
|
href="#"
|
||||||
Просмотр
|
onClick={(e) => {
|
||||||
</button>
|
e.preventDefault();
|
||||||
<button
|
setSelectedServer(server);
|
||||||
className="btn btn-outline-primary"
|
}}
|
||||||
onClick={copyConfigToClipboard}
|
>
|
||||||
disabled={serverFilters.length === 0}
|
<div className="d-flex align-items-center">
|
||||||
>
|
<IconServer size={16} className="me-2" />
|
||||||
<IconCopy className="icon" />
|
<span className="flex-grow-1">{server.name}</span>
|
||||||
Копировать
|
</div>
|
||||||
</button>
|
</a>
|
||||||
<button
|
))}
|
||||||
className="btn btn-success"
|
|
||||||
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
|
||||||
disabled={loading || serverFilters.length === 0}
|
|
||||||
>
|
|
||||||
<IconDeviceFloppy className="icon" />
|
|
||||||
Сохранить
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-primary"
|
|
||||||
onClick={handleBgpUpdate}
|
|
||||||
disabled={bgpUpdateLoading}
|
|
||||||
title="Запустить /system script run update_bgp_filter на выбранном MikroTik"
|
|
||||||
>
|
|
||||||
<IconRefresh className={`icon ${bgpUpdateLoading ? 'spin' : ''}`} />
|
|
||||||
{bgpUpdateLoading ? 'Запуск...' : 'Обновить BGP фильтры'}
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{inactiveServers.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="dropdown-divider"></div>
|
||||||
|
<h6 className="dropdown-header">Неактивные серверы</h6>
|
||||||
|
{inactiveServers.map(server => (
|
||||||
|
<a
|
||||||
|
key={server.id}
|
||||||
|
className={`dropdown-item ${selectedServer?.id === server.id ? 'active' : ''}`}
|
||||||
|
href="#"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedServer(server);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center text-muted">
|
||||||
|
<IconServer size={16} className="me-2" />
|
||||||
|
<span className="flex-grow-1">{server.name}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="dropdown-divider"></div>
|
||||||
|
<button
|
||||||
|
className="dropdown-item"
|
||||||
|
onClick={() => setServerListModalOpen(true)}
|
||||||
|
>
|
||||||
|
<IconSettings size={16} className="me-2" />
|
||||||
|
Управление серверами
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedServer && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handlePreviewConfig}
|
||||||
|
disabled={serverFilters.length === 0}
|
||||||
|
>
|
||||||
|
<IconEye className="icon" />
|
||||||
|
Просмотр
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={copyConfigToClipboard}
|
||||||
|
disabled={serverFilters.length === 0}
|
||||||
|
>
|
||||||
|
<IconCopy className="icon" />
|
||||||
|
Копировать
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-success"
|
||||||
|
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||||
|
disabled={loading || serverFilters.length === 0}
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy className="icon" />
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handleBgpUpdate}
|
||||||
|
disabled={bgpUpdateLoading}
|
||||||
|
title="Запустить /system script run update_bgp_filter на выбранном MikroTik"
|
||||||
|
>
|
||||||
|
<IconRefresh className={`icon ${bgpUpdateLoading ? 'spin' : ''}`} />
|
||||||
|
{bgpUpdateLoading ? 'Запуск...' : 'Обновить BGP фильтры'}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="page-body">
|
<div className="page-body">
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
|
|||||||
@@ -790,6 +790,7 @@ function IPRangesManager() {
|
|||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="IP-диапазоны"
|
title="IP-диапазоны"
|
||||||
|
icon={<IconNetwork size={24} />}
|
||||||
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
meta={<LastSaved timestamp={lastModified} variant="compact" />}
|
||||||
actions={
|
actions={
|
||||||
<PageHeaderActions
|
<PageHeaderActions
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ function MikrotikBackupsManager() {
|
|||||||
<div className="page">
|
<div className="page">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="MikroTik Backups"
|
title="MikroTik Backups"
|
||||||
|
icon={<IconDownload size={24} />}
|
||||||
meta={`Текущий сервер: ${currentServerLabel}`}
|
meta={`Текущий сервер: ${currentServerLabel}`}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ function MikrotikTools() {
|
|||||||
<div className="page">
|
<div className="page">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="MikroTik Tools"
|
title="MikroTik Tools"
|
||||||
|
icon={<IconRoute size={24} />}
|
||||||
meta={`Текущий сервер: ${currentServerLabel}`}
|
meta={`Текущий сервер: ${currentServerLabel}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useNotify } from './components/NotifyProvider.jsx';
|
|||||||
import FormModal from './components/FormModal.jsx';
|
import FormModal from './components/FormModal.jsx';
|
||||||
import FormField from './components/FormField.jsx';
|
import FormField from './components/FormField.jsx';
|
||||||
import ConfirmModal from './components/ConfirmModal.jsx';
|
import ConfirmModal from './components/ConfirmModal.jsx';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
|
import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
|
||||||
import GatewayAutocompleteInput from './components/GatewayAutocompleteInput.jsx';
|
import GatewayAutocompleteInput from './components/GatewayAutocompleteInput.jsx';
|
||||||
import Tooltip from './components/Tooltip.jsx';
|
import Tooltip from './components/Tooltip.jsx';
|
||||||
@@ -3231,100 +3232,91 @@ function NetworkConfigManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="network-config-manager">
|
<div className="network-config-manager">
|
||||||
{/* Page Header */}
|
<PageHeader
|
||||||
<div className="page-header d-print-none mb-4">
|
title="Сетевые настройки"
|
||||||
<div className="row align-items-center">
|
icon={<IconNetwork size={24} />}
|
||||||
<div className="col">
|
actions={
|
||||||
<h2 className="page-title d-flex align-items-center">
|
<div className="btn-list">
|
||||||
<IconNetwork className="me-2" size={28} />
|
<div className="btn-group me-2" role="group">
|
||||||
Сетевые настройки
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="col-auto ms-auto d-print-none">
|
|
||||||
<div className="btn-list">
|
|
||||||
{/* Tabs as button group */}
|
|
||||||
<div className="btn-group me-2" role="group">
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'gateways' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('gateways')}
|
|
||||||
>
|
|
||||||
<IconWorld size={16} className="me-1 d-none d-sm-inline" />
|
|
||||||
Gateways
|
|
||||||
<span className={`badge ms-1 ${activeTab === 'gateways' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
|
||||||
{config.gateways?.length || 0}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'interfaces' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('interfaces')}
|
|
||||||
>
|
|
||||||
<IconRouter size={16} className="me-1 d-none d-sm-inline" />
|
|
||||||
Интерфейсы
|
|
||||||
<span className={`badge ms-1 ${activeTab === 'interfaces' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
|
||||||
{config.tunnelInterfaces?.length || 0}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'pools' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('pools')}
|
|
||||||
>
|
|
||||||
<IconDatabase size={16} className="me-1 d-none d-sm-inline" />
|
|
||||||
IP пулы
|
|
||||||
<span className={`badge ms-1 ${activeTab === 'pools' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
|
||||||
{config.ipPools?.length || 0}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'ip-registry' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('ip-registry')}
|
|
||||||
>
|
|
||||||
<IconList size={16} className="me-1 d-none d-sm-inline" />
|
|
||||||
Реестр IP
|
|
||||||
<span className={`badge ms-1 ${activeTab === 'ip-registry' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
|
||||||
{ipRegistry.length}
|
|
||||||
</span>
|
|
||||||
{ipRegistry.some(item => item.hasConflict) && (
|
|
||||||
<span className="badge bg-danger ms-1" title="Обнаружены конфликты IP адресов">
|
|
||||||
!
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
className={`btn ${activeTab === 'gateways' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
className="btn btn-outline-secondary"
|
onClick={() => setActiveTab('gateways')}
|
||||||
onClick={() => { fetchConfig(); fetchServers(); }}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
>
|
||||||
<IconRefresh size={16} className={loading ? 'spin' : ''} />
|
<IconWorld size={16} className="me-1 d-none d-sm-inline" />
|
||||||
|
Gateways
|
||||||
|
<span className={`badge ms-1 ${activeTab === 'gateways' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{config.gateways?.length || 0}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
className={`btn ${activeTab === 'interfaces' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
className="btn btn-outline-primary"
|
onClick={() => setActiveTab('interfaces')}
|
||||||
onClick={handleGenerateMikrotikCode}
|
>
|
||||||
disabled={loading || (
|
<IconRouter size={16} className="me-1 d-none d-sm-inline" />
|
||||||
(config.gateways || []).filter(gw => gw.type === 'recursive').length === 0 &&
|
Интерфейсы
|
||||||
(config.tunnelInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp).length === 0
|
<span className={`badge ms-1 ${activeTab === 'interfaces' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{config.tunnelInterfaces?.length || 0}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn ${activeTab === 'pools' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
|
onClick={() => setActiveTab('pools')}
|
||||||
|
>
|
||||||
|
<IconDatabase size={16} className="me-1 d-none d-sm-inline" />
|
||||||
|
IP пулы
|
||||||
|
<span className={`badge ms-1 ${activeTab === 'pools' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{config.ipPools?.length || 0}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`btn ${activeTab === 'ip-registry' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
|
onClick={() => setActiveTab('ip-registry')}
|
||||||
|
>
|
||||||
|
<IconList size={16} className="me-1 d-none d-sm-inline" />
|
||||||
|
Реестр IP
|
||||||
|
<span className={`badge ms-1 ${activeTab === 'ip-registry' ? 'bg-white text-primary' : 'bg-primary-lt text-primary'}`}>
|
||||||
|
{ipRegistry.length}
|
||||||
|
</span>
|
||||||
|
{ipRegistry.some(item => item.hasConflict) && (
|
||||||
|
<span className="badge bg-danger ms-1" title="Обнаружены конфликты IP адресов">
|
||||||
|
!
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
title="Генерировать код MikroTik для рекурсивных маршрутов и IP адресов интерфейсов"
|
|
||||||
>
|
|
||||||
<IconCode size={16} className="me-1" />
|
|
||||||
Код для MikroTik
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
<IconDatabase size={16} className={`me-1 ${saving ? 'spin' : ''}`} />
|
|
||||||
Сохранить
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={() => { fetchConfig(); fetchServers(); }}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconRefresh size={16} className={loading ? 'spin' : ''} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handleGenerateMikrotikCode}
|
||||||
|
disabled={loading || (
|
||||||
|
(config.gateways || []).filter(gw => gw.type === 'recursive').length === 0 &&
|
||||||
|
(config.tunnelInterfaces || []).filter(i => i.serverId && i.serverId2 && i.localIp && i.remoteIp).length === 0
|
||||||
|
)}
|
||||||
|
title="Генерировать код MikroTik для рекурсивных маршрутов и IP адресов интерфейсов"
|
||||||
|
>
|
||||||
|
<IconCode size={16} className="me-1" />
|
||||||
|
Код для MikroTik
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
<IconDatabase size={16} className={`me-1 ${saving ? 'spin' : ''}`} />
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="card mb-3">
|
<div className="card mb-3">
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
ServerCard,
|
ServerCard,
|
||||||
ServerBulkActions
|
ServerBulkActions
|
||||||
} from './components/server/index.js';
|
} from './components/server/index.js';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -585,49 +586,39 @@ function ServerManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="server-manager-layout">
|
<div className="server-manager-layout">
|
||||||
{/* Header */}
|
<PageHeader
|
||||||
<div className="page-header d-print-none mb-3">
|
title="Управление серверами"
|
||||||
<div className="row align-items-center">
|
icon={<IconServer size={24} />}
|
||||||
<div className="col">
|
actions={
|
||||||
<h2 className="page-title d-flex align-items-center">
|
<div className="btn-list">
|
||||||
<IconServer className="me-2" />
|
<div className="btn-group me-2" role="group">
|
||||||
Управление серверами
|
<button
|
||||||
</h2>
|
className={`btn ${activeTab === 'servers' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
</div>
|
onClick={() => setActiveTab('servers')}
|
||||||
<div className="col-auto ms-auto d-print-none">
|
>
|
||||||
<div className="btn-list">
|
<IconServer size={16} className="me-1" />
|
||||||
{/* Tabs */}
|
Серверы
|
||||||
<div className="btn-group me-2" role="group">
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'servers' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('servers')}
|
|
||||||
>
|
|
||||||
<IconServer size={16} className="me-1" />
|
|
||||||
Серверы
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`btn ${activeTab === 'connections' ? 'btn-primary' : 'btn-outline-primary'}`}
|
|
||||||
onClick={() => setActiveTab('connections')}
|
|
||||||
>
|
|
||||||
<IconGitBranch size={16} className="me-1" />
|
|
||||||
Связи
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<button type="button" className="btn btn-outline-secondary" onClick={exportAllLinks} disabled={servers.length === 0}>
|
|
||||||
<IconDownload size={16} className="me-1" /> Экспорт
|
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-outline-primary" onClick={handleSaveChanges} disabled={loading}>
|
<button
|
||||||
<IconDatabase size={16} className={loading ? 'spin me-1' : 'me-1'} /> Сохранить
|
className={`btn ${activeTab === 'connections' ? 'btn-primary' : 'btn-outline-primary'}`}
|
||||||
</button>
|
onClick={() => setActiveTab('connections')}
|
||||||
<button type="button" className="btn btn-primary" onClick={handleOpenAddServerModal}>
|
>
|
||||||
<IconPlus size={16} className="me-1" /> Добавить
|
<IconGitBranch size={16} className="me-1" />
|
||||||
|
Связи
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" className="btn btn-outline-secondary" onClick={exportAllLinks} disabled={servers.length === 0}>
|
||||||
|
<IconDownload size={16} className="me-1" /> Экспорт
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-outline-primary" onClick={handleSaveChanges} disabled={loading}>
|
||||||
|
<IconDatabase size={16} className={loading ? 'spin me-1' : 'me-1'} /> Сохранить
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn btn-primary" onClick={handleOpenAddServerModal}>
|
||||||
|
<IconPlus size={16} className="me-1" /> Добавить
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Offcanvas: фильтры (только для вкладки «Серверы») */}
|
{/* Offcanvas: фильтры (только для вкладки «Серверы») */}
|
||||||
{activeTab === 'servers' && (
|
{activeTab === 'servers' && (
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
/** Заголовок страницы в стиле Tabler (page-header + page-title + опционально actions справа) */
|
/**
|
||||||
function PageHeader({ title, pretitle, actions, meta }) {
|
* Единый заголовок страницы в стиле Tabler UI.
|
||||||
|
* Используется на всех страницах для одинакового UX.
|
||||||
|
* @param {string} title - Заголовок страницы
|
||||||
|
* @param {React.ReactNode} [icon] - Иконка слева от заголовка (например, <IconServer />)
|
||||||
|
* @param {string} [pretitle] - Небольшая надпись над заголовком (page-pretitle)
|
||||||
|
* @param {React.ReactNode} [meta] - Подзаголовок или мета-информация под заголовком
|
||||||
|
* @param {React.ReactNode} [actions] - Кнопки/действия справа (btn-list, btn-group)
|
||||||
|
*/
|
||||||
|
function PageHeader({ title, icon, pretitle, actions, meta }) {
|
||||||
return (
|
return (
|
||||||
<div className="page-header d-print-none mb-4">
|
<div className="page-header d-print-none mb-4">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
@@ -9,7 +17,10 @@ function PageHeader({ title, pretitle, actions, meta }) {
|
|||||||
{pretitle && (
|
{pretitle && (
|
||||||
<div className="page-pretitle">{pretitle}</div>
|
<div className="page-pretitle">{pretitle}</div>
|
||||||
)}
|
)}
|
||||||
<h1 className="page-title">{title}</h1>
|
<h1 className="page-title d-flex align-items-center">
|
||||||
|
{icon && <span className="me-2 d-flex align-items-center">{icon}</span>}
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
{meta && (
|
{meta && (
|
||||||
<div className="text-muted small mt-1">{meta}</div>
|
<div className="text-muted small mt-1">{meta}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user