diff --git a/EASY_SWITCH.md b/EASY_SWITCH.md index 62b61c1..2ed0de1 100644 --- a/EASY_SWITCH.md +++ b/EASY_SWITCH.md @@ -2,20 +2,23 @@ ## Описание -Easy Switch - это новая страница для быстрого переключения шлюзов (gateways) на основе communities из фильтров. Интерфейс вдохновлен мобильными VPN приложениями и обеспечивает удобный способ управления маршрутизацией. +Easy Switch - это новая страница для быстрого переключения шлюзов (gateways) внутри серверов на основе communities из фильтров. Интерфейс вдохновлен мобильными VPN приложениями и обеспечивает удобный способ управления маршрутизацией. ## Особенности ### 1. **Визуальный интерфейс** -- Карточки серверов с флагами стран +- Компактные карточки шлюзов (уменьшенный размер) +- Карточки с флагами стран - Отображение пинга и протокола подключения -- Метка "Fastest" для самого быстрого сервера в группе -- Активный сервер выделен синим цветом +- Метка "Fast" для самого быстрого шлюза в группе +- Активный шлюз выделен синим цветом -### 2. **Группировка по Communities** -- Все шлюзы сгруппированы по BGP communities -- Для каждого community можно выбрать активный шлюз -- Отображается количество доступных шлюзов +### 2. **Группировка по Серверам** +- Сервера группируются в отдельные блоки +- Каждый сервер показывает свои communities +- Для каждого community на сервере можно выбрать активный шлюз **этого сервера** +- Переключение происходит между шлюзами **внутри одного сервера** +- Отображается количество communities на каждом сервере ### 3. **Поиск и фильтрация** - Поиск по community, описанию или имени шлюза @@ -53,6 +56,42 @@ Easy Switch - это новая страница для быстрого пер } ``` +### Структура данных в приложении + +После загрузки данные организованы следующим образом: + +```javascript +servers = [ + { + id: "server-001", + name: "SWE-HIPHOST", + description: "Шведский сервер", + communities: [ + { + community: "65001:100", + description: "Стриминг сервисы", + gateways: [ + { name: "gateway1" }, + { name: "gateway2" }, + { name: "gateway3" } + ] + }, + // ... другие communities + ] + }, + // ... другие серверы +] +``` + +Активные шлюзы хранятся в формате: +```javascript +activeGateways = { + "server-001:65001:100": "gateway1", + "server-002:65001:100": "gateway4", + // ключ: "{serverId}:{community}", значение: "{gatewayName}" +} +``` + ## Интерфейс ### Заголовок @@ -69,20 +108,30 @@ Easy Switch - это новая страница для быстрого пер - Поле поиска - поиск по ключевым словам - Кнопки инструментов (настройки, быстрые действия) -### Карточки серверов +### Блоки серверов -Каждая карточка содержит: -- 🏴 **Флаг страны** (если доступен) -- ⚡ **Метка Fastest** (для самого быстрого сервера) -- **Название** шлюза или страны -- **Протокол** подключения (vless/xudp и т.д.) +Каждый сервер отображается в отдельной карточке с: +- 🏴 **Флаг страны** сервера +- 🖥️ **Иконка сервера** +- **Название** сервера +- **Описание** сервера +- **Количество communities** на этом сервере + +### Карточки шлюзов + +Каждая карточка шлюза (компактная) содержит: +- ⚡ **Метка Fast** (для первого шлюза в списке) +- **Название** шлюза +- **Протокол** подключения (vless и т.д.) - **Пинг** в миллисекундах -- ✓ **Галочка** для активного сервера +- ✓ **Галочка** для активного шлюза -### Состояния карточки +Карточки имеют меньший размер (col-6 col-sm-4 col-md-3 col-lg-2) для компактности. + +### Состояния карточки шлюза - **Неактивная** - белый фон, серая рамка - **Активная** - синий фон, белый текст, тень -- **Hover** - подъем карточки, легкая тень +- **Hover** - подъем карточки на 1px, легкая тень ## Использование @@ -92,16 +141,20 @@ Easy Switch - это новая страница для быстрого пер URL: `/easy-switch` ### Переключение шлюза -1. Найдите нужный community в списке -2. Нажмите на карточку желаемого шлюза -3. Карточка станет синей (активной) -4. Нажмите "Применить изменения" для сохранения +1. Найдите нужный **сервер** в списке +2. В этом сервере найдите нужный **community** +3. Нажмите на карточку желаемого **шлюза этого сервера** +4. Карточка станет синей (активной) +5. Нажмите "Применить изменения" для сохранения + +**Важно:** Переключение происходит между шлюзами **внутри одного сервера**. Каждый сервер имеет свой набор шлюзов для каждого community. ### Поиск Введите в поле поиска: +- Название сервера (например, `SWE-HIPHOST`) - Номер community (например, `65001:100`) - Описание (например, `Стриминг`) -- Название шлюза (например, `SWE-HIPHOST`) +- Название шлюза (например, `gateway1`) ## Технические детали @@ -124,12 +177,13 @@ GET /api/servers // Метаданные серверов ```javascript - loading: boolean // Загрузка данных - error: string // Текст ошибки -- servers: [] // Список серверов -- communities: [] // Группы communities -- activeGateways: {} // Активные шлюзы +- servers: [] // Список серверов с communities и gateways +- inventoryServers: [] // Метаданные серверов (флаги, провайдеры) +- activeGateways: {} // Активные шлюзы (ключ: "serverId:community") - hasChanges: boolean // Наличие изменений - searchQuery: string // Поисковый запрос -- activeTab: string // Активная вкладка +- selectedRule: string // Выбранное правило фильтрации +- activeTab: string // Активная вкладка (proxies/providers) ``` ## Roadmap diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index 4007e93..fb575dc 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -3,7 +3,6 @@ import api from './lib/api.js'; import { IconSearch, IconRefresh, - IconChevronDown, IconCheck, IconWorld, IconServer, @@ -15,8 +14,6 @@ import { IconBolt } from '@tabler/icons-react'; -const API_URL = '/api'; - // Функция для получения флага по коду страны const countryToFlag = (code) => { if (!code || typeof code !== 'string') return ''; @@ -26,20 +23,6 @@ const countryToFlag = (code) => { return String.fromCodePoint(...codePoints); }; -// Парсинг community value для отображения -const parseCommunity = (value) => { - if (!value) return { asn: '', local: '', display: '' }; - const parts = String(value).split(':'); - if (parts.length === 2) { - return { - asn: parts[0], - local: parts[1], - display: value - }; - } - return { asn: '', local: '', display: value }; -}; - function EasySwitchManager() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); @@ -47,8 +30,6 @@ function EasySwitchManager() { // Серверы и фильтры const [servers, setServers] = useState([]); const [inventoryServers, setInventoryServers] = useState([]); - const [allFilters, setAllFilters] = useState([]); // Все фильтры со всех серверов - const [communities, setCommunities] = useState([]); // Список communities для отображения // Поиск и фильтры const [searchQuery, setSearchQuery] = useState(''); @@ -81,61 +62,63 @@ function EasySwitchManager() { // Загружаем список серверов из server-configs const serversResponse = await api.get(`/server-configs`); const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : []; - setServers(serversList); // Загружаем фильтры для каждого сервера - const filtersPromises = serversList.map(async (server) => { - try { - const response = await api.get(`/server-filters/${server.id}`); - const filters = Array.isArray(response.data) ? response.data : []; - return filters.map(f => ({ - ...f, - serverId: server.id, - serverName: server.name - })); - } catch (e) { - console.error(`Error loading filters for ${server.id}:`, e); - return []; - } - }); + const serversWithFilters = await Promise.all( + serversList.map(async (server) => { + try { + const response = await api.get(`/server-filters/${server.id}`); + const filters = Array.isArray(response.data) ? response.data : []; + + // Группируем фильтры по community внутри сервера + const communitiesMap = new Map(); + filters.forEach(filter => { + const comm = filter.community; + if (!communitiesMap.has(comm)) { + communitiesMap.set(comm, { + community: comm, + description: filter.description || '', + gateways: [] + }); + } + + const communityData = communitiesMap.get(comm); + // Добавляем gateway, если его еще нет + if (!communityData.gateways.find(g => g.name === filter.gateway)) { + communityData.gateways.push({ + name: filter.gateway, + description: filter.description + }); + } + }); + + return { + ...server, + communities: Array.from(communitiesMap.values()) + }; + } catch (e) { + console.error(`Error loading filters for ${server.id}:`, e); + return { + ...server, + communities: [] + }; + } + }) + ); - const filtersArrays = await Promise.all(filtersPromises); - const combinedFilters = filtersArrays.flat(); - setAllFilters(combinedFilters); + // Фильтруем серверы, у которых есть communities + const serversWithCommunities = serversWithFilters.filter(s => s.communities.length > 0); + setServers(serversWithCommunities); - // Группируем по community - const communityMap = new Map(); - combinedFilters.forEach(filter => { - const comm = filter.community; - if (!communityMap.has(comm)) { - communityMap.set(comm, { - community: comm, - description: filter.description || '', - gateways: [] - }); - } - - const communityData = communityMap.get(comm); - // Добавляем gateway, если его еще нет - if (!communityData.gateways.find(g => g.gateway === filter.gateway && g.serverId === filter.serverId)) { - communityData.gateways.push({ - gateway: filter.gateway, - serverId: filter.serverId, - serverName: filter.serverName, - description: filter.description - }); - } - }); - - const communitiesArray = Array.from(communityMap.values()); - setCommunities(communitiesArray); - - // Устанавливаем активные шлюзы (первый для каждого community) + // Устанавливаем активные шлюзы (первый gateway для каждого community на каждом сервере) const initialActive = {}; - communitiesArray.forEach(comm => { - if (comm.gateways.length > 0) { - initialActive[comm.community] = comm.gateways[0].gateway; - } + serversWithCommunities.forEach(server => { + server.communities.forEach(comm => { + if (comm.gateways.length > 0) { + const key = `${server.id}:${comm.community}`; + initialActive[key] = comm.gateways[0].name; + } + }); }); setActiveGateways(initialActive); @@ -147,10 +130,11 @@ function EasySwitchManager() { } }; - const handleGatewaySelect = (community, gateway) => { + const handleGatewaySelect = (serverId, community, gateway) => { + const key = `${serverId}:${community}`; setActiveGateways(prev => ({ ...prev, - [community]: gateway + [key]: gateway })); setHasChanges(true); }; @@ -176,23 +160,29 @@ function EasySwitchManager() { ); }; - // Фильтрация communities по поиску - const filteredCommunities = communities.filter(comm => { + // Фильтрация серверов по поиску + const filteredServers = servers.filter(server => { if (!searchQuery.trim()) return true; const search = searchQuery.toLowerCase(); - const commValue = (comm.community || '').toLowerCase(); - const desc = (comm.description || '').toLowerCase(); + const serverName = (server.name || '').toLowerCase(); + const serverDesc = (server.description || '').toLowerCase(); - return commValue.includes(search) || - desc.includes(search) || - comm.gateways.some(g => - (g.gateway || '').toLowerCase().includes(search) || - (g.serverName || '').toLowerCase().includes(search) - ); + // Поиск по имени/описанию сервера + if (serverName.includes(search) || serverDesc.includes(search)) return true; + + // Поиск по communities и gateways + return server.communities.some(comm => { + const commValue = (comm.community || '').toLowerCase(); + const commDesc = (comm.description || '').toLowerCase(); + + return commValue.includes(search) || + commDesc.includes(search) || + comm.gateways.some(g => (g.name || '').toLowerCase().includes(search)); + }); }); - if (loading && communities.length === 0) { + if (loading && servers.length === 0) { return (
- {searchQuery ? 'Попробуйте изменить критерии поиска' : 'Нет доступных communities'} + {searchQuery ? 'Попробуйте изменить критерии поиска' : 'Нет доступных серверов'}