feat: Refactor pagination components across multiple managers to improve UI consistency and enhance user experience with better navigation controls
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m1s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 6m1s
This commit is contained in:
@@ -358,18 +358,41 @@ function ASNsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Пагинация */}
|
{/* Пагинация */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="card-footer d-flex justify-content-center">
|
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||||
|
<div className="text-muted">
|
||||||
|
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length}
|
||||||
|
</div>
|
||||||
<ul className="pagination m-0">
|
<ul className="pagination m-0">
|
||||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>«</button>
|
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||||
</li>
|
</li>
|
||||||
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<li key={page} className={`page-item${currentPage === page ? ' active' : ''}`}>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(page)}>{page}</button>
|
</li>
|
||||||
</li>
|
{(() => {
|
||||||
))}
|
const pages = [];
|
||||||
|
let start = Math.max(1, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
|
if (currentPage <= 3) end = Math.min(totalPages, 5);
|
||||||
|
if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4);
|
||||||
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
|
return pages.map((p) => (
|
||||||
|
p === 'start-ellipsis' || p === 'end-ellipsis' ? (
|
||||||
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
|
) : (
|
||||||
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
));
|
||||||
|
})()}
|
||||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>»</button>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -724,34 +724,39 @@ function BillingManager() {
|
|||||||
<div className="text-muted">
|
<div className="text-muted">
|
||||||
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredData.length)} из {filteredData.length}
|
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredData.length)} из {filteredData.length}
|
||||||
</div>
|
</div>
|
||||||
<div className="btn-group">
|
<ul className="pagination m-0">
|
||||||
<button
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
className="btn btn-outline-secondary"
|
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||||
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
|
</li>
|
||||||
disabled={currentPage === 1}
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||||
Предыдущая
|
</li>
|
||||||
</button>
|
{(() => {
|
||||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
const pages = [];
|
||||||
const page = i + 1;
|
let start = Math.max(1, currentPage - 2);
|
||||||
return (
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
<button
|
if (currentPage <= 3) end = Math.min(totalPages, 5);
|
||||||
key={page}
|
if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4);
|
||||||
className={`btn ${currentPage === page ? 'btn-primary' : 'btn-outline-secondary'}`}
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
onClick={() => setCurrentPage(page)}
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
>
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
{page}
|
return pages.map((p) => (
|
||||||
</button>
|
p === 'start-ellipsis' || p === 'end-ellipsis' ? (
|
||||||
);
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
})}
|
) : (
|
||||||
<button
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
className="btn btn-outline-secondary"
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
|
</li>
|
||||||
disabled={currentPage === totalPages}
|
)
|
||||||
>
|
));
|
||||||
Следующая
|
})()}
|
||||||
</button>
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
</div>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -413,18 +413,41 @@ function DataManager({ entityName, entityKey, placeholder }) {
|
|||||||
</div>
|
</div>
|
||||||
{/* Пагинация */}
|
{/* Пагинация */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="card-footer d-flex justify-content-center">
|
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||||
|
<div className="text-muted">
|
||||||
|
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length}
|
||||||
|
</div>
|
||||||
<ul className="pagination m-0">
|
<ul className="pagination m-0">
|
||||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>«</button>
|
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||||
</li>
|
</li>
|
||||||
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<li key={page} className={`page-item${currentPage === page ? ' active' : ''}`}>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(page)}>{page}</button>
|
</li>
|
||||||
</li>
|
{(() => {
|
||||||
))}
|
const pages = [];
|
||||||
|
let start = Math.max(1, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
|
if (currentPage <= 3) end = Math.min(totalPages, 5);
|
||||||
|
if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4);
|
||||||
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
|
return pages.map((p) => (
|
||||||
|
p === 'start-ellipsis' || p === 'end-ellipsis' ? (
|
||||||
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
|
) : (
|
||||||
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
));
|
||||||
|
})()}
|
||||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>»</button>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -431,18 +431,41 @@ function DomainsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Пагинация */}
|
{/* Пагинация */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="card-footer d-flex justify-content-center">
|
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||||
|
<div className="text-muted">
|
||||||
|
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length}
|
||||||
|
</div>
|
||||||
<ul className="pagination m-0">
|
<ul className="pagination m-0">
|
||||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>«</button>
|
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||||
</li>
|
</li>
|
||||||
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<li key={page} className={`page-item${currentPage === page ? ' active' : ''}`}>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(page)}>{page}</button>
|
</li>
|
||||||
</li>
|
{(() => {
|
||||||
))}
|
const pages = [];
|
||||||
|
let start = Math.max(1, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
|
if (currentPage <= 3) end = Math.min(totalPages, 5);
|
||||||
|
if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4);
|
||||||
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
|
return pages.map((p) => (
|
||||||
|
p === 'start-ellipsis' || p === 'end-ellipsis' ? (
|
||||||
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
|
) : (
|
||||||
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
));
|
||||||
|
})()}
|
||||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>»</button>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import {
|
|||||||
IconCheck,
|
IconCheck,
|
||||||
IconX,
|
IconX,
|
||||||
IconDatabase,
|
IconDatabase,
|
||||||
IconRefresh
|
IconRefresh,
|
||||||
|
IconUpload,
|
||||||
|
IconDownload,
|
||||||
|
IconDeviceFloppy
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
@@ -16,6 +19,7 @@ const API_URL = '/api';
|
|||||||
function IPRangesManager() {
|
function IPRangesManager() {
|
||||||
const [items, setItems] = useState([]);
|
const [items, setItems] = useState([]);
|
||||||
const [newItem, setNewItem] = useState({ ipRange: '', community: '' });
|
const [newItem, setNewItem] = useState({ ipRange: '', community: '' });
|
||||||
|
const [newInvalid, setNewInvalid] = useState({ ipRange: false, community: false });
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [success, setSuccess] = useState('');
|
const [success, setSuccess] = useState('');
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -31,6 +35,25 @@ function IPRangesManager() {
|
|||||||
const editInputRef = useRef(null);
|
const editInputRef = useRef(null);
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
|
||||||
|
const isValidIPv4 = (ip) => {
|
||||||
|
const octets = ip.split('.');
|
||||||
|
if (octets.length !== 4) return false;
|
||||||
|
return octets.every(o => /^\d{1,3}$/.test(o) && Number(o) >= 0 && Number(o) <= 255);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidCidr = (value) => {
|
||||||
|
const v = String(value).trim();
|
||||||
|
const parts = v.split('/');
|
||||||
|
if (parts.length !== 2) return false;
|
||||||
|
const [ip, mask] = parts;
|
||||||
|
if (!isValidIPv4(ip)) return false;
|
||||||
|
if (!/^\d{1,2}$/.test(mask)) return false;
|
||||||
|
const m = Number(mask);
|
||||||
|
return m >= 0 && m <= 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidCommunity = (value) => /^[0-9]+$/.test(String(value).trim());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchItems();
|
fetchItems();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -50,17 +73,17 @@ function IPRangesManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
if (newItem.ipRange.trim() === '') {
|
const cidrOk = isValidCidr(newItem.ipRange);
|
||||||
setError('IP-диапазон не может быть пустым.');
|
const communityOk = isValidCommunity(newItem.community);
|
||||||
return;
|
setNewInvalid({ ipRange: !cidrOk, community: !communityOk });
|
||||||
}
|
if (!cidrOk || !communityOk) {
|
||||||
if (newItem.community.trim() === '') {
|
setError('Введите корректный IP-диапазон в формате CIDR (например 192.168.1.0/24) и числовой community.');
|
||||||
setError('Community не может быть пустым.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setError('');
|
setError('');
|
||||||
setItems([...items, newItem]);
|
setItems([...items, { ipRange: newItem.ipRange.trim(), community: String(newItem.community).trim() }]);
|
||||||
setNewItem({ ipRange: '', community: '' });
|
setNewItem({ ipRange: '', community: '' });
|
||||||
|
setNewInvalid({ ipRange: false, community: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (item) => {
|
const handleEdit = (item) => {
|
||||||
@@ -69,8 +92,12 @@ function IPRangesManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveEdit = (ipRange) => {
|
const handleSaveEdit = (ipRange) => {
|
||||||
|
if (!isValidCommunity(editingValue)) {
|
||||||
|
setError('Community должен быть числом.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const updatedItems = items.map(i =>
|
const updatedItems = items.map(i =>
|
||||||
i.ipRange === ipRange ? { ...i, community: editingValue } : i
|
i.ipRange === ipRange ? { ...i, community: String(editingValue).trim() } : i
|
||||||
);
|
);
|
||||||
setItems(updatedItems);
|
setItems(updatedItems);
|
||||||
setEditingIp(null);
|
setEditingIp(null);
|
||||||
@@ -100,7 +127,8 @@ function IPRangesManager() {
|
|||||||
const handleSaveChanges = async () => {
|
const handleSaveChanges = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await axios.post(`${API_URL}/ip-ranges`, { ipRanges: items });
|
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community));
|
||||||
|
await axios.post(`${API_URL}/ip-ranges`, { ipRanges: valid });
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -111,6 +139,37 @@ function IPRangesManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImport = () => {
|
||||||
|
const text = window.prompt('Вставьте строки: CIDR ПРОБЕЛ COMMUNITY (по одной записи на строку)');
|
||||||
|
if (!text) return;
|
||||||
|
const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
||||||
|
const parsed = lines.map(l => {
|
||||||
|
const [cidr, c] = l.split(/\s+/);
|
||||||
|
return { ipRange: cidr || '', community: c || '' };
|
||||||
|
});
|
||||||
|
setItems(prev => [...prev, ...parsed]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
const header = ['ipRange', 'community'];
|
||||||
|
const csv = [header, ...items.map(i => [i.ipRange, i.community])]
|
||||||
|
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `ip_ranges_${new Date().toISOString().split('T')[0]}.csv`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearInvalid = () => {
|
||||||
|
setItems(prev => prev.filter(i => i.ipRange || i.community)
|
||||||
|
.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Сортировка
|
// Сортировка
|
||||||
const sortedItems = [...items].sort((a, b) => {
|
const sortedItems = [...items].sort((a, b) => {
|
||||||
let valA = a[sortField] || '';
|
let valA = a[sortField] || '';
|
||||||
@@ -207,21 +266,23 @@ function IPRangesManager() {
|
|||||||
<label className="form-label">IP-диапазон</label>
|
<label className="form-label">IP-диапазон</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className={`form-control${newInvalid.ipRange ? ' is-invalid' : ''}`}
|
||||||
placeholder="192.168.1.0/24"
|
placeholder="192.168.1.0/24"
|
||||||
value={newItem.ipRange}
|
value={newItem.ipRange}
|
||||||
onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })}
|
onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })}
|
||||||
/>
|
/>
|
||||||
|
{newInvalid.ipRange && <div className="invalid-feedback">Некорректный CIDR</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Community</label>
|
<label className="form-label">Community</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||||
placeholder="65001:100"
|
placeholder="65001:100"
|
||||||
value={newItem.community}
|
value={newItem.community}
|
||||||
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
|
onChange={(e) => setNewItem({ ...newItem, community: e.target.value })}
|
||||||
/>
|
/>
|
||||||
|
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="form-footer">
|
<div className="form-footer">
|
||||||
<button type="submit" className="btn btn-primary w-100">
|
<button type="submit" className="btn btn-primary w-100">
|
||||||
@@ -255,7 +316,7 @@ function IPRangesManager() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IconDatabase className="icon me-2" />
|
<IconDeviceFloppy className="icon me-2" />
|
||||||
Сохранить в S3
|
Сохранить в S3
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -268,6 +329,28 @@ function IPRangesManager() {
|
|||||||
<IconRefresh className="icon me-2" />
|
<IconRefresh className="icon me-2" />
|
||||||
Обновить
|
Обновить
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handleImport}
|
||||||
|
>
|
||||||
|
<IconUpload className="icon me-2" />
|
||||||
|
Импорт из буфера
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={items.length === 0}
|
||||||
|
>
|
||||||
|
<IconDownload className="icon me-2" />
|
||||||
|
Экспорт CSV
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary"
|
||||||
|
onClick={clearInvalid}
|
||||||
|
disabled={items.length === 0}
|
||||||
|
>
|
||||||
|
Очистить пустые/невалидные
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,7 +359,7 @@ function IPRangesManager() {
|
|||||||
<div className="col-lg-9">
|
<div className="col-lg-9">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header d-flex justify-content-between align-items-center">
|
<div className="card-header d-flex justify-content-between align-items-center">
|
||||||
<h3 className="card-title mb-0">Список IP-диапазонов</h3>
|
<h3 className="card-title mb-0">Список IP-диапазонов <span className="badge bg-blue-lt text-blue ms-2">{items.length}</span></h3>
|
||||||
<div className="d-flex gap-2 w-50">
|
<div className="d-flex gap-2 w-50">
|
||||||
{/* Фильтр по community */}
|
{/* Фильтр по community */}
|
||||||
<select className="form-select w-auto" value={filterCommunity} onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }}>
|
<select className="form-select w-auto" value={filterCommunity} onChange={e => { setFilterCommunity(e.target.value); setCurrentPage(1); }}>
|
||||||
@@ -333,14 +416,14 @@ function IPRangesManager() {
|
|||||||
<>
|
<>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control d-inline-block w-auto me-2"
|
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||||
value={editingValue}
|
value={editingValue}
|
||||||
ref={editInputRef}
|
ref={editInputRef}
|
||||||
onChange={e => setEditingValue(e.target.value)}
|
onChange={e => setEditingValue(e.target.value)}
|
||||||
onKeyDown={e => handleEditKeyDown(e, item.ipRange)}
|
onKeyDown={e => handleEditKeyDown(e, item.ipRange)}
|
||||||
style={{maxWidth: 120}}
|
style={{maxWidth: 120}}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.ipRange)}><IconCheck size={18} /></button>
|
<button className="btn btn-success btn-icon me-1" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)}><IconCheck size={18} /></button>
|
||||||
<button className="btn btn-secondary btn-icon" onClick={handleCancelEdit}><IconX size={18} /></button>
|
<button className="btn btn-secondary btn-icon" onClick={handleCancelEdit}><IconX size={18} /></button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -357,18 +440,46 @@ function IPRangesManager() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Пагинация */}
|
{/* Пагинация */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="card-footer d-flex justify-content-center">
|
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||||
|
<div className="text-muted">
|
||||||
|
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length}
|
||||||
|
</div>
|
||||||
<ul className="pagination m-0">
|
<ul className="pagination m-0">
|
||||||
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>«</button>
|
<button className="page-link" onClick={() => setCurrentPage(1)} tabIndex={-1} disabled={currentPage === 1}>Первая</button>
|
||||||
</li>
|
</li>
|
||||||
{Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
|
<li className={`page-item${currentPage === 1 ? ' disabled' : ''}`}>
|
||||||
<li key={page} className={`page-item${currentPage === page ? ' active' : ''}`}>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} tabIndex={-1} disabled={currentPage === 1}>Назад</button>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(page)}>{page}</button>
|
</li>
|
||||||
</li>
|
{(() => {
|
||||||
))}
|
const pages = [];
|
||||||
|
const maxToShow = 7; // 1 .. 5 .. N
|
||||||
|
let start = Math.max(1, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
|
if (currentPage <= 3) { end = Math.min(totalPages, 5); }
|
||||||
|
if (currentPage >= totalPages - 2) { start = Math.max(1, totalPages - 4); }
|
||||||
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
|
const renderItem = (p) => {
|
||||||
|
if (p === 'start-ellipsis' || p === 'end-ellipsis') {
|
||||||
|
return (
|
||||||
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return pages.map(renderItem);
|
||||||
|
})()}
|
||||||
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>»</button>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} tabIndex={-1} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
|
</li>
|
||||||
|
<li className={`page-item${currentPage === totalPages ? ' disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} tabIndex={-1} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -606,22 +606,41 @@ function ServerManager() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Пагинация */}
|
{/* Пагинация */}
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
<div className="card-footer d-flex align-items-center">
|
<div className="card-footer d-flex align-items-center justify-content-between">
|
||||||
<p className="m-0 text-muted">
|
<div className="text-muted">
|
||||||
Показано <span>{(currentPage - 1) * pageSize + 1}</span> до <span>{Math.min(currentPage * pageSize, filteredServers.length)}</span> из <span>{filteredServers.length}</span> серверов
|
Показано {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length}
|
||||||
</p>
|
</div>
|
||||||
<ul className="pagination m-0 ms-auto">
|
<ul className="pagination m-0">
|
||||||
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>
|
<button className="page-link" onClick={() => setCurrentPage(1)} disabled={currentPage === 1}>Первая</button>
|
||||||
<IconChevronDown className="icon icon-sm rotate-180" />
|
</li>
|
||||||
Назад
|
<li className={`page-item ${currentPage === 1 ? 'disabled' : ''}`}>
|
||||||
</button>
|
<button className="page-link" onClick={() => setCurrentPage(currentPage - 1)} disabled={currentPage === 1}>Назад</button>
|
||||||
|
</li>
|
||||||
|
{(() => {
|
||||||
|
const pages = [];
|
||||||
|
let start = Math.max(1, currentPage - 2);
|
||||||
|
let end = Math.min(totalPages, currentPage + 2);
|
||||||
|
if (currentPage <= 3) end = Math.min(totalPages, 5);
|
||||||
|
if (currentPage >= totalPages - 2) start = Math.max(1, totalPages - 4);
|
||||||
|
if (start > 1) pages.push('start-ellipsis');
|
||||||
|
for (let p = start; p <= end; p++) pages.push(p);
|
||||||
|
if (end < totalPages) pages.push('end-ellipsis');
|
||||||
|
return pages.map((p) => (
|
||||||
|
p === 'start-ellipsis' || p === 'end-ellipsis' ? (
|
||||||
|
<li key={p} className="page-item disabled"><span className="page-link">…</span></li>
|
||||||
|
) : (
|
||||||
|
<li key={p} className={`page-item${currentPage === p ? ' active' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(p)}>{p}</button>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||||
|
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>Вперед</button>
|
||||||
</li>
|
</li>
|
||||||
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
<li className={`page-item ${currentPage === totalPages ? 'disabled' : ''}`}>
|
||||||
<button className="page-link" onClick={() => setCurrentPage(currentPage + 1)} disabled={currentPage === totalPages}>
|
<button className="page-link" onClick={() => setCurrentPage(totalPages)} disabled={currentPage === totalPages}>Последняя</button>
|
||||||
Вперед
|
|
||||||
<IconChevronDown className="icon icon-sm" />
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user