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

This commit is contained in:
2025-07-11 08:46:03 +07:00
parent 9e4c6863b6
commit 85ed473cca
+150 -189
View File
@@ -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 */}