feat: Add search functionality and server statistics panel in FilterManager for improved user experience and server management insights
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m59s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m59s
This commit is contained in:
+245
-55
@@ -19,7 +19,9 @@ import {
|
||||
IconEye,
|
||||
IconServer,
|
||||
IconSettings,
|
||||
IconDeviceFloppy
|
||||
IconDeviceFloppy,
|
||||
IconHash,
|
||||
IconRocket
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const API_URL = '/api';
|
||||
@@ -490,7 +492,19 @@ function FilterManager() {
|
||||
}
|
||||
};
|
||||
|
||||
// Удаляем старые переменные фильтрации
|
||||
// Состояние для поиска
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
// Фильтрация серверов по поиску
|
||||
const filteredServers = servers.filter(server =>
|
||||
server.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(server.description && server.description.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
// Статистика
|
||||
const totalServers = servers.length;
|
||||
const enabledServers = servers.filter(s => s.enabled).length;
|
||||
const serversWithFilters = servers.filter(s => s.id === selectedServer?.id ? serverFilters.length > 0 : false).length;
|
||||
|
||||
return (
|
||||
<div className="container-xl">
|
||||
@@ -507,6 +521,8 @@ function FilterManager() {
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handlePreviewConfig}
|
||||
disabled={serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Предварительный просмотр конфигурации"}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
@@ -514,6 +530,8 @@ function FilterManager() {
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={copyConfigToClipboard}
|
||||
disabled={serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Копировать конфигурацию в буфер"}
|
||||
>
|
||||
<IconCopy className="me-2" />
|
||||
Копировать конфигурацию
|
||||
@@ -550,6 +568,82 @@ function FilterManager() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Панель статистики */}
|
||||
<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 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 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>
|
||||
</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">
|
||||
@@ -560,6 +654,18 @@ function FilterManager() {
|
||||
Готовые конфигурации серверов
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<div className="input-group input-group-sm me-3" style={{ width: '300px' }}>
|
||||
<span className="input-group-text">
|
||||
<IconSearch />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск серверов..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setServerConfigModalOpen(true)}
|
||||
@@ -582,14 +688,42 @@ function FilterManager() {
|
||||
Добавить первый сервер
|
||||
</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">
|
||||
{servers.map(server => (
|
||||
{filteredServers.map(server => (
|
||||
<div key={server.id} className="col-md-6 col-lg-4">
|
||||
<div className={`card ${selectedServer?.id === server.id ? 'border-primary' : ''}`}>
|
||||
<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-2">
|
||||
<h4 className="card-title mb-0">{server.name}</h4>
|
||||
<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-primary 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"
|
||||
@@ -609,10 +743,12 @@ function FilterManager() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{server.description && (
|
||||
<p className="text-muted small mb-3">{server.description}</p>
|
||||
)}
|
||||
<div className="d-flex gap-2">
|
||||
|
||||
<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)}
|
||||
@@ -654,51 +790,85 @@ function FilterManager() {
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconFilter className="me-2" />
|
||||
Фильтры сервера: {selectedServer.name}
|
||||
<span className="badge bg-green-lt text-green ms-2">
|
||||
{serverFilters.length} фильтров
|
||||
</span>
|
||||
</h3>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm me-2"
|
||||
onClick={handlePreviewConfig}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm me-2"
|
||||
onClick={copyConfigToClipboard}
|
||||
>
|
||||
<IconCopy className="me-2" />
|
||||
Копировать конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm me-2"
|
||||
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||
disabled={loading || serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Сохранить конфигурацию"}
|
||||
>
|
||||
<IconDeviceFloppy className="me-2" />
|
||||
Сохранить конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setAddFilterModalOpen(true)}
|
||||
>
|
||||
<IconPlus className="me-2" />
|
||||
Добавить фильтр
|
||||
</button>
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h3 className="card-title">
|
||||
<IconFilter className="me-2" />
|
||||
Фильтры сервера: {selectedServer.name}
|
||||
<span className="badge bg-green-lt text-green ms-2">
|
||||
{serverFilters.length} фильтров
|
||||
</span>
|
||||
</h3>
|
||||
<div className="text-muted">
|
||||
Управление фильтрами community/gateway для генерации конфигурации MikroTik
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={handlePreviewConfig}
|
||||
disabled={serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Предварительный просмотр конфигурации"}
|
||||
>
|
||||
<IconEye className="me-2" />
|
||||
Предварительный просмотр
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm"
|
||||
onClick={copyConfigToClipboard}
|
||||
disabled={serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Копировать конфигурацию в буфер"}
|
||||
>
|
||||
<IconCopy className="me-2" />
|
||||
Копировать конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm"
|
||||
onClick={() => handleSaveServerConfig(selectedServer.id)}
|
||||
disabled={loading || serverFilters.length === 0}
|
||||
title={serverFilters.length === 0 ? "Сначала добавьте фильтры" : "Сохранить конфигурацию"}
|
||||
>
|
||||
<IconDeviceFloppy className="me-2" />
|
||||
Сохранить конфигурацию
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={() => setAddFilterModalOpen(true)}
|
||||
>
|
||||
<IconPlus className="me-2" />
|
||||
Добавить фильтр
|
||||
</button>
|
||||
{serverFilters.length > 0 && (
|
||||
<button
|
||||
className="btn btn-outline-warning btn-sm"
|
||||
onClick={handleDeployToAllServers}
|
||||
disabled={loading || servers.filter(s => s.enabled && s.id !== selectedServer.id).length === 0}
|
||||
title="Развернуть фильтры и конфигурацию на все активные серверы"
|
||||
>
|
||||
<IconRocket className="me-2" />
|
||||
Развернуть на все серверы
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{serverFilters.length === 0 ? (
|
||||
{loading ? (
|
||||
<div className="text-center text-muted py-4">
|
||||
<div className="spinner-border spinner-border-sm me-2" role="status">
|
||||
<span className="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
Загрузка фильтров...
|
||||
</div>
|
||||
) : serverFilters.length === 0 ? (
|
||||
<div className="text-center text-muted py-4">
|
||||
<IconFilter className="icon-lg mb-2" />
|
||||
<p>Нет фильтров для этого сервера</p>
|
||||
<p className="small text-muted mb-3">
|
||||
Добавьте фильтры community/gateway для генерации конфигурации MikroTik
|
||||
</p>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => setAddFilterModalOpen(true)}
|
||||
@@ -712,27 +882,46 @@ function FilterManager() {
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>COMMUNITY</th>
|
||||
<th>GATEWAY</th>
|
||||
<th>ОПИСАНИЕ</th>
|
||||
<th className="text-end">ДЕЙСТВИЯ</th>
|
||||
<th style={{ width: '25%' }}>COMMUNITY</th>
|
||||
<th style={{ width: '25%' }}>GATEWAY</th>
|
||||
<th style={{ width: '35%' }}>ОПИСАНИЕ</th>
|
||||
<th style={{ width: '15%' }} className="text-end">ДЕЙСТВИЯ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{serverFilters.map((filter) => (
|
||||
<tr key={filter.community}>
|
||||
{serverFilters.map((filter, index) => (
|
||||
<tr key={`${filter.community}-${index}`}>
|
||||
<td>
|
||||
<span className="badge bg-blue-lt text-blue">{filter.community}</span>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="badge bg-blue-lt text-blue me-2">
|
||||
<IconHash size={12} />
|
||||
</span>
|
||||
<code className="text-blue">{filter.community}</code>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className="badge bg-green-lt text-green">{filter.gateway}</span>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="badge bg-green-lt text-green me-2">
|
||||
<IconServer size={12} />
|
||||
</span>
|
||||
<code className="text-green">{filter.gateway}</code>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="text-truncate" style={{ maxWidth: '300px' }}>
|
||||
{filter.description ? (
|
||||
<span className="text-muted">{filter.description}</span>
|
||||
) : (
|
||||
<span className="text-muted fst-italic">Без описания</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td>{filter.description || '-'}</td>
|
||||
<td className="text-end">
|
||||
<div className="btn-list">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-icon btn-sm"
|
||||
onClick={() => handleEditFilter(filter)}
|
||||
title="Редактировать фильтр"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
@@ -742,6 +931,7 @@ function FilterManager() {
|
||||
setFilterToDelete(filter);
|
||||
setDeleteFilterModalOpen(true);
|
||||
}}
|
||||
title="Удалить фильтр"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user