refactor: Simplify FilterManager layout by conditionally rendering server statistics and search section based on advanced mode, and remove custom hover effects in App.css for cleaner UI
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m47s

This commit is contained in:
2025-07-14 15:00:39 +07:00
parent fd769dc81a
commit e6a9c17aa0
2 changed files with 199 additions and 201 deletions
-6
View File
@@ -22,12 +22,6 @@
animation: fadeIn 0.3s ease-out;
}
/* Custom hover effects */
.btn:hover {
transform: translateY(-1px);
transition: all 0.2s ease;
}
/* Custom focus states */
.btn:focus,
.form-control:focus,
+199 -195
View File
@@ -662,230 +662,234 @@ function FilterManager() {
)}
{/* Панель статистики */}
<div className="row mb-4">
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-primary text-white avatar">
<IconServer />
</span>
</div>
<div className="col">
<div className="font-weight-medium">{totalServers}</div>
<div className="text-muted">Всего серверов</div>
{mode === 'advanced' && (
<div className="row mb-4">
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-primary text-white avatar">
<IconServer />
</span>
</div>
<div className="col">
<div className="font-weight-medium">{totalServers}</div>
<div className="text-muted">Всего серверов</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-green text-white avatar">
<IconCheck />
</span>
</div>
<div className="col">
<div className="font-weight-medium">{enabledServers}</div>
<div className="text-muted">Активных серверов</div>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-green text-white avatar">
<IconCheck />
</span>
</div>
<div className="col">
<div className="font-weight-medium">{enabledServers}</div>
<div className="text-muted">Активных серверов</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-blue text-white avatar">
<IconFilter />
</span>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-blue text-white avatar">
<IconFilter />
</span>
</div>
<div className="col">
<div className="font-weight-medium">{selectedServer ? serverFilters.length : '-'}</div>
<div className="text-muted">
{selectedServer ? 'Фильтров в выбранном сервере' : 'Выберите сервер'}
</div>
</div>
</div>
<div className="col">
<div className="font-weight-medium">{selectedServer ? serverFilters.length : '-'}</div>
<div className="text-muted">
{selectedServer ? 'Фильтров в выбранном сервере' : 'Выберите сервер'}
</div>
</div>
</div>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-orange text-white avatar">
<IconDeviceFloppy />
</span>
</div>
<div className="col">
<div className="font-weight-medium">
{!selectedServer ? 'Не выбран' : serverFilters.length > 0 ? 'Готов' : 'Не готов'}
</div>
<div className="text-muted">Статус конфигурации</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-3">
<div className="card card-sm">
<div className="card-body">
<div className="row align-items-center">
<div className="col-auto">
<span className="bg-orange text-white avatar">
<IconDeviceFloppy />
</span>
</div>
<div className="col">
<div className="font-weight-medium">
{!selectedServer ? 'Не выбран' : serverFilters.length > 0 ? 'Готов' : 'Не готов'}
</div>
<div className="text-muted">Статус конфигурации</div>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{/* Секция управления серверами */}
<div className="row mb-4">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">
<IconServer className="me-2" />
Готовые конфигурации серверов
</h3>
<div className="card-actions">
<div className="input-group" style={{ maxWidth: 400 }}>
<span className="input-group-text">
<IconSearch />
</span>
<input
type="text"
className="form-control"
placeholder="Поиск серверов..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button
className="btn btn-primary"
type="button"
onClick={() => setServerConfigModalOpen(true)}
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
>
<IconPlus className="me-1" />
Добавить сервер
</button>
{mode === 'advanced' && (
<div className="row mb-4">
<div className="col-12">
<div className="card">
<div className="card-header">
<h3 className="card-title">
<IconServer className="me-2" />
Готовые конфигурации серверов
</h3>
<div className="card-actions">
<div className="input-group" style={{ maxWidth: 400 }}>
<span className="input-group-text">
<IconSearch />
</span>
<input
type="text"
className="form-control"
placeholder="Поиск серверов..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<button
className="btn btn-primary"
type="button"
onClick={() => setServerConfigModalOpen(true)}
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
>
<IconPlus className="me-1" />
Добавить сервер
</button>
</div>
</div>
</div>
</div>
<div className="card-body">
{servers.length === 0 ? (
<div className="text-center text-muted py-4">
<IconServer className="icon-lg mb-2" />
<p>Нет добавленных серверов</p>
<button
className="btn btn-primary"
onClick={() => setServerConfigModalOpen(true)}
>
<IconPlus className="me-2" />
Добавить первый сервер
</button>
</div>
) : filteredServers.length === 0 ? (
<div className="text-center text-muted py-4">
<IconSearch className="icon-lg mb-2" />
<p>Серверы не найдены по запросу "{searchTerm}"</p>
<button
className="btn btn-outline-secondary"
onClick={() => setSearchTerm('')}
>
Очистить поиск
</button>
</div>
) : (
<div className="row g-3">
{filteredServers.map(server => (
<div key={server.id} className="col-md-6 col-lg-4">
<div className={`card ${selectedServer?.id === server.id ? 'border-primary' : ''} h-100`}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-3">
<div className="flex-grow-1">
<h4 className="card-title mb-1 d-flex align-items-center">
{server.name}
{selectedServer?.id === server.id && (
<span className="badge bg-blue-lt text-blue ms-2">Выбран</span>
)}
</h4>
<div className="d-flex align-items-center gap-2">
<span className={`badge ${server.enabled ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
{server.enabled ? 'Активен' : 'Неактивен'}
</span>
{server.id === selectedServer?.id && (
<span className="badge bg-blue-lt text-blue">
{serverFilters.length} фильтров
<div className="card-body">
{servers.length === 0 ? (
<div className="text-center text-muted py-4">
<IconServer className="icon-lg mb-2" />
<p>Нет добавленных серверов</p>
<button
className="btn btn-primary"
onClick={() => setServerConfigModalOpen(true)}
>
<IconPlus className="me-2" />
Добавить первый сервер
</button>
</div>
) : filteredServers.length === 0 ? (
<div className="text-center text-muted py-4">
<IconSearch className="icon-lg mb-2" />
<p>Серверы не найдены по запросу "{searchTerm}"</p>
<button
className="btn btn-outline-secondary"
onClick={() => setSearchTerm('')}
>
Очистить поиск
</button>
</div>
) : (
<div className="row g-3">
{filteredServers.map(server => (
<div key={server.id} className="col-md-6 col-lg-4">
<div className={`card ${selectedServer?.id === server.id ? 'border-primary' : ''} h-100`}>
<div className="card-body">
<div className="d-flex justify-content-between align-items-start mb-3">
<div className="flex-grow-1">
<h4 className="card-title mb-1 d-flex align-items-center">
{server.name}
{selectedServer?.id === server.id && (
<span className="badge bg-blue-lt text-blue ms-2">Выбран</span>
)}
</h4>
<div className="d-flex align-items-center gap-2">
<span className={`badge ${server.enabled ? 'bg-green-lt text-green' : 'bg-red-lt text-red'}`}>
{server.enabled ? 'Активен' : 'Неактивен'}
</span>
)}
{server.id === selectedServer?.id && (
<span className="badge bg-blue-lt text-blue">
{serverFilters.length} фильтров
</span>
)}
</div>
</div>
<div className="form-check form-switch">
<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 axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
setServers(updatedServers);
} catch (error) {
setError('Не удалось обновить статус сервера');
}
}}
/>
</div>
</div>
<div className="form-check form-switch">
<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 axios.post(`${API_URL}/server-configs`, { servers: updatedServers });
setServers(updatedServers);
} catch (error) {
setError('Не удалось обновить статус сервера');
}
}}
/>
{server.description && (
<p className="text-muted small mb-3">{server.description}</p>
)}
<div className="d-flex gap-2 flex-wrap">
<button
className={`btn btn-sm ${selectedServer?.id === server.id ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setSelectedServer(server)}
>
<IconFilter className="me-1" />
{selectedServer?.id === server.id ? 'Выбран' : 'Выбрать'}
</button>
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => handleEditServer(server)}
title="Редактировать сервер"
>
<IconEdit className="me-1" />
Редактировать
</button>
<button
className="btn btn-outline-warning btn-sm"
onClick={() => handleCopyConfigToAllServers(server.id)}
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
title="Скопировать конфигурацию на все остальные серверы"
>
<IconCopy className="me-1" />
Копировать на все
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={() => handleDeleteServer(server.id)}
>
<IconTrash className="me-1" />
Удалить
</button>
</div>
</div>
{server.description && (
<p className="text-muted small mb-3">{server.description}</p>
)}
<div className="d-flex gap-2 flex-wrap">
<button
className={`btn btn-sm ${selectedServer?.id === server.id ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setSelectedServer(server)}
>
<IconFilter className="me-1" />
{selectedServer?.id === server.id ? 'Выбран' : 'Выбрать'}
</button>
<button
className="btn btn-outline-secondary btn-sm"
onClick={() => handleEditServer(server)}
title="Редактировать сервер"
>
<IconEdit className="me-1" />
Редактировать
</button>
<button
className="btn btn-outline-warning btn-sm"
onClick={() => handleCopyConfigToAllServers(server.id)}
disabled={loading || servers.filter(s => s.id !== server.id && s.enabled).length === 0}
title="Скопировать конфигурацию на все остальные серверы"
>
<IconCopy className="me-1" />
Копировать на все
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={() => handleDeleteServer(server.id)}
>
<IconTrash className="me-1" />
Удалить
</button>
</div>
</div>
</div>
</div>
))}
</div>
)}
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Секция фильтров выбранного сервера */}
{selectedServer && (