fix: Replace sidebar with dropdown for server selection in FilterManager
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
+227
-138
@@ -70,8 +70,8 @@ function FilterManager() {
|
||||
const [sortBy, setSortBy] = useState('community'); // community | gateway | description
|
||||
const [sortDir, setSortDir] = useState('asc'); // asc | desc
|
||||
|
||||
// Состояние sidebar
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
// Состояние управления серверами
|
||||
const [serverListModalOpen, setServerListModalOpen] = useState(false);
|
||||
const [serverSearch, setServerSearch] = useState('');
|
||||
|
||||
// Состояние для инлайн-редактирования и массовых действий
|
||||
@@ -718,140 +718,107 @@ function FilterManager() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
{/* Боковая панель для выбора сервера */}
|
||||
{!sidebarCollapsed && (
|
||||
<aside className="navbar navbar-vertical navbar-expand-lg" style={{ width: '280px' }}>
|
||||
<div className="container-fluid">
|
||||
<div className="navbar-nav">
|
||||
<div className="nav-item mb-3">
|
||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||
<strong className="text-uppercase text-muted" style={{ fontSize: '0.65rem', letterSpacing: '0.04em' }}>Серверы</strong>
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => setSidebarCollapsed(true)}
|
||||
title="Свернуть панель"
|
||||
>
|
||||
<IconChevronDown size={16} style={{ transform: 'rotate(90deg)' }} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Поиск..."
|
||||
value={serverSearch}
|
||||
onChange={(e) => setServerSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Активные серверы */}
|
||||
{activeServers.length > 0 && (
|
||||
<div className="nav-item mb-3">
|
||||
<div className="text-uppercase text-muted mb-2" style={{ fontSize: '0.625rem', letterSpacing: '0.04em' }}>
|
||||
Активные ({activeServers.length})
|
||||
<div className="page-wrapper">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="container-xl">
|
||||
<div className="row g-2 align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">Управление фильтрами frouting</h2>
|
||||
<div className="page-pretitle">
|
||||
{selectedServer ? (
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="text-muted">Сервер:</span>
|
||||
<span className="ms-2 text-primary fw-bold">{selectedServer.name}</span>
|
||||
{serverFilters.length > 0 && (
|
||||
<span className="badge bg-blue-lt text-blue ms-2">{serverFilters.length} фильтров</span>
|
||||
)}
|
||||
</div>
|
||||
{activeServers.map(server => (
|
||||
<a
|
||||
key={server.id}
|
||||
href="#"
|
||||
className={`nav-link ${selectedServer?.id === server.id ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedServer(server);
|
||||
}}
|
||||
>
|
||||
<span className="nav-link-icon">
|
||||
<IconServer size={18} />
|
||||
</span>
|
||||
<span className="nav-link-title">{server.name}</span>
|
||||
{selectedServer?.id === server.id && serverFilters.length > 0 && (
|
||||
<span className="badge bg-primary ms-auto">{serverFilters.length}</span>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Неактивные серверы */}
|
||||
{inactiveServers.length > 0 && (
|
||||
<div className="nav-item mb-3">
|
||||
<div className="text-uppercase text-muted mb-2" style={{ fontSize: '0.625rem', letterSpacing: '0.04em' }}>
|
||||
Неактивные ({inactiveServers.length})
|
||||
</div>
|
||||
{inactiveServers.map(server => (
|
||||
<a
|
||||
key={server.id}
|
||||
href="#"
|
||||
className={`nav-link ${selectedServer?.id === server.id ? 'active' : ''}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectedServer(server);
|
||||
}}
|
||||
>
|
||||
<span className="nav-link-icon">
|
||||
<IconServer size={18} className="text-muted" />
|
||||
</span>
|
||||
<span className="nav-link-title text-muted">{server.name}</span>
|
||||
{selectedServer?.id === server.id && serverFilters.length > 0 && (
|
||||
<span className="badge bg-secondary ms-auto">{serverFilters.length}</span>
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Кнопка добавления сервера */}
|
||||
<div className="nav-item mt-auto">
|
||||
<button
|
||||
className="btn btn-primary btn-sm w-100"
|
||||
onClick={() => setServerConfigModalOpen(true)}
|
||||
>
|
||||
<IconPlus size={16} className="me-1" />
|
||||
Добавить сервер
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-muted">Выберите сервер для работы с фильтрами</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
)}
|
||||
|
||||
{/* Кнопка для разворачивания боковой панели */}
|
||||
{sidebarCollapsed && (
|
||||
<div className="position-fixed top-0 start-0 p-3" style={{ zIndex: 1000 }}>
|
||||
<button
|
||||
className="btn btn-primary btn-icon"
|
||||
onClick={() => setSidebarCollapsed(false)}
|
||||
title="Показать панель серверов"
|
||||
>
|
||||
<IconServer size={20} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Основной контент */}
|
||||
<div className="page-wrapper">
|
||||
<div className="page-header d-print-none">
|
||||
<div className="container-xl">
|
||||
<div className="row g-2 align-items-center">
|
||||
<div className="col">
|
||||
<h2 className="page-title">
|
||||
Управление фильтрами frouting
|
||||
</h2>
|
||||
<div className="page-pretitle">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol className="breadcrumb breadcrumb-arrows">
|
||||
<li className="breadcrumb-item">Главная</li>
|
||||
<li className="breadcrumb-item">Фильтры</li>
|
||||
{selectedServer && <li className="breadcrumb-item active">{selectedServer.name}</li>}
|
||||
</ol>
|
||||
</nav>
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="btn-list">
|
||||
{/* Dropdown выбора сервера */}
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-primary dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<IconServer className="icon" />
|
||||
{selectedServer ? selectedServer.name : 'Выбрать сервер'}
|
||||
</button>
|
||||
<div className="dropdown-menu dropdown-menu-end" style={{ minWidth: '300px' }}>
|
||||
<div className="px-3 py-2">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
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>
|
||||
{selectedServer && (
|
||||
<div className="col-auto ms-auto d-print-none">
|
||||
<div className="btn-list">
|
||||
|
||||
{selectedServer && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handlePreviewConfig}
|
||||
@@ -876,15 +843,16 @@ function FilterManager() {
|
||||
<IconDeviceFloppy className="icon" />
|
||||
Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
<div className="page-body">
|
||||
<div className="container-xl">
|
||||
{/* Уведомления */}
|
||||
{error && (
|
||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||
@@ -1136,7 +1104,6 @@ function FilterManager() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Модальные окна */}
|
||||
<FormModal
|
||||
@@ -1418,6 +1385,128 @@ function FilterManager() {
|
||||
)}
|
||||
</FormModal>
|
||||
|
||||
{/* Модальное окно управления серверами */}
|
||||
<Modal
|
||||
show={serverListModalOpen}
|
||||
onClose={() => setServerListModalOpen(false)}
|
||||
title="Управление серверами"
|
||||
size="xl"
|
||||
>
|
||||
<div className="mb-3">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setServerListModalOpen(false);
|
||||
setServerConfigModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<IconPlus className="icon" />
|
||||
Добавить сервер
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{servers.length === 0 ? (
|
||||
<div className="empty">
|
||||
<div className="empty-icon">
|
||||
<IconServer size={48} />
|
||||
</div>
|
||||
<p className="empty-title">Нет серверов</p>
|
||||
<p className="empty-subtitle text-muted">
|
||||
Создайте первый сервер для работы с фильтрами
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Описание</th>
|
||||
<th>Статус</th>
|
||||
<th className="w-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{servers.map(server => (
|
||||
<tr key={server.id}>
|
||||
<td>
|
||||
<div className="d-flex align-items-center">
|
||||
<IconServer size={18} className="me-2 text-muted" />
|
||||
<strong>{server.name}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-muted">{server.description || '—'}</td>
|
||||
<td>
|
||||
<span className={`badge ${server.enabled ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
|
||||
{server.enabled ? 'Активен' : 'Неактивен'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="btn-list">
|
||||
<div className="form-check form-switch m-0">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={server.enabled}
|
||||
onChange={async () => {
|
||||
const updatedServers = servers.map(s =>
|
||||
s.id === server.id ? { ...s, enabled: !s.enabled } : s
|
||||
);
|
||||
try {
|
||||
await api.post(`/server-configs`, { servers: updatedServers });
|
||||
setServers(updatedServers);
|
||||
} catch (error) {
|
||||
setError('Не удалось обновить статус сервера');
|
||||
}
|
||||
}}
|
||||
title={server.enabled ? 'Деактивировать' : 'Активировать'}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => {
|
||||
setServerListModalOpen(false);
|
||||
setSelectedServer(server);
|
||||
}}
|
||||
title="Выбрать"
|
||||
>
|
||||
<IconFilter size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => {
|
||||
setServerListModalOpen(false);
|
||||
handleEditServer(server);
|
||||
}}
|
||||
title="Редактировать"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => handleCopyConfigToAllServers(server.id)}
|
||||
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
|
||||
title="Копировать на все серверы"
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-icon btn-sm text-danger"
|
||||
onClick={() => handleDeleteServer(server.id)}
|
||||
title="Удалить"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmState.open}
|
||||
title="Подтверждение"
|
||||
|
||||
Reference in New Issue
Block a user