diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index b0eec48..6acd177 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -358,18 +358,41 @@ function ASNsNewManager() { {/* Пагинация */} {totalPages > 1 && ( -
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length} +
diff --git a/frontend/src/BillingManager.jsx b/frontend/src/BillingManager.jsx index 08e7219..b8f5eff 100644 --- a/frontend/src/BillingManager.jsx +++ b/frontend/src/BillingManager.jsx @@ -724,34 +724,39 @@ function BillingManager() {
Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredData.length)} из {filteredData.length}
-
- - {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { - const page = i + 1; - return ( - - ); - })} - -
+
)} diff --git a/frontend/src/DataManager.jsx b/frontend/src/DataManager.jsx index edec905..d233f9d 100644 --- a/frontend/src/DataManager.jsx +++ b/frontend/src/DataManager.jsx @@ -413,18 +413,41 @@ function DataManager({ entityName, entityKey, placeholder }) { {/* Пагинация */} {totalPages > 1 && ( -
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length} +
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index b76a286..3988aea 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -431,18 +431,41 @@ function DomainsNewManager() {
{/* Пагинация */} {totalPages > 1 && ( -
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length} +
diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 0d04f72..0716c11 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -8,7 +8,10 @@ import { IconCheck, IconX, IconDatabase, - IconRefresh + IconRefresh, + IconUpload, + IconDownload, + IconDeviceFloppy } from '@tabler/icons-react'; const API_URL = '/api'; @@ -16,6 +19,7 @@ const API_URL = '/api'; function IPRangesManager() { const [items, setItems] = useState([]); const [newItem, setNewItem] = useState({ ipRange: '', community: '' }); + const [newInvalid, setNewInvalid] = useState({ ipRange: false, community: false }); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [searchTerm, setSearchTerm] = useState(''); @@ -31,6 +35,25 @@ function IPRangesManager() { const editInputRef = useRef(null); 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(() => { fetchItems(); }, []); @@ -50,17 +73,17 @@ function IPRangesManager() { }; const handleAddItem = () => { - if (newItem.ipRange.trim() === '') { - setError('IP-диапазон не может быть пустым.'); - return; - } - if (newItem.community.trim() === '') { - setError('Community не может быть пустым.'); + const cidrOk = isValidCidr(newItem.ipRange); + const communityOk = isValidCommunity(newItem.community); + setNewInvalid({ ipRange: !cidrOk, community: !communityOk }); + if (!cidrOk || !communityOk) { + setError('Введите корректный IP-диапазон в формате CIDR (например 192.168.1.0/24) и числовой community.'); return; } setError(''); - setItems([...items, newItem]); + setItems([...items, { ipRange: newItem.ipRange.trim(), community: String(newItem.community).trim() }]); setNewItem({ ipRange: '', community: '' }); + setNewInvalid({ ipRange: false, community: false }); }; const handleEdit = (item) => { @@ -69,8 +92,12 @@ function IPRangesManager() { }; const handleSaveEdit = (ipRange) => { + if (!isValidCommunity(editingValue)) { + setError('Community должен быть числом.'); + return; + } const updatedItems = items.map(i => - i.ipRange === ipRange ? { ...i, community: editingValue } : i + i.ipRange === ipRange ? { ...i, community: String(editingValue).trim() } : i ); setItems(updatedItems); setEditingIp(null); @@ -100,7 +127,8 @@ function IPRangesManager() { const handleSaveChanges = async () => { setLoading(true); 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('Изменения успешно сохранены!'); setTimeout(() => setSuccess(''), 3000); } 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) => { let valA = a[sortField] || ''; @@ -207,21 +266,23 @@ function IPRangesManager() { setNewItem({ ...newItem, ipRange: e.target.value })} /> + {newInvalid.ipRange &&
Некорректный CIDR
}
setNewItem({ ...newItem, community: e.target.value })} /> + {newInvalid.community &&
Только цифры
}
+ + +
@@ -276,7 +359,7 @@ function IPRangesManager() {
-

Список IP-диапазонов

+

Список IP-диапазонов {items.length}

{/* Фильтр по community */} setEditingValue(e.target.value)} onKeyDown={e => handleEditKeyDown(e, item.ipRange)} style={{maxWidth: 120}} /> - + ) : ( @@ -357,18 +440,46 @@ function IPRangesManager() {
{/* Пагинация */} {totalPages > 1 && ( -
+
+
+ Показано {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, filteredItems.length)} из {filteredItems.length} +
  • - +
  • - {Array.from({length: totalPages}, (_, i) => i + 1).map(page => ( -
  • - -
  • - ))} +
  • + +
  • + {(() => { + 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 ( +
  • + ); + } + return ( +
  • + +
  • + ); + }; + return pages.map(renderItem); + })()}
  • - + +
  • +
  • +
diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index c8016aa..36e0be0 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -606,22 +606,41 @@ function ServerManager() {
{/* Пагинация */} {totalPages > 1 && ( -
-

- Показано {(currentPage - 1) * pageSize + 1} до {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length} серверов -

-
    +
    +
    + Показано {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, filteredServers.length)} из {filteredServers.length} +
    +
    • - + +
    • +
    • + +
    • + {(() => { + 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' ? ( +
    • + ) : ( +
    • + +
    • + ) + )); + })()} +
    • +
    • - +