feat(MikrotikBackupsManager): implement server selection UI with search, sorting, and pagination features
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m43s
This commit is contained in:
@@ -6,6 +6,7 @@ import TableSkeleton from './components/TableSkeleton.jsx';
|
||||
import EmptyState from './components/EmptyState.jsx';
|
||||
import Tooltip from './components/Tooltip.jsx';
|
||||
import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx';
|
||||
import DataTable from './components/DataTable.jsx';
|
||||
import {
|
||||
IconRefresh,
|
||||
IconAlertTriangle,
|
||||
@@ -58,6 +59,13 @@ function MikrotikBackupsManager() {
|
||||
const [diffResult, setDiffResult] = useState(null);
|
||||
const [diffLoading, setDiffLoading] = useState(false);
|
||||
|
||||
// UI-состояние для таблицы выбора серверов автобэкапов
|
||||
const [backupSearch, setBackupSearch] = useState('');
|
||||
const [backupSortField, setBackupSortField] = useState('ip');
|
||||
const [backupSortOrder, setBackupSortOrder] = useState('asc');
|
||||
const [backupPage, setBackupPage] = useState(1);
|
||||
const backupPageSize = 10;
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
fetchUiSettings();
|
||||
@@ -91,6 +99,74 @@ function MikrotikBackupsManager() {
|
||||
|
||||
const backupServerIdsList = useMemo(() => Array.from(backupServerIds), [backupServerIds]);
|
||||
|
||||
// Отфильтрованный и отсортированный список Jumphost-серверов для блока автобэкапов
|
||||
const filteredBackupServers = useMemo(() => {
|
||||
let list = [...jumphostInputServers];
|
||||
|
||||
if (backupSearch.trim()) {
|
||||
const q = backupSearch.trim().toLowerCase();
|
||||
list = list.filter((s) => {
|
||||
const fields = [
|
||||
s.id,
|
||||
s.ip,
|
||||
s.dns,
|
||||
s.extIp,
|
||||
s.internalIp,
|
||||
s.country,
|
||||
s.provider,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return fields.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
list.sort((a, b) => {
|
||||
const field = backupSortField || 'ip';
|
||||
let av = a[field] ?? '';
|
||||
let bv = b[field] ?? '';
|
||||
if (typeof av === 'string') av = av.toLowerCase();
|
||||
if (typeof bv === 'string') bv = bv.toLowerCase();
|
||||
if (av < bv) return backupSortOrder === 'asc' ? -1 : 1;
|
||||
if (av > bv) return backupSortOrder === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return list;
|
||||
}, [jumphostInputServers, backupSearch, backupSortField, backupSortOrder]);
|
||||
|
||||
const backupTotalPages = useMemo(
|
||||
() => Math.max(1, Math.ceil(filteredBackupServers.length / backupPageSize)),
|
||||
[filteredBackupServers.length],
|
||||
);
|
||||
|
||||
const paginatedBackupServers = useMemo(() => {
|
||||
const page = Math.min(Math.max(1, backupPage), backupTotalPages);
|
||||
const start = (page - 1) * backupPageSize;
|
||||
return filteredBackupServers.slice(start, start + backupPageSize);
|
||||
}, [filteredBackupServers, backupPage, backupTotalPages]);
|
||||
|
||||
const handleBackupSort = (field) => {
|
||||
setBackupSortField((prevField) => {
|
||||
if (prevField === field) {
|
||||
setBackupSortOrder((prevOrder) => (prevOrder === 'asc' ? 'desc' : 'asc'));
|
||||
return prevField;
|
||||
}
|
||||
setBackupSortOrder('asc');
|
||||
return field;
|
||||
});
|
||||
};
|
||||
|
||||
const handleBackupSelectAll = () => {
|
||||
const allIds = filteredBackupServers.map((s) => String(s.id));
|
||||
setBackupServerIds(new Set(allIds));
|
||||
};
|
||||
|
||||
const handleBackupDeselectAll = () => {
|
||||
setBackupServerIds(new Set());
|
||||
};
|
||||
|
||||
const fetchServers = async () => {
|
||||
try {
|
||||
setLoadingServers(true);
|
||||
@@ -342,21 +418,61 @@ function MikrotikBackupsManager() {
|
||||
|
||||
{/* Блок выбора серверов для автобэкапа */}
|
||||
<div className="card mb-4">
|
||||
<div className="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div className="card-title mb-0">Сервера для автоматических бэкапов</div>
|
||||
<div className="text-muted small">
|
||||
Отмеченные здесь Jumphost-серверы будут периодически бэкапиться планировщиком на backend.
|
||||
<div className="card-header">
|
||||
<div className="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||
<div>
|
||||
<div className="card-title mb-0">Сервера для автоматических бэкапов</div>
|
||||
<div className="text-muted small">
|
||||
Отмеченные здесь Jumphost-серверы будут периодически бэкапиться планировщиком на backend.
|
||||
</div>
|
||||
<div className="text-muted small mt-1">
|
||||
Выбрано для автобэкапов:{' '}
|
||||
<span className="fw-semibold">
|
||||
{backupServerIds.size} из {jumphostServers.length}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex flex-wrap align-items-center gap-2">
|
||||
<div className="input-icon">
|
||||
<span className="input-icon-addon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 4a6 6 0 1 0 4 10" />
|
||||
<path d="M21 21l-6 -6" />
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Поиск по IP, DNS, провайдеру..."
|
||||
value={backupSearch}
|
||||
onChange={(e) => {
|
||||
setBackupSearch(e.target.value);
|
||||
setBackupPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm d-inline-flex align-items-center"
|
||||
onClick={saveBackupServers}
|
||||
disabled={jumphostServers.length === 0}
|
||||
>
|
||||
Сохранить список
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary btn-sm d-inline-flex align-items-center"
|
||||
onClick={saveBackupServers}
|
||||
disabled={jumphostServers.length === 0}
|
||||
>
|
||||
Сохранить список
|
||||
</button>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{jumphostServers.length === 0 ? (
|
||||
@@ -364,47 +480,62 @@ function MikrotikBackupsManager() {
|
||||
Нет серверов типа <code>jumphost</code>. Добавьте их в разделе «Серверы».
|
||||
</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 40 }}></th>
|
||||
<th>Сервер</th>
|
||||
<th>IP / DNS</th>
|
||||
<th>Страна</th>
|
||||
<th>Провайдер</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jumphostServers.map((s) => {
|
||||
const id = s.id || s.dns || s.ip;
|
||||
const checked = id && backupServerIds.has(String(id));
|
||||
return (
|
||||
<tr key={id || `srv-${s.ip}-${s.dns}`}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
checked={!!checked}
|
||||
onChange={() => id && toggleBackupServer(String(id))}
|
||||
/>
|
||||
</td>
|
||||
<td>{s.dns || s.ip || id}</td>
|
||||
<td>
|
||||
<div className="small text-muted">
|
||||
{s.ip}
|
||||
{s.dns && s.ip ? ' · ' : ''}
|
||||
{s.dns}
|
||||
</div>
|
||||
</td>
|
||||
<td className="small">{s.country || '-'}</td>
|
||||
<td className="small">{s.provider || '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
key: 'ip',
|
||||
title: 'Сервер',
|
||||
sortable: true,
|
||||
render: (_value, item) => (
|
||||
<div>
|
||||
<div className="fw-medium">{item.dns || item.ip || item.id}</div>
|
||||
<div className="small text-muted">
|
||||
{item.ip}
|
||||
{item.dns && item.ip ? ' · ' : ''}
|
||||
{item.dns}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'country',
|
||||
title: 'Страна',
|
||||
sortable: true,
|
||||
render: (value) => <span className="small">{value || '-'}</span>,
|
||||
},
|
||||
{
|
||||
key: 'provider',
|
||||
title: 'Провайдер',
|
||||
sortable: true,
|
||||
render: (value) => <span className="small">{value || '-'}</span>,
|
||||
},
|
||||
]}
|
||||
items={paginatedBackupServers}
|
||||
itemKey="id"
|
||||
loading={loadingServers}
|
||||
sortField={backupSortField}
|
||||
sortOrder={backupSortOrder}
|
||||
onSort={handleBackupSort}
|
||||
selectedItems={backupServerIds}
|
||||
onSelectItem={(id) => id && toggleBackupServer(String(id))}
|
||||
onSelectAll={handleBackupSelectAll}
|
||||
onDeselectAll={handleBackupDeselectAll}
|
||||
pagination={{
|
||||
currentPage: backupPage,
|
||||
totalPages: backupTotalPages,
|
||||
totalItems: filteredBackupServers.length,
|
||||
pageSize: backupPageSize,
|
||||
onPageChange: setBackupPage,
|
||||
}}
|
||||
emptyState={{
|
||||
title: 'Серверы не найдены',
|
||||
description:
|
||||
backupSearch.trim().length > 0
|
||||
? 'По вашему запросу нет Jumphost-серверов. Измените фильтры поиска.'
|
||||
: 'Нет доступных Jumphost-серверов для автобэкапа.',
|
||||
}}
|
||||
skeletonRows={5}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user