feat: Refactor ServerManager layout to improve server list display and enhance filtering options
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 18m28s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 18m28s
This commit is contained in:
+150
-189
@@ -433,200 +433,161 @@ function ServerManager() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row">
|
{/* Таблица серверов на всю ширину */}
|
||||||
<div className="col-lg-4">
|
<div className="card w-100">
|
||||||
{/* Add New Server Card */}
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
<div className="card card-md">
|
<h3 className="card-title mb-0">Список серверов</h3>
|
||||||
<div className="card-header">
|
<div className="d-flex gap-2 w-75">
|
||||||
<h3 className="card-title">
|
{/* Фильтры и поиск */}
|
||||||
<IconPlus className="icon me-2" />
|
<select className="form-select w-auto" value={filterTunnel} onChange={e => { setFilterTunnel(e.target.value); setCurrentPage(1); }}>
|
||||||
Управление серверами
|
<option value="">Все туннели</option>
|
||||||
</h3>
|
{allTunnels.map(tunnel => (
|
||||||
</div>
|
<option key={tunnel} value={tunnel}>{tunnel}</option>
|
||||||
<div className="card-body">
|
))}
|
||||||
<div className="form-footer">
|
</select>
|
||||||
{/* The button for adding new server is now outside this column */}
|
<select className="form-select w-auto" value={filterCountry} onChange={e => { setFilterCountry(e.target.value); setCurrentPage(1); }}>
|
||||||
</div>
|
<option value="">Все страны</option>
|
||||||
</div>
|
{allCountries.map(country => (
|
||||||
</div>
|
<option key={country} value={country}>{country}</option>
|
||||||
|
))}
|
||||||
{/* Save Changes Card */}
|
</select>
|
||||||
<div className="card card-md mt-3">
|
<select className="form-select w-auto" value={filterProvider} onChange={e => { setFilterProvider(e.target.value); setCurrentPage(1); }}>
|
||||||
<div className="card-header">
|
<option value="">Все провайдеры</option>
|
||||||
<h3 className="card-title">
|
{allProviders.map(provider => (
|
||||||
<IconDatabase className="icon me-2" />
|
<option key={provider} value={provider}>{provider}</option>
|
||||||
Сохранение изменений
|
))}
|
||||||
</h3>
|
</select>
|
||||||
</div>
|
<div className="input-icon flex-grow-1">
|
||||||
<div className="card-body">
|
<span className="input-icon-addon">
|
||||||
<p className="text-muted mb-3">
|
<IconSearch size={18} />
|
||||||
Все изменения сохраняются в файл servers.json в S3 хранилище.
|
</span>
|
||||||
</p>
|
<input
|
||||||
<div className="form-footer">
|
type="text"
|
||||||
{/* The save changes buttons are now outside this column */}
|
className="form-control"
|
||||||
</div>
|
placeholder="Поиск серверов..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="table-responsive">
|
||||||
<div className="col-lg-8">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
<div className="card">
|
<thead>
|
||||||
<div className="card-header d-flex justify-content-between align-items-center">
|
<tr>
|
||||||
<h3 className="card-title mb-0">Список серверов</h3>
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('ip')}>
|
||||||
<div className="d-flex gap-2 w-75">
|
IP адрес
|
||||||
{/* Фильтры */}
|
{sortField === 'ip' && (
|
||||||
<select className="form-select w-auto" value={filterTunnel} onChange={e => { setFilterTunnel(e.target.value); setCurrentPage(1); }}>
|
<span className="ms-1">
|
||||||
<option value="">Все туннели</option>
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
{allTunnels.map(tunnel => (
|
</span>
|
||||||
<option key={tunnel} value={tunnel}>{tunnel}</option>
|
)}
|
||||||
))}
|
</th>
|
||||||
</select>
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('dns')}>
|
||||||
<select className="form-select w-auto" value={filterCountry} onChange={e => { setFilterCountry(e.target.value); setCurrentPage(1); }}>
|
DNS имя
|
||||||
<option value="">Все страны</option>
|
{sortField === 'dns' && (
|
||||||
{allCountries.map(country => (
|
<span className="ms-1">
|
||||||
<option key={country} value={country}>{country}</option>
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
))}
|
</span>
|
||||||
</select>
|
)}
|
||||||
<select className="form-select w-auto" value={filterProvider} onChange={e => { setFilterProvider(e.target.value); setCurrentPage(1); }}>
|
</th>
|
||||||
<option value="">Все провайдеры</option>
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('country')}>
|
||||||
{allProviders.map(provider => (
|
Страна
|
||||||
<option key={provider} value={provider}>{provider}</option>
|
{sortField === 'country' && (
|
||||||
))}
|
<span className="ms-1">
|
||||||
</select>
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
{/* Поиск */}
|
</span>
|
||||||
<div className="input-icon flex-grow-1">
|
)}
|
||||||
<span className="input-icon-addon">
|
</th>
|
||||||
<IconSearch size={18} />
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('provider')}>
|
||||||
</span>
|
Провайдер
|
||||||
<input
|
{sortField === 'provider' && (
|
||||||
type="text"
|
<span className="ms-1">
|
||||||
className="form-control"
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
placeholder="Поиск серверов..."
|
</span>
|
||||||
value={searchTerm}
|
)}
|
||||||
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
</th>
|
||||||
/>
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('tunnel')}>
|
||||||
</div>
|
Туннель
|
||||||
</div>
|
{sortField === 'tunnel' && (
|
||||||
</div>
|
<span className="ms-1">
|
||||||
<div className="table-responsive">
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
</span>
|
||||||
<thead>
|
)}
|
||||||
<tr>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('ip')}>
|
<th style={{cursor:'pointer'}} onClick={() => handleSort('gateway')}>
|
||||||
IP адрес
|
Основной шлюз
|
||||||
{sortField === 'ip' && (
|
{sortField === 'gateway' && (
|
||||||
<span className="ms-1">
|
<span className="ms-1">
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</th>
|
</th>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('dns')}>
|
<th className="text-center">Статус</th>
|
||||||
DNS имя
|
<th className="text-end">Ссылка</th>
|
||||||
{sortField === 'dns' && (
|
<th className="text-end">Действия</th>
|
||||||
<span className="ms-1">
|
</tr>
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
</thead>
|
||||||
</span>
|
<tbody>
|
||||||
)}
|
{paginatedServers.map((server) => (
|
||||||
</th>
|
<tr key={server.ip} className={editingServer === server.ip ? 'table-info' : ''}>
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('country')}>
|
<td>{server.ip}</td>
|
||||||
Страна
|
<td>{server.dns}</td>
|
||||||
{sortField === 'country' && (
|
<td><span className="badge bg-blue-lt text-blue">{countryToFlag(server.country)} {server.country}</span></td>
|
||||||
<span className="ms-1">
|
<td>{server.provider}</td>
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
<td><span className="badge bg-green-lt text-green">{server.tunnel}</span></td>
|
||||||
</span>
|
<td className="text-center">
|
||||||
)}
|
<span className="badge bg-blue-lt text-blue">
|
||||||
</th>
|
{server.gateway}
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('provider')}>
|
</span>
|
||||||
Провайдер
|
</td>
|
||||||
{sortField === 'provider' && (
|
<td className="text-center">
|
||||||
<span className="ms-1">
|
<span className="badge bg-success-lt text-success">Онлайн</span>
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
</td>
|
||||||
</span>
|
<td className="text-end">
|
||||||
)}
|
<button
|
||||||
</th>
|
className="btn btn-outline-info btn-icon"
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('tunnel')}>
|
onClick={() => handleGenerateLink(server)}
|
||||||
Туннель
|
onContextMenu={(e) => {
|
||||||
{sortField === 'tunnel' && (
|
e.preventDefault();
|
||||||
<span className="ms-1">
|
quickCopyLink(server);
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
}}
|
||||||
</span>
|
title="Левый клик - настройки, правый клик - быстрое копирование"
|
||||||
)}
|
>
|
||||||
</th>
|
<IconLink size={18} />
|
||||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('gateway')}>
|
|
||||||
Основной шлюз
|
|
||||||
{sortField === 'gateway' && (
|
|
||||||
<span className="ms-1">
|
|
||||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</th>
|
|
||||||
<th className="text-center">Статус</th>
|
|
||||||
<th className="text-end">Ссылка</th>
|
|
||||||
<th className="text-end">Действия</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{paginatedServers.map((server) => (
|
|
||||||
<tr key={server.ip} className={editingServer === server.ip ? 'table-info' : ''}>
|
|
||||||
<td>{server.ip}</td>
|
|
||||||
<td>{server.dns}</td>
|
|
||||||
<td><span className="badge bg-blue-lt text-blue">{countryToFlag(server.country)} {server.country}</span></td>
|
|
||||||
<td>{server.provider}</td>
|
|
||||||
<td><span className="badge bg-green-lt text-green">{server.tunnel}</span></td>
|
|
||||||
<td className="text-center">
|
|
||||||
<span className="badge bg-blue-lt text-blue">
|
|
||||||
{server.gateway}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<span className="badge bg-success-lt text-success">Онлайн</span>
|
|
||||||
</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<button
|
|
||||||
className="btn btn-outline-info btn-icon"
|
|
||||||
onClick={() => handleGenerateLink(server)}
|
|
||||||
onContextMenu={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
quickCopyLink(server);
|
|
||||||
}}
|
|
||||||
title="Левый клик - настройки, правый клик - быстрое копирование"
|
|
||||||
>
|
|
||||||
<IconLink size={18} />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td className="text-end">
|
|
||||||
<button className="btn btn-outline-primary btn-icon me-1" onClick={() => handleEdit(server)}><IconEdit size={18} /></button>
|
|
||||||
<button className="btn btn-outline-danger btn-icon" onClick={() => confirmDelete(server)}><IconTrash size={18} /></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{/* Пагинация */}
|
|
||||||
{totalPages > 1 && (
|
|
||||||
<div className="card-footer d-flex align-items-center">
|
|
||||||
<p className="m-0 text-muted">
|
|
||||||
Показано <span>{(currentPage - 1) * pageSize + 1}</span> до <span>{Math.min(currentPage * pageSize, filteredServers.length)}</span> из <span>{filteredServers.length}</span> серверов
|
|
||||||
</p>
|
|
||||||
<ul className="pagination m-0 ms-auto">
|
|
||||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
|
|
||||||
<IconChevronDown className="icon icon-sm rotate-180" />
|
|
||||||
Назад
|
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</td>
|
||||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
<td className="text-end">
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>
|
<button className="btn btn-outline-primary btn-icon me-1" onClick={() => handleEdit(server)}><IconEdit size={18} /></button>
|
||||||
Вперед
|
<button className="btn btn-outline-danger btn-icon" onClick={() => confirmDelete(server)}><IconTrash size={18} /></button>
|
||||||
<IconChevronDown className="icon icon-sm" />
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
</li>
|
))}
|
||||||
</ul>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{/* Пагинация */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="card-footer d-flex align-items-center">
|
||||||
|
<p className="m-0 text-muted">
|
||||||
|
Показано <span>{(currentPage - 1) * pageSize + 1}</span> до <span>{Math.min(currentPage * pageSize, filteredServers.length)}</span> из <span>{filteredServers.length}</span> серверов
|
||||||
|
</p>
|
||||||
|
<ul className="pagination m-0 ms-auto">
|
||||||
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
|
||||||
|
<IconChevronDown className="icon icon-sm rotate-180" />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>
|
||||||
|
Вперед
|
||||||
|
<IconChevronDown className="icon icon-sm" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Delete Modal */}
|
{/* Delete Modal */}
|
||||||
|
|||||||
Reference in New Issue
Block a user