diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b66285c..6ee992b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,13 +11,12 @@ "@tabler/core": "^1.3.2", "@tabler/icons-react": "^3.34.0", "@tanstack/react-query": "^5.56.2", + "@tanstack/react-virtual": "^3.10.8", "@xyflow/react": "^12.3.4", "axios": "^1.10.0", - "html-to-image": "^1.11.11", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router-dom": "^6.30.1", - "react-sigma": "^1.2.35" + "react-router-dom": "^6.30.1" }, "devDependencies": { "@eslint/js": "^9.29.0", @@ -28,6 +27,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.2.0", + "typescript": "^5.7.2", "vite": "^7.0.0" }, "optionalDependencies": { @@ -660,6 +660,33 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", + "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", + "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -1689,12 +1716,6 @@ "node": ">= 0.4" } }, - "node_modules/html-to-image": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", - "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==", - "license": "MIT" - }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -2108,19 +2129,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-sigma": { - "version": "1.2.35", - "resolved": "https://registry.npmjs.org/react-sigma/-/react-sigma-1.2.35.tgz", - "integrity": "sha512-amjKjaQusefDp8dD9er3wE/lfubSQT5fJSRF4SsS0LjxRue4bpSapAQ1WpmSM6nQEgawbrKel+bQBmatl7MAIw==", - "license": "MIT", - "engines": { - "node": ">=8.0" - }, - "peerDependencies": { - "react": ">=15.3", - "react-dom": ">=15.3" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "dev": true, @@ -2216,6 +2224,20 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "dev": true, diff --git a/frontend/src/components/server/ServerCard.jsx b/frontend/src/components/server/ServerCard.jsx index 9c55cff..7139a33 100644 --- a/frontend/src/components/server/ServerCard.jsx +++ b/frontend/src/components/server/ServerCard.jsx @@ -1,6 +1,5 @@ import { IconServer, - IconWorld, IconEdit, IconTrash, IconLink, @@ -8,18 +7,27 @@ import { IconChevronDown, IconChevronUp, IconNetwork, - IconMapPin, IconCheck, - IconSquare, - IconSquareCheck + IconCircleCheck, + IconExternalLink, + IconRefresh } from '@tabler/icons-react'; import { useState } from 'react'; // Преобразование кода страны в emoji-флаг function countryToFlag(isoCode) { if (!isoCode) return ''; - return isoCode - .toUpperCase() + const codeMap = { + 'SWE': 'SE', + 'RUS': 'RU', + 'USA': 'US', + 'GER': 'DE', + 'FIN': 'FI', + 'NLD': 'NL' + }; + const code = codeMap[isoCode?.toUpperCase()] || isoCode?.toUpperCase(); + if (!code || code.length !== 2) return isoCode; + return code .replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt()) ); @@ -32,147 +40,279 @@ export default function ServerCard({ onEdit, onDelete, onGenerateLink, - onQuickCopy, - compact = false + onQuickCopy }) { const [expanded, setExpanded] = useState(false); const isExit = server.type === 'exit'; - const primaryGateway = server.gateways?.find(g => g.primary)?.name || server.gateway || '—'; + const primaryGateway = server.gateways?.find(g => g.primary)?.name || server.gateway || ''; + const connectionsCount = server.gateways?.length || 0; + const shortName = server.dns?.split('.')[0] || server.ip?.split('.').slice(0, 2).join('.'); return (
-
- {/* Верхняя строка: checkbox + IP + тип + actions */} -
-
- {/* Чекбокс выбора */} - + {/* Основная строка - всё в одну линию */} +
+ {/* Статус онлайн */} +
- {/* IP адрес */} -
- {server.ip} -
+ {/* Чекбокс выбора */} + - {/* Тип */} - - {isExit ? 'Exit' : 'Jumphost'} - - - {/* Страна */} - - {countryToFlag(server.country)} {server.country} - -
- - {/* Действия */} -
- - - - - -
+ {/* Количество соединений */} +
+ + {connectionsCount}
- {/* Основная информация */} -
-
- - {server.dns || '—'} -
-
- - {server.provider} -
-
- - {server.tunnel} -
-
- - {primaryGateway} -
+ {/* Флаг */} + + {countryToFlag(server.country)} + + + {/* Название */} + + {shortName} + + + {/* Тип */} + + {isExit ? 'EXIT' : 'JUMP'} + + + {/* Провайдер */} + + {server.provider} + + + {/* Туннель */} + + {server.tunnel} + + + {/* DNS */} +
+ + + {server.dns} +
- {/* Расширенная информация */} - {expanded && ( -
-
-
-
Внешний IP
- {server.extIp || '—'} -
-
-
Внутренний IP
- {server.internalIp || '—'} -
- {server.gateways?.length > 0 && ( -
-
Все шлюзы
-
- {server.gateways.map((gw, idx) => ( - - {gw.name}{gw.ip ? ` (${gw.ip})` : ''} - - ))} -
-
- )} -
-
+ {/* Gateway */} + {primaryGateway && ( + + + {primaryGateway} + )} + + {/* Действия */} +
+ + + + + +
+ + {/* Расширенная информация */} + {expanded && ( +
+
+
+
IP адрес
+ {server.ip} +
+
+
Внешний IP
+ {server.extIp || '—'} +
+
+
Внутренний IP
+ {server.internalIp || '—'} +
+
+
Страна
+ {countryToFlag(server.country)} {server.country} +
+ {server.gateways?.length > 0 && ( +
+
+ Шлюзы ({server.gateways.length}) +
+
+ {server.gateways.map((gw, idx) => ( + + {gw.primary && } + {gw.name} + {gw.ip && ({gw.ip})} + + ))} +
+
+ )} +
+
+ )}
); } diff --git a/frontend/src/components/server/ServerSidebar.jsx b/frontend/src/components/server/ServerSidebar.jsx index be59f75..edcc282 100644 --- a/frontend/src/components/server/ServerSidebar.jsx +++ b/frontend/src/components/server/ServerSidebar.jsx @@ -6,18 +6,26 @@ import { IconNetwork, IconChevronDown, IconChevronRight, - IconCheck, IconX, IconSearch, IconFilter, - IconRefresh + IconDatabase } from '@tabler/icons-react'; // Преобразование кода страны в emoji-флаг function countryToFlag(isoCode) { if (!isoCode) return ''; - return isoCode - .toUpperCase() + const codeMap = { + 'SWE': 'SE', + 'RUS': 'RU', + 'USA': 'US', + 'GER': 'DE', + 'FIN': 'FI', + 'NLD': 'NL' + }; + const code = codeMap[isoCode?.toUpperCase()] || isoCode?.toUpperCase(); + if (!code || code.length !== 2) return isoCode; + return code .replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt()) ); @@ -76,220 +84,340 @@ export default function ServerSidebar({ }; return ( -
- {/* Заголовок с кнопкой сброса */} -
+
+ {/* Заголовок */} +
- + Фильтры {activeFiltersCount > 0 && ( - {activeFiltersCount} + + {activeFiltersCount} + )}
{activeFiltersCount > 0 && ( )}
{/* Поиск */} -
+
- + onSearch(e.target.value)} + style={{ + background: 'rgba(0,0,0,0.02)', + border: '1px solid rgba(0,0,0,0.06)', + borderRadius: '8px', + fontSize: '0.85rem' + }} /> - {searchTerm && ( - onSearch('')} - style={{ right: 0 }} - > - - +
+
+ + {/* Скроллируемый контент */} +
+ {/* Секция: Страны */} +
+
toggleSection('country')} + style={{ + background: 'rgba(0,0,0,0.02)', + borderBottom: '1px solid rgba(0,0,0,0.04)' + }} + > +
+ + Страны +
+ {expandedSections.country ? : } +
+ {expandedSections.country && ( +
+ {Object.entries(stats.byCountry) + .sort((a, b) => b[1] - a[1]) + .map(([country, count]) => ( + + ))} +
+ )} +
+ + {/* Секция: Провайдеры */} +
+
toggleSection('provider')} + style={{ + background: 'rgba(0,0,0,0.02)', + borderBottom: '1px solid rgba(0,0,0,0.04)' + }} + > +
+ + Провайдеры +
+ {expandedSections.provider ? : } +
+ {expandedSections.provider && ( +
+ {Object.entries(stats.byProvider) + .sort((a, b) => b[1] - a[1]) + .map(([provider, count]) => ( + + ))} +
+ )} +
+ + {/* Секция: Типы */} +
+
toggleSection('type')} + style={{ + background: 'rgba(0,0,0,0.02)', + borderBottom: '1px solid rgba(0,0,0,0.04)' + }} + > +
+ + Тип сервера +
+ {expandedSections.type ? : } +
+ {expandedSections.type && ( +
+ {Object.entries(stats.byType) + .sort((a, b) => b[1] - a[1]) + .map(([type, count]) => ( + + ))} +
+ )} +
+ + {/* Секция: Туннели */} +
+
toggleSection('tunnel')} + style={{ + background: 'rgba(0,0,0,0.02)', + borderBottom: '1px solid rgba(0,0,0,0.04)' + }} + > +
+ + Туннель +
+ {expandedSections.tunnel ? : } +
+ {expandedSections.tunnel && ( +
+ {Object.entries(stats.byTunnel) + .sort((a, b) => b[1] - a[1]) + .map(([tunnel, count]) => ( + + ))} +
)}
- {/* Секция: Страны */} -
-
toggleSection('country')} - style={{ background: 'var(--tblr-bg-surface-secondary)' }} - > -
- - Страны -
- {expandedSections.country ? : } + {/* Статистика внизу */} +
+
+ + Статистика
- {expandedSections.country && ( -
- {Object.entries(stats.byCountry) - .sort((a, b) => b[1] - a[1]) - .map(([country, count]) => ( - - ))} -
- )} -
- - {/* Секция: Провайдеры */} -
-
toggleSection('provider')} - style={{ background: 'var(--tblr-bg-surface-secondary)' }} - > -
- - Провайдеры -
- {expandedSections.provider ? : } +
+ Всего серверов + {servers.length}
- {expandedSections.provider && ( -
- {Object.entries(stats.byProvider) - .sort((a, b) => b[1] - a[1]) - .map(([provider, count]) => ( - - ))} -
- )} -
- - {/* Секция: Типы серверов */} -
-
toggleSection('type')} - style={{ background: 'var(--tblr-bg-surface-secondary)' }} - > -
- - Тип сервера -
- {expandedSections.type ? : } +
+ Выходных нод + {stats.byType['exit'] || 0}
- {expandedSections.type && ( -
- {Object.entries(stats.byType) - .sort((a, b) => b[1] - a[1]) - .map(([type, count]) => ( - - ))} -
- )} -
- - {/* Секция: Туннели */} -
-
toggleSection('tunnel')} - style={{ background: 'var(--tblr-bg-surface-secondary)' }} - > -
- - Тип туннеля -
- {expandedSections.tunnel ? : } -
- {expandedSections.tunnel && ( -
- {Object.entries(stats.byTunnel) - .sort((a, b) => b[1] - a[1]) - .map(([tunnel, count]) => ( - - ))} -
- )} -
- - {/* Статистика */} -
-
-
- Всего серверов: - {servers.length} -
-
- Выходных нод: - {stats.byType['exit'] || 0} -
-
- Jumphosts: - {stats.byType['jumphost'] || 0} -
+
+ Jumphosts + {stats.byType['jumphost'] || 0}