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 [sortBy, setSortBy] = useState('community'); // community | gateway | description
const [sortDir, setSortDir] = useState('asc'); // asc | desc const [sortDir, setSortDir] = useState('asc'); // asc | desc
// Состояние sidebar // Состояние управления серверами
const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [serverListModalOpen, setServerListModalOpen] = useState(false);
const [serverSearch, setServerSearch] = useState(''); const [serverSearch, setServerSearch] = useState('');
// Состояние для инлайн-редактирования и массовых действий // Состояние для инлайн-редактирования и массовых действий
@@ -718,140 +718,107 @@ function FilterManager() {
return ( return (
<div className="page"> <div className="page-wrapper">
{/* Боковая панель для выбора сервера */} <div className="page-header d-print-none">
{!sidebarCollapsed && ( <div className="container-xl">
<aside className="navbar navbar-vertical navbar-expand-lg" style={{ width: '280px' }}> <div className="row g-2 align-items-center">
<div className="container-fluid"> <div className="col">
<div className="navbar-nav"> <h2 className="page-title">Управление фильтрами frouting</h2>
<div className="nav-item mb-3"> <div className="page-pretitle">
<div className="d-flex justify-content-between align-items-center mb-2"> {selectedServer ? (
<strong className="text-uppercase text-muted" style={{ fontSize: '0.65rem', letterSpacing: '0.04em' }}>Серверы</strong> <div className="d-flex align-items-center">
<button <span className="text-muted">Сервер:</span>
className="btn btn-icon btn-sm" <span className="ms-2 text-primary fw-bold">{selectedServer.name}</span>
onClick={() => setSidebarCollapsed(true)} {serverFilters.length > 0 && (
title="Свернуть панель" <span className="badge bg-blue-lt text-blue ms-2">{serverFilters.length} фильтров</span>
> )}
<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> </div>
{activeServers.map(server => ( ) : (
<a <span className="text-muted">Выберите сервер для работы с фильтрами</span>
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>
</div> </div>
</div> </div>
</div> <div className="col-auto ms-auto d-print-none">
</aside> <div className="btn-list">
)} {/* Dropdown выбора сервера */}
<div className="dropdown">
{/* Кнопка для разворачивания боковой панели */} <button
{sidebarCollapsed && ( className="btn btn-primary dropdown-toggle"
<div className="position-fixed top-0 start-0 p-3" style={{ zIndex: 1000 }}> type="button"
<button data-bs-toggle="dropdown"
className="btn btn-primary btn-icon" aria-expanded="false"
onClick={() => setSidebarCollapsed(false)} >
title="Показать панель серверов" <IconServer className="icon" />
> {selectedServer ? selectedServer.name : 'Выбрать сервер'}
<IconServer size={20} /> </button>
</button> <div className="dropdown-menu dropdown-menu-end" style={{ minWidth: '300px' }}>
</div> <div className="px-3 py-2">
)} <input
type="text"
{/* Основной контент */} className="form-control form-control-sm"
<div className="page-wrapper"> placeholder="Поиск серверов..."
<div className="page-header d-print-none"> value={serverSearch}
<div className="container-xl"> onChange={(e) => setServerSearch(e.target.value)}
<div className="row g-2 align-items-center"> onClick={(e) => e.stopPropagation()}
<div className="col"> />
<h2 className="page-title"> </div>
Управление фильтрами frouting <div className="dropdown-divider"></div>
</h2> {activeServers.length > 0 && (
<div className="page-pretitle"> <>
<nav aria-label="breadcrumb"> <h6 className="dropdown-header">Активные серверы</h6>
<ol className="breadcrumb breadcrumb-arrows"> {activeServers.map(server => (
<li className="breadcrumb-item">Главная</li> <a
<li className="breadcrumb-item">Фильтры</li> key={server.id}
{selectedServer && <li className="breadcrumb-item active">{selectedServer.name}</li>} className={`dropdown-item ${selectedServer?.id === server.id ? 'active' : ''}`}
</ol> href="#"
</nav> 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>
{selectedServer && ( {selectedServer && (
<div className="col-auto ms-auto d-print-none"> <>
<div className="btn-list">
<button <button
className="btn btn-outline-primary" className="btn btn-outline-primary"
onClick={handlePreviewConfig} onClick={handlePreviewConfig}
@@ -876,15 +843,16 @@ function FilterManager() {
<IconDeviceFloppy className="icon" /> <IconDeviceFloppy className="icon" />
Сохранить Сохранить
</button> </button>
</div> </>
</div> )}
)} </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<div className="page-body"> <div className="page-body">
<div className="container-xl"> <div className="container-xl">
{/* Уведомления */} {/* Уведомления */}
{error && ( {error && (
<div className="alert alert-danger alert-dismissible" role="alert"> <div className="alert alert-danger alert-dismissible" role="alert">
@@ -1136,7 +1104,6 @@ function FilterManager() {
)} )}
</div> </div>
</div> </div>
</div>
{/* Модальные окна */} {/* Модальные окна */}
<FormModal <FormModal
@@ -1418,6 +1385,128 @@ function FilterManager() {
)} )}
</FormModal> </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 <ConfirmDialog
open={confirmState.open} open={confirmState.open}
title="Подтверждение" title="Подтверждение"