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}
+
-
-
+
- {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
- -
-
-
- ))}
+ -
+
+
+ {(() => {
+ 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' ? (
+ - …
+ ) : (
+ -
+
+
+ )
+ ));
+ })()}
-
-
+
+
+ -
+
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 (
-
- );
- })}
-
-
+
+ -
+
+
+ -
+
+
+ {(() => {
+ 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' ? (
+ - …
+ ) : (
+ -
+
+
+ )
+ ));
+ })()}
+ -
+
+
+ -
+
+
+
)}
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}
+
-
-
+
- {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
- -
-
-
- ))}
+ -
+
+
+ {(() => {
+ 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' ? (
+ - …
+ ) : (
+ -
+
+
+ )
+ ));
+ })()}
-
-
+
+
+ -
+
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
}