fix: Replace sidebar with dropdown for server selection in FilterManager
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s

This commit is contained in:
2025-11-13 15:43:27 +07:00
parent a1d59cf36a
commit bf6735a470
+227 -138
View File
@@ -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="Подтверждение"