diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 9420ecf..380c149 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -29,6 +29,8 @@ import { IconCode, IconArrowsRightLeft, IconLock, + IconChevronDown, + IconChevronUp, } from '@tabler/icons-react'; /** @@ -118,6 +120,7 @@ function NetworkConfigManager() { const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table' const [ipRegistrySearch, setIpRegistrySearch] = useState(''); // Поиск в реестре IP + const [expandedCategories, setExpandedCategories] = useState(new Set(['home', 'jumphost', 'exit'])); // Развернутые категории аккордеонов // === Modals === const [gatewayModalOpen, setGatewayModalOpen] = useState(false); @@ -247,6 +250,45 @@ function NetworkConfigManager() { return server.dns || server.ip; }; + // === Определение категории сервера === + const getServerCategory = (serverId) => { + if (!serverId || serverId === '__unassigned__') return 'other'; + const server = getServerInfo(serverId); + if (!server) return 'other'; + const serverType = server.type?.toLowerCase(); + if (serverType === 'home') return 'home'; + if (serverType === 'jumphost') return 'jumphost'; + if (serverType === 'exit') return 'exit'; + return 'other'; + }; + + // === Получение названия категории === + const getCategoryLabel = (category) => { + switch (category) { + case 'home': + return 'Домашние (входные роутеры)'; + case 'jumphost': + return 'Jumphosts'; + case 'exit': + return 'Exit nodes'; + default: + return 'Прочие'; + } + }; + + // === Переключение аккордеона категории === + const toggleCategory = (category) => { + setExpandedCategories(prev => { + const newSet = new Set(prev); + if (newSet.has(category)) { + newSet.delete(category); + } else { + newSet.add(category); + } + return newSet; + }); + }; + // === Уникальные серверы для фильтра === const uniqueServersInConfig = useMemo(() => { const allServerIds = new Set([ @@ -376,6 +418,45 @@ function NetworkConfigManager() { return grouped; }, [filteredInterfaces]); + // === Группировка по категориям серверов === + const gatewaysByCategory = useMemo(() => { + const grouped = { + home: {}, + jumphost: {}, + exit: {}, + other: {} + }; + + Object.entries(gatewaysByServer).forEach(([serverId, gateways]) => { + const category = getServerCategory(serverId); + if (!grouped[category][serverId]) { + grouped[category][serverId] = []; + } + grouped[category][serverId] = gateways; + }); + + return grouped; + }, [gatewaysByServer, servers]); + + const interfacesByCategory = useMemo(() => { + const grouped = { + home: {}, + jumphost: {}, + exit: {}, + other: {} + }; + + Object.entries(interfacesByServer).forEach(([serverId, interfaces]) => { + const category = getServerCategory(serverId); + if (!grouped[category][serverId]) { + grouped[category][serverId] = []; + } + grouped[category][serverId] = interfaces; + }); + + return grouped; + }, [interfacesByServer, servers]); + // === Сброс фильтров === const resetFilters = () => { setSearchTerm(''); @@ -2041,6 +2122,64 @@ function NetworkConfigManager() { return t?.color || 'secondary'; }; + // === Рендер категории с аккордеоном === + const renderCategoryAccordion = (category, itemsByServer, type) => { + const categoryItems = Object.entries(itemsByServer); + if (categoryItems.length === 0) return null; + + const isExpanded = expandedCategories.has(category); + const totalItems = categoryItems.reduce((sum, [, items]) => sum + items.length, 0); + + return ( +