diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index 6da9139..41cc460 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -40,6 +40,7 @@ import AccordionCard from './components/AccordionCard.jsx'; import SavedFilters from './components/SavedFilters.jsx'; import BulkActionsBar from './components/BulkActionsBar.jsx'; import Tooltip from './components/Tooltip.jsx'; +import Pagination from './components/Pagination.jsx'; import { getAsnName, getAsnNameSync } from './lib/asn.js'; const API_URL = '/api'; @@ -900,58 +901,13 @@ function ASNsNewManager() { )} {/* Пагинация */} - {totalPages > 1 && ( -
-
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.length} -
- -
-
- )} + diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 1316ca2..7f7872e 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -46,6 +46,7 @@ import SavedFilters from './components/SavedFilters.jsx'; import BulkActionsBar from './components/BulkActionsBar.jsx'; import FormField from './components/FormField.jsx'; import Tooltip from './components/Tooltip.jsx'; +import Pagination from './components/Pagination.jsx'; import { useToast } from './components/ToastContainer.jsx'; const API_URL = '/api'; @@ -954,87 +955,13 @@ function DomainsNewManager() { )} {/* Пагинация */} - {totalPages > 1 && ( -
-
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.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' ? ( -
  • - -
  • - ) : ( -
  • - -
  • - ) - )); - })()} -
  • - -
  • -
  • - -
  • -
-
-
- )} + diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index 75a33cc..9c93088 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -32,44 +32,10 @@ import { IconWorld, IconFileText } from '@tabler/icons-react'; +import { normalizeGateways, countryToFlag } from './utils/serverUtils.js'; const API_URL = '/api'; -// Нормализация шлюзов (аналог из ServerManager) -const normalizeGateways = (gateways = [], fallbackName = '') => { - if (!Array.isArray(gateways)) return []; - const cleaned = gateways - .map((g, idx) => ({ - id: g?.id || `${Date.now()}-${idx}`, - name: String(g?.name || '').trim(), - ip: String(g?.ip || '').trim(), - comment: String(g?.comment || '').trim(), - primary: Boolean(g?.primary), - })) - .filter(g => g.name || g.ip || g.comment); - if (cleaned.length === 0) { - return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }]; - } - let hasPrimary = false; - cleaned.forEach((g) => { - if (g.primary && !hasPrimary) { - hasPrimary = true; - } else { - g.primary = false; - } - }); - if (!hasPrimary) cleaned[0].primary = true; - return cleaned; -}; - -const countryToFlag = (code) => { - if (!code || typeof code !== 'string') return ''; - const cc = code.trim().toUpperCase(); - if (cc.length !== 2) return ''; - const codePoints = [...cc].map((c) => 0x1f1e6 - 65 + c.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -}; - // Компонент автокомплита для выбора Community function CommunityAutocomplete({ label, value, onChange, communities = [], required = false, placeholder = 'Введите или выберите community...', baseAS = '65001' }) { const [inputValue, setInputValue] = useState(value || ''); diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index 703aeab..294b65e 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -42,6 +42,7 @@ import AccordionCard from './components/AccordionCard.jsx'; import SavedFilters from './components/SavedFilters.jsx'; import BulkActionsBar from './components/BulkActionsBar.jsx'; import Tooltip from './components/Tooltip.jsx'; +import Pagination from './components/Pagination.jsx'; const API_URL = '/api'; @@ -1132,58 +1133,13 @@ function IPRangesManager() { )} {/* Пагинация */} - {totalPages > 1 && ( -
-
-
- Показано {((currentPage - 1) * pageSize) + 1} - {((currentPage - 1) * pageSize) + paginatedItems.length} из {totalItems || filtered.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' ? ( -
  • - ) : ( -
  • - -
  • - ) - )); - })()} -
  • - -
  • -
  • - -
  • -
-
-
- )} + diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 6b8bae0..c576e4a 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -25,49 +25,14 @@ import { IconWorld } from '@tabler/icons-react'; import GraphView from './GraphView'; +import { + makeGateway, + normalizeGateways, + needsGateways as NEEDS_GATEWAYS, + SERVER_TYPE_OPTIONS +} from './utils/serverUtils.js'; const API_URL = '/api'; -const SERVER_TYPE_OPTIONS = [ - { value: 'jumphost', label: 'Jumphost' }, - { value: 'exit', label: 'Выходная нода' }, -]; -const NEEDS_GATEWAYS = (type) => ['jumphost', 'exit'].includes(String(type || '').toLowerCase()); - -const makeGateway = (over = {}) => ({ - id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, - name: '', - ip: '', - comment: '', - primary: false, - ...over, -}); - -const normalizeGateways = (gateways = [], fallbackName = '') => { - if (!Array.isArray(gateways)) return []; - const cleaned = gateways - .map((g, idx) => ({ - id: g?.id || `${Date.now()}-${idx}`, - name: String(g?.name || '').trim(), - ip: String(g?.ip || '').trim(), - comment: String(g?.comment || '').trim(), - primary: Boolean(g?.primary), - })) - .filter(g => g.name || g.ip || g.comment); - if (cleaned.length === 0) { - return [{ id: `${Date.now()}-primary`, name: fallbackName || 'gateway', ip: '', comment: '', primary: true }]; - } - // ensure single primary - let hasPrimary = false; - cleaned.forEach((g, i) => { - if (g.primary && !hasPrimary) { - hasPrimary = true; - } else { - g.primary = false; - } - }); - if (!hasPrimary) cleaned[0].primary = true; - return cleaned; -}; function ServerManager() { const [servers, setServers] = useState([]); diff --git a/frontend/src/utils/serverUtils.js b/frontend/src/utils/serverUtils.js new file mode 100644 index 0000000..25de61f --- /dev/null +++ b/frontend/src/utils/serverUtils.js @@ -0,0 +1,131 @@ +/** + * Общие утилиты для работы с серверами и шлюзами + * Используется в ServerManager, FilterManager и других компонентах + */ + +/** + * Создаёт объект шлюза с дефолтными значениями + * @param {Object} overrides - Переопределяемые поля + * @returns {Object} Объект шлюза + */ +export const makeGateway = (overrides = {}) => ({ + id: `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`, + name: '', + ip: '', + comment: '', + primary: false, + ...overrides, +}); + +/** + * Нормализует массив шлюзов: очищает, удаляет пустые, гарантирует один primary + * @param {Array} gateways - Массив шлюзов + * @param {string} fallbackName - Имя по умолчанию если массив пуст + * @returns {Array} Нормализованный массив шлюзов + */ +export const normalizeGateways = (gateways = [], fallbackName = '') => { + if (!Array.isArray(gateways)) return []; + + const cleaned = gateways + .map((g, idx) => ({ + id: g?.id || `${Date.now()}-${idx}`, + name: String(g?.name || '').trim(), + ip: String(g?.ip || '').trim(), + comment: String(g?.comment || '').trim(), + primary: Boolean(g?.primary), + })) + .filter(g => g.name || g.ip || g.comment); + + if (cleaned.length === 0) { + return [{ + id: `${Date.now()}-primary`, + name: fallbackName || 'gateway', + ip: '', + comment: '', + primary: true + }]; + } + + // Гарантируем только один primary + let hasPrimary = false; + cleaned.forEach((g) => { + if (g.primary && !hasPrimary) { + hasPrimary = true; + } else { + g.primary = false; + } + }); + + if (!hasPrimary) { + cleaned[0].primary = true; + } + + return cleaned; +}; + +/** + * Конвертирует код страны в эмодзи флага + * @param {string} code - Двухбуквенный код страны (ISO 3166-1 alpha-2) + * @returns {string} Эмодзи флага или пустая строка + */ +export const countryToFlag = (code) => { + if (!code || typeof code !== 'string') return ''; + const cc = code.trim().toUpperCase(); + if (cc.length !== 2) return ''; + const codePoints = [...cc].map((c) => 0x1f1e6 - 65 + c.charCodeAt(0)); + return String.fromCodePoint(...codePoints); +}; + +/** + * Проверяет, требует ли тип сервера наличие шлюзов + * @param {string} type - Тип сервера + * @returns {boolean} + */ +export const needsGateways = (type) => { + return ['jumphost', 'exit'].includes(String(type || '').toLowerCase()); +}; + +/** + * Константы типов серверов + */ +export const SERVER_TYPE_OPTIONS = [ + { value: 'jumphost', label: 'Jumphost' }, + { value: 'exit', label: 'Выходная нода' }, +]; + +/** + * Типы туннелей + */ +export const TUNNEL_TYPES = ['GRE', 'IPSec', 'WireGuard', 'VXLAN', 'OpenVPN']; + +/** + * Генерирует уникальный ID + * @returns {string} + */ +export const generateId = () => { + return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +}; + +/** + * Нормализует community value (добавляет ASN если отсутствует) + * @param {string} value - Значение community + * @param {string} defaultASN - ASN по умолчанию + * @returns {string} + */ +export const normalizeCommunityValue = (value, defaultASN = '65001') => { + const trimmed = String(value || '').trim(); + if (!trimmed) return ''; + return trimmed.includes(':') ? trimmed : `${defaultASN}:${trimmed}`; +}; + +/** + * Извлекает короткую часть community (после двоеточия) + * @param {string} value - Полное значение community (например "65000:120") + * @returns {string} Короткая часть (например "120") + */ +export const getShortCommunityValue = (value) => { + const str = String(value || '').trim(); + if (!str.includes(':')) return str; + return str.split(':')[1] || str; +}; +