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>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-4">
|
||||
{/* Add New Server Card */}
|
||||
<div className="card card-md">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconPlus className="icon me-2" />
|
||||
Управление серверами
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="form-footer">
|
||||
{/* The button for adding new server is now outside this column */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Changes Card */}
|
||||
<div className="card card-md mt-3">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
<IconDatabase className="icon me-2" />
|
||||
Сохранение изменений
|
||||
</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-muted mb-3">
|
||||
Все изменения сохраняются в файл servers.json в S3 хранилище.
|
||||
</p>
|
||||
<div className="form-footer">
|
||||
{/* The save changes buttons are now outside this column */}
|
||||
</div>
|
||||
{/* Таблица серверов на всю ширину */}
|
||||
<div className="card w-100">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 className="card-title mb-0">Список серверов</h3>
|
||||
<div className="d-flex gap-2 w-75">
|
||||
{/* Фильтры и поиск */}
|
||||
<select className="form-select w-auto" value={filterTunnel} onChange={e => { setFilterTunnel(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все туннели</option>
|
||||
{allTunnels.map(tunnel => (
|
||||
<option key={tunnel} value={tunnel}>{tunnel}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="form-select w-auto" value={filterCountry} onChange={e => { setFilterCountry(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все страны</option>
|
||||
{allCountries.map(country => (
|
||||
<option key={country} value={country}>{country}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="form-select w-auto" value={filterProvider} onChange={e => { setFilterProvider(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все провайдеры</option>
|
||||
{allProviders.map(provider => (
|
||||
<option key={provider} value={provider}>{provider}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="input-icon flex-grow-1">
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={18} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск серверов..."
|
||||
value={searchTerm}
|
||||
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-lg-8">
|
||||
<div className="card">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<h3 className="card-title mb-0">Список серверов</h3>
|
||||
<div className="d-flex gap-2 w-75">
|
||||
{/* Фильтры */}
|
||||
<select className="form-select w-auto" value={filterTunnel} onChange={e => { setFilterTunnel(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все туннели</option>
|
||||
{allTunnels.map(tunnel => (
|
||||
<option key={tunnel} value={tunnel}>{tunnel}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="form-select w-auto" value={filterCountry} onChange={e => { setFilterCountry(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все страны</option>
|
||||
{allCountries.map(country => (
|
||||
<option key={country} value={country}>{country}</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="form-select w-auto" value={filterProvider} onChange={e => { setFilterProvider(e.target.value); setCurrentPage(1); }}>
|
||||
<option value="">Все провайдеры</option>
|
||||
{allProviders.map(provider => (
|
||||
<option key={provider} value={provider}>{provider}</option>
|
||||
))}
|
||||
</select>
|
||||
{/* Поиск */}
|
||||
<div className="input-icon flex-grow-1">
|
||||
<span className="input-icon-addon">
|
||||
<IconSearch size={18} />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Поиск серверов..."
|
||||
value={searchTerm}
|
||||
onChange={e => { setSearchTerm(e.target.value); setCurrentPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('ip')}>
|
||||
IP адрес
|
||||
{sortField === 'ip' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('dns')}>
|
||||
DNS имя
|
||||
{sortField === 'dns' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('country')}>
|
||||
Страна
|
||||
{sortField === 'country' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('provider')}>
|
||||
Провайдер
|
||||
{sortField === 'provider' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('tunnel')}>
|
||||
Туннель
|
||||
{sortField === 'tunnel' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<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" />
|
||||
Назад
|
||||
<div className="table-responsive">
|
||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('ip')}>
|
||||
IP адрес
|
||||
{sortField === 'ip' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('dns')}>
|
||||
DNS имя
|
||||
{sortField === 'dns' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('country')}>
|
||||
Страна
|
||||
{sortField === 'country' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('provider')}>
|
||||
Провайдер
|
||||
{sortField === 'provider' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<th style={{cursor:'pointer'}} onClick={() => handleSort('tunnel')}>
|
||||
Туннель
|
||||
{sortField === 'tunnel' && (
|
||||
<span className="ms-1">
|
||||
{sortOrder === 'asc' ? '▲' : '▼'}
|
||||
</span>
|
||||
)}
|
||||
</th>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
{/* Delete Modal */}
|
||||
|
||||
Reference in New Issue
Block a user