From d34ae9beb20fb5047520d13a4bbdc5d57ac1901e Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Tue, 13 Aug 2024 15:51:22 +0200 Subject: [PATCH] Sync changes with netbird cloud (#407) * Update axa oidc library and package.json * Update ACL port state to show correct value * Filter user groups by unique groups only * Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations * Add peer multiselect, optimize dropdown performance for peer selection, remove 'all' group from some dropdowns, various ui / ux optimizations --- package-lock.json | 528 +++++++++++++++--- package.json | 7 +- src/app/(dashboard)/peer/page.tsx | 1 + src/app/(dashboard)/peers/page.tsx | 31 +- src/app/(dashboard)/team/user/page.tsx | 26 +- src/app/globals.css | 5 + src/assets/os-icons/FreeBSD.png | Bin 0 -> 3197 bytes src/auth/OIDCProvider.tsx | 6 +- src/auth/SecureProvider.tsx | 10 +- src/components/DropdownInfoText.tsx | 11 + src/components/DropdownInput.tsx | 48 ++ src/components/NetworkRouteSelector.tsx | 17 +- src/components/PeerGroupSelector.tsx | 30 +- src/components/PeerSelector.tsx | 241 +++----- src/components/ScrollArea.tsx | 60 +- src/components/Slider.tsx | 27 + src/components/VirtualScrollAreaList.tsx | 132 +++++ src/components/table/DataTable.tsx | 33 +- .../table/DataTableHeadingPortal.tsx | 73 +++ src/components/ui/GroupBadge.tsx | 5 +- src/contexts/PeersProvider.tsx | 18 +- src/hooks/useOperatingSystem.ts | 12 + src/hooks/usePortalElement.tsx | 12 + src/hooks/usePrevious.ts | 13 + src/hooks/useSearch.ts | 91 +++ src/interfaces/OperatingSystem.ts | 1 + .../access-control/AccessControlModal.tsx | 12 +- .../table/AccessControlPortsCell.tsx | 10 +- src/modules/common-table-rows/GroupsRow.tsx | 6 + src/modules/groups/GroupSelector.tsx | 2 +- src/modules/peers/PeerGroupCell.tsx | 1 + src/modules/peers/PeerMultiSelect.tsx | 457 +++++++++++++++ src/modules/peers/PeerOSCell.tsx | 3 + src/modules/peers/PeersTable.tsx | 463 ++++++++------- .../modal/PostureCheckModal.tsx | 2 +- .../table/PostureCheckBrowseTable.tsx | 29 +- .../GroupedRouteHighAvailabilityCell.tsx | 194 +++---- .../route-group/NetworkRoutesTable.tsx | 252 +++++---- .../routes/RouteAddRoutingPeerProvider.tsx | 46 ++ src/modules/routes/RouteModal.tsx | 2 +- src/modules/setup-keys/SetupKeyGroupsCell.tsx | 1 + src/modules/setup-keys/SetupKeyModal.tsx | 1 + .../setup-netbird-modal/SetupModal.tsx | 9 +- src/modules/users/ServiceUsersTable.tsx | 5 +- src/modules/users/UserInviteModal.tsx | 1 + src/modules/users/UserRoleSelector.tsx | 34 +- src/modules/users/UsersTable.tsx | 5 +- .../users/table-cells/UserGroupCell.tsx | 3 +- src/utils/api.tsx | 22 +- src/utils/config.ts | 2 +- src/utils/helpers.ts | 2 +- 51 files changed, 2245 insertions(+), 757 deletions(-) create mode 100644 src/assets/os-icons/FreeBSD.png create mode 100644 src/components/DropdownInfoText.tsx create mode 100644 src/components/DropdownInput.tsx create mode 100644 src/components/Slider.tsx create mode 100644 src/components/VirtualScrollAreaList.tsx create mode 100644 src/components/table/DataTableHeadingPortal.tsx create mode 100644 src/hooks/usePortalElement.tsx create mode 100644 src/hooks/usePrevious.ts create mode 100644 src/hooks/useSearch.ts create mode 100644 src/modules/peers/PeerMultiSelect.tsx create mode 100644 src/modules/routes/RouteAddRoutingPeerProvider.tsx diff --git a/package-lock.json b/package-lock.json index a6173a1..5a85620 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "netbird-dashboard", "version": "2.0.0", "dependencies": { - "@axa-fr/react-oidc": "^5.14.0", + "@axa-fr/react-oidc": "^7.22.18", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", @@ -17,8 +17,9 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -32,6 +33,7 @@ "@types/node": "20.10.6", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-window": "^1.8.8", "autoprefixer": "^10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -62,6 +64,7 @@ "react-jwt": "^1.2.0", "react-loading-skeleton": "^3.3.1", "react-responsive": "^9.0.2", + "react-virtuoso": "^4.9.0", "swr": "^2.2.4", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", @@ -93,16 +96,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@axa-fr/react-oidc": { - "version": "5.14.2", - "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-5.14.2.tgz", - "integrity": "sha512-N+ssJlVtVHnsvlusMxY3zLPKCB+lGzeHIxWXUb0WY3uA7Z+jxx7A2m9W1kHbhYzHuihgA3rWIcdKsvtdkeKXwg==", + "node_modules/@axa-fr/oidc-client": { + "version": "7.22.21", + "resolved": "https://registry.npmjs.org/@axa-fr/oidc-client/-/oidc-client-7.22.21.tgz", + "integrity": "sha512-w6CokGCz9Au0E3bCS5yJCUDlQemGE/TlT8jdN9FltOHI/NUw0Mn/5Rzeh/LOtlo5TIhaOS2nIlCEOY+JEIpj2w==", + "hasInstallScript": true, "dependencies": { - "@openid/appauth": "1.3.1" + "@axa-fr/oidc-client-service-worker": "7.22.21" + } + }, + "node_modules/@axa-fr/oidc-client-service-worker": { + "version": "7.22.21", + "resolved": "https://registry.npmjs.org/@axa-fr/oidc-client-service-worker/-/oidc-client-service-worker-7.22.21.tgz", + "integrity": "sha512-wDZTpRsY36sl4Ah9/ZhzDxybLj46HZjMl7Rn0qLhpK1Sb+GL+d9Agq6xNclkvizDFwuyX6hTaPGQpwcE0WNRQQ==" + }, + "node_modules/@axa-fr/react-oidc": { + "version": "7.22.21", + "resolved": "https://registry.npmjs.org/@axa-fr/react-oidc/-/react-oidc-7.22.21.tgz", + "integrity": "sha512-lEdCt/q7kBXJ1AX+tEK/QAkz4p4G2qOSlhdYxPSSBRIf4ZwZEcmlH6F28W/FySk6tj/coi56dGvmcHz+hSZUDQ==", + "hasInstallScript": true, + "dependencies": { + "@axa-fr/oidc-client": "7.22.21", + "@axa-fr/oidc-client-service-worker": "7.22.21" }, "peerDependencies": { - "react": "x", - "react-dom": "x" + "react": "^17.0.0 || ^18.0.0" } }, "node_modules/@babel/runtime": { @@ -542,32 +560,6 @@ "node": ">= 8" } }, - "node_modules/@openid/appauth": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@openid/appauth/-/appauth-1.3.1.tgz", - "integrity": "sha512-e54kpi219wES2ijPzeHe1kMnT8VKH8YeTd1GAn9BzVBmutz3tBgcG1y8a4pziNr4vNjFnuD4W446Ua7ELnNDiA==", - "dependencies": { - "@types/base64-js": "^1.3.0", - "@types/jquery": "^3.5.5", - "base64-js": "^1.5.1", - "follow-redirects": "^1.13.3", - "form-data": "^4.0.0", - "opener": "^1.5.2" - } - }, - "node_modules/@openid/appauth/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1202,26 +1194,25 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.5.tgz", - "integrity": "sha512-b6PAgH4GQf9QEn8zbT2XUHpW5z8BzqEc7Kl11TwDrvuTrxlkcjTD5qa/bxgKr+nmuXKu4L/W5UZ4mlP/VG/5Gw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz", + "integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1232,6 +1223,148 @@ } } }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.0.0.tgz", @@ -1275,6 +1408,230 @@ } } }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.0.tgz", + "integrity": "sha512-dAHCDA4/ySXROEPaRtaMV5WHL8+JB/DbtyTbJjYkY0RXmKMO2Ln8DFZhywG5/mVQ4WqHDBc8smc14yPXPqZHYA==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -1658,24 +2015,11 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@types/base64-js": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.2.tgz", - "integrity": "sha512-Q2Xn2/vQHRGLRXhQ5+BSLwhHkR3JVflxVKywH0Q6fVoAiUE8fFYL2pE5/l2ZiOiBDfA8qUqRnSxln4G/NFz1Sg==" - }, "node_modules/@types/crypto-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==" }, - "node_modules/@types/jquery": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", - "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", - "dependencies": { - "@types/sizzle": "*" - } - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -1717,6 +2061,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -1731,7 +2083,8 @@ "node_modules/@types/sizzle": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==" + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true }, "node_modules/@types/yauzl": { "version": "2.10.3", @@ -2187,7 +2540,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -2285,6 +2639,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -2927,6 +3282,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3170,6 +3526,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -4057,25 +4414,6 @@ "tailwindcss": "^3" } }, - "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5396,6 +5734,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -5404,6 +5743,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5712,14 +6052,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "bin": { - "opener": "bin/opener-bin.js" - } - }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -6346,6 +6678,18 @@ } } }, + "node_modules/react-virtuoso": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.10.0.tgz", + "integrity": "sha512-CyxU5TYMH4bw2cybH0bNqN/yIg2q2Vd0kbs92tQc5ResZALAIzIVJY4JL6BHgJFQjwrLhCYrFwKq0p+lvBgA0w==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/package.json b/package.json index 5bb2105..70e1d3a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "cypress:open": "cypress open" }, "dependencies": { - "@axa-fr/react-oidc": "^5.14.0", + "@axa-fr/react-oidc": "^7.22.18", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", @@ -22,8 +22,9 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -37,6 +38,7 @@ "@types/node": "20.10.6", "@types/react": "^18", "@types/react-dom": "^18", + "@types/react-window": "^1.8.8", "autoprefixer": "^10", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", @@ -67,6 +69,7 @@ "react-jwt": "^1.2.0", "react-loading-skeleton": "^3.3.1", "react-responsive": "^9.0.2", + "react-virtuoso": "^4.9.0", "swr": "^2.2.4", "tailwind-merge": "^1.14.0", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/(dashboard)/peer/page.tsx b/src/app/(dashboard)/peer/page.tsx index f830f7a..17b6509 100644 --- a/src/app/(dashboard)/peer/page.tsx +++ b/src/app/(dashboard)/peer/page.tsx @@ -327,6 +327,7 @@ function PeerOverview() { disabled={isUser} onChange={setSelectedGroups} values={selectedGroups} + hideAllGroup={true} peer={peer} /> diff --git a/src/app/(dashboard)/peers/page.tsx b/src/app/(dashboard)/peers/page.tsx index 0561786..4170ccd 100644 --- a/src/app/(dashboard)/peers/page.tsx +++ b/src/app/(dashboard)/peers/page.tsx @@ -4,13 +4,12 @@ import Breadcrumbs from "@components/Breadcrumbs"; import InlineLink from "@components/InlineLink"; import Paragraph from "@components/Paragraph"; import SkeletonTable from "@components/skeletons/SkeletonTable"; -import useFetchApi from "@utils/api"; +import { usePortalElement } from "@hooks/usePortalElement"; import { ExternalLinkIcon } from "lucide-react"; -import React, { lazy, Suspense, useEffect } from "react"; +import React, { lazy, Suspense } from "react"; import PeerIcon from "@/assets/icons/PeerIcon"; -import { useGroups } from "@/contexts/GroupsProvider"; +import PeersProvider, { usePeers } from "@/contexts/PeersProvider"; import { useLoggedInUser, useUsers } from "@/contexts/UsersProvider"; -import { Peer } from "@/interfaces/Peer"; import PageContainer from "@/layouts/PageContainer"; import { SetupModalContent } from "@/modules/setup-netbird-modal/SetupModal"; @@ -21,24 +20,22 @@ export default function Peers() { return ( - {permission?.dashboard_view === "blocked" ? ( + {permission.dashboard_view === "blocked" ? ( ) : ( - + + + )} ); } function PeersView() { - const { data: peers, isLoading } = useFetchApi("/peers"); + const { peers, isLoading } = usePeers(); const { users } = useUsers(); - const { refresh } = useGroups(); - - useEffect(() => { - refresh(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const { ref: headingRef, portalTarget } = + usePortalElement(); const peersWithUser = peers?.map((peer) => { if (!users) return peer; @@ -58,7 +55,7 @@ function PeersView() { icon={} /> -

{peers && peers.length > 1 ? `${peers.length} Peers` : "Peers"}

+

Peers

A list of all machines and devices connected to your private network. Use this view to manage peers. @@ -76,7 +73,11 @@ function PeersView() { }> - + ); diff --git a/src/app/(dashboard)/team/user/page.tsx b/src/app/(dashboard)/team/user/page.tsx index 62320f1..d42d823 100644 --- a/src/app/(dashboard)/team/user/page.tsx +++ b/src/app/(dashboard)/team/user/page.tsx @@ -187,7 +187,7 @@ function UserOverview({ user }: Props) { )} -
+
{!user.is_service_user && ( @@ -200,6 +200,7 @@ function UserOverview({ user }: Props) { disabled={isUser} onChange={setSelectedGroups} values={selectedGroups} + hideAllGroup={true} />
)} @@ -214,6 +215,8 @@ function UserOverview({ user }: Props) { - - - Block User - - } - value={} - /> + {!user.is_current && user.role != Role.Owner && ( + + + Block User + + } + value={} + /> + )} + diff --git a/src/app/globals.css b/src/app/globals.css index ca461db..99f5698 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -68,4 +68,9 @@ p { .stepper-bg-variant .step-circle { @apply !border-[#1d2024]; +} + +.webkit-scroll{ + -webkit-overflow-scrolling: touch; + -webkit-transform: translate3d(0, 0, 0); } \ No newline at end of file diff --git a/src/assets/os-icons/FreeBSD.png b/src/assets/os-icons/FreeBSD.png new file mode 100644 index 0000000000000000000000000000000000000000..ab0955688cffb8ea7a23c678d25df8572830269b GIT binary patch literal 3197 zcmV-@41)8CP)NE!UpV~#K?R?wfSC#~6)aRhrh;WEfT;keAfW<=3dU2xW-FL%1+W!pb>pYtF>zvB zk}W&;caBaN$IRIB^^@MylO#e&;w%mwG{!u$9~ZXdwXZg|{9G&+g%C0jk3?$4nc0sI zwk%uxUkJ9a6aiLHbLf7F&Ara#|oS*jH# zGXn<9miPLcyJ#Yu9_%ms%Y7we_%(#8Ex`VM{1Ob7k*)Ej@7}g+P}Qm$fZ7!TTva zF=hWdm$E-~$VrUJMhp6GMGC?-WwZR=mUq(ag>1vd-s7e8#EJAyhI>eblwvJpKq{hy zb%cdTsqYNSTS)1I-VXU9n{Li>Z|prCizMOiU+AH&95QBR9PVt%B_bwFpK1(ny`N0k z{fJpND>EEf!!^KJhl7=C`KZV8`pIK*X99}V{;p@Hj5173m`&h5@S+8={APG@I`RY8 zvNEMWKUU7eG2HJ%{bZleQz-6?hgeM+LR*=6vobyF$c!gW$Hjqb0?vA(;2KT!yXwhz zCLLDhK+|xf^52-Wu73y~OzDBQsUNf~D>E_;*X9ISwWOc)*n?4#K9TfT+JXywf9xbv zHtA>*zR9qjc?x8FZiQb19$wg`S&NzS~>ULdK_%QlO8T z2J;tT(9Ak|ZmpcI}{B!-F_E%sP zn6y%Y2X|AgtBKUnbDv@HrNoGJx+Qn@wu)GRAYMR6Q8p{wsy_%*4ws?w$8y?Ip=kO-gYr}C0)3SLJQAroFk6YkZ+$HdJ-~v}d$%JkhYp%D z*Y|@=5BM5ETINH;qA{%LA%UW4k1-oyHmDQ1lxcFi2s9hxkVw}OmO}!CI6nzF#XFfU zHxh{7M5^jvOBk92x)f69{|Am`GEIUwOpc~3s1O}q3bY%!%!goB6dV+(jtLjipBoCa zGnwg-N1HI)Md0~s$MQ-0I#3AIf2EK{b~9IH06K|O!%WxJVb99E8xn_Vv%jlj{$7dYSmeC zt>`Bcd4suLBGtDi6EYUdnou!r9=qw^xTub{qAuhJfktiidpS-W()UPQ_QCYZh+i&o zs74p+g9>}6P9o6Rl4{^S6OsaMmzA9}rDN!yB|JA3fx>FmqY8LIq7CMBwxnj*jmbzm z9<5Lqc+JxNZgy&|9~yq|s7cRzG%lJ8New)gJC`i2AtDIIH!;OG-^DI1Ou_lZ`JrE1gBy}|8gzOg2i!I(56I|f8;p9t!bCc ze9l{%9=kqe(~2T$^|I>uixbsqPDmZA*qKs{}9lHLpjIz3$Tzn0QEK8j;%e{(C{ z_}_`|;XqxS5_=cgHuvm{PMf8aTVXE6pS;C6L6@v-E7B>ilSxW;$?2xU=DBq=a?qwl z4QkZdtYCth`ypg*Y^9w3(P~9z)dBpHOxkf7s>yF-N!irTlT6A{DOlE(?s|PNdZU)N zYraM>-L$c!T(5;hSi}=AJ~y zH|9=}k|D^%1B#R+4@F9nhax4(Ly?l?p-4&cP^2VzC{i*6UP;d!aU#))IpVYxLK1)? zCCS4+DTz%Xo*WZtutiV_@!^~@iunHL|O^}Y%r?YWhJv=wR4 zKT2j#P(P;i+!HqWq$NR{de9{^pG`h#!&(B<#gcO0d(wtr``kNPTT%_ou7!|hHlYOy zx=zGnlTVKEMIz97(mow1(m-&B$vjA&gaZl%HVxek@_xJ zMr(TB+{6|r@Di!Lz(P0*b*>cl`LAUFyqbHUWBPESI9cz!2tG#Ng!lTSYVO(WpBFbn z2^4r+QU{%=vjIsYmogwBMVi?UjO(-6pEs97T2T;eIdWZ?>PpC*xfxoZAebd}Sc|%{ zA4?%~1K#{=8G#rgMRSk-DbxApL-e7~V+7xQL~jJmK39sK53k1(D2TE}j`myFKb;3x zWAsC-MBB&7-WcZUQ>fEsJ=CbuF`{lZrZ>12GDRTJB#caO$LSFC_$LRN_fh+yRR$3$ zpin16QUV2oi4;(%$&;Kw!Jr}q6l$_0DNr!DNCAbK9LWk4jIx!udW~x#DX^A=1qw#7 zqz-FQm-b^NWGs>iGaX}8kpe5!H&P0fDSpFpl|rDt4Wo+`SfL^r1wv&~N_^0)zaD$2 zvkRm2N$Y5ZAq{LTBqoKF*-VoWC`dx203CPjA2EX}4oFx|+Cgd}iRQkJPDMfUp9}G2 zmytF#>scikOIig}Q8WwuT4+^$fc3nbvOqzSmb8jm)las(5z>~zmg}jD(~-PL9agNl zlwE*KNE2Hrt>;>1#AHP3&;fuCLa|msmk28@f1aXabc)G|6zF8X_qO~g=RF88YjHXz zDbgxuYFW2Z?gTSyaXKb1Qil$&!m{QvUkY2k&aKaRirI+Nq0xY)E$?I=*41tD_H{1a zdRJyA(khx00I|Z#W-=WhOw6|ZuCs2>npumqiV7BDeI;pj31B6$1^4Z_TFfTSMx=E# z?uznK%IvF5h69!oLcEu0(v00000NkvXXu0mjfG;sG_ literal 0 HcmV?d00001 diff --git a/src/auth/OIDCProvider.tsx b/src/auth/OIDCProvider.tsx index 88a5fec..aaf2d8c 100644 --- a/src/auth/OIDCProvider.tsx +++ b/src/auth/OIDCProvider.tsx @@ -1,10 +1,10 @@ "use client"; -import { OidcProvider } from "@axa-fr/react-oidc"; import { AuthorityConfiguration, OidcConfiguration, -} from "@axa-fr/react-oidc/dist/vanilla/oidc"; + OidcProvider, +} from "@axa-fr/react-oidc"; import FullScreenLoading from "@components/ui/FullScreenLoading"; import { useLocalStorage } from "@hooks/useLocalStorage"; import { useRedirect } from "@hooks/useRedirect"; @@ -30,7 +30,7 @@ const auth0AuthorityConfig: AuthorityConfiguration = { revocation_endpoint: new URL("oauth/revoke", config.authority).href, end_session_endpoint: new URL("v2/logout", config.authority).href, userinfo_endpoint: new URL("userinfo", config.authority).href, - //issuer: new URL("", config.authority).href, + issuer: new URL("", config.authority).href, }; const onEvent = (configurationName: any, eventName: any, data: any) => { diff --git a/src/auth/SecureProvider.tsx b/src/auth/SecureProvider.tsx index 41cb13c..dcc47d4 100644 --- a/src/auth/SecureProvider.tsx +++ b/src/auth/SecureProvider.tsx @@ -11,9 +11,17 @@ export const SecureProvider = ({ children }: Props) => { const currentPath = usePathname(); useEffect(() => { + let timeout: NodeJS.Timeout | undefined = undefined; if (!isAuthenticated) { - login(currentPath); + timeout = setTimeout(async () => { + if (!isAuthenticated) { + await login(currentPath); + } + }, 1500); } + return () => { + clearTimeout(timeout); + }; }, [currentPath, isAuthenticated, login]); return ( diff --git a/src/components/DropdownInfoText.tsx b/src/components/DropdownInfoText.tsx new file mode 100644 index 0000000..a8d1879 --- /dev/null +++ b/src/components/DropdownInfoText.tsx @@ -0,0 +1,11 @@ +import * as React from "react"; + +type Props = { + children: React.ReactNode; +}; + +export const DropdownInfoText = ({ children }: Props) => { + return ( +
{children}
+ ); +}; diff --git a/src/components/DropdownInput.tsx b/src/components/DropdownInput.tsx new file mode 100644 index 0000000..7928ab0 --- /dev/null +++ b/src/components/DropdownInput.tsx @@ -0,0 +1,48 @@ +import { IconArrowBack } from "@tabler/icons-react"; +import { cn } from "@utils/helpers"; +import { SearchIcon } from "lucide-react"; +import * as React from "react"; +import { forwardRef } from "react"; + +type Props = { + value: string; + onChange: (value: string) => void; + placeholder?: string; +}; + +export const DropdownInput = forwardRef( + ({ value, onChange, placeholder = "Search..." }, ref) => { + return ( +
+ onChange(e.target.value)} + placeholder={placeholder} + /> +
+
+ +
+
+
+
+ +
+
+
+ ); + }, +); + +DropdownInput.displayName = "DropdownInput"; diff --git a/src/components/NetworkRouteSelector.tsx b/src/components/NetworkRouteSelector.tsx index eac7a48..01cfb06 100644 --- a/src/components/NetworkRouteSelector.tsx +++ b/src/components/NetworkRouteSelector.tsx @@ -1,6 +1,7 @@ import { CommandItem } from "@components/Command"; import FullTooltip from "@components/FullTooltip"; import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover"; +import TextWithTooltip from "@components/ui/TextWithTooltip"; import { IconArrowBack } from "@tabler/icons-react"; import useFetchApi from "@utils/api"; import { cn } from "@utils/helpers"; @@ -108,12 +109,12 @@ export function NetworkRouteSelector({ {value ? (
- {value.network_id} +
- {option.network_id} +
{domains.join(", ")}
} > -
+
{firstDomain} {domains.length > 1 && "+" + (domains.length - 1)}
diff --git a/src/components/PeerGroupSelector.tsx b/src/components/PeerGroupSelector.tsx index c75994d..c43402b 100644 --- a/src/components/PeerGroupSelector.tsx +++ b/src/components/PeerGroupSelector.tsx @@ -29,6 +29,7 @@ interface MultiSelectProps { max?: number; disabled?: boolean; popoverWidth?: "auto" | number; + hideAllGroup?: boolean; } export function PeerGroupSelector({ onChange, @@ -37,6 +38,7 @@ export function PeerGroupSelector({ max, disabled = false, popoverWidth = "auto", + hideAllGroup = false, }: MultiSelectProps) { const { groups, dropdownOptions, setDropdownOptions } = useGroups(); const searchRef = React.useRef(null); @@ -47,7 +49,13 @@ export function PeerGroupSelector({ useEffect(() => { if (!groups) return; const sortedGroups = sortBy([...groups], "name") as Group[]; - setDropdownOptions(unionBy(sortedGroups, dropdownOptions, "name")); + + let uniqueGroups = unionBy(sortedGroups, dropdownOptions, "name"); + uniqueGroups = hideAllGroup + ? uniqueGroups.filter((group) => group.name !== "All") + : uniqueGroups; + + setDropdownOptions(uniqueGroups); // eslint-disable-next-line react-hooks/exhaustive-deps }, [groups]); @@ -66,8 +74,11 @@ export function PeerGroupSelector({ const option = dropdownOptions.find((option) => option.name == name); const groupPeers: GroupPeer[] | undefined = (group?.peers as GroupPeer[]) || []; - groupPeers && - groupPeers.push({ id: peer?.id as string, name: peer?.name as string }); + + if (peer) { + groupPeers && + groupPeers.push({ id: peer?.id as string, name: peer?.name as string }); + } if (!group && !option) { setDropdownOptions((previous) => [ @@ -100,17 +111,18 @@ export function PeerGroupSelector({ const isSearching = search.length > 0; const groupDoesNotExist = dropdownOptions.filter((item) => item.name == trim(search)).length == 0; - return isSearching && groupDoesNotExist; + const isAllGroup = search.toLowerCase() == "all"; + return isSearching && groupDoesNotExist && !isAllGroup; }, [search, dropdownOptions]); const [open, setOpen] = useState(false); const folderIcon = useMemo(() => { - return ; + return ; }, []); const peerIcon = useMemo(() => { - return ; + return ; }, []); const [slice, setSlice] = useState(10); @@ -203,7 +215,7 @@ export function PeerGroupSelector({ "min-h-[42px] w-full relative", "border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center", "bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0", - "dark:placeholder:text-neutral-500 font-light placeholder:text-neutral-500 pl-10", + "dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10", )} ref={searchRef} value={search} @@ -238,9 +250,7 @@ export function PeerGroupSelector({ {searchedGroupNotFound && ( ); +MapPinIcon.displayName = "MapPinIcon"; + +const LinuxIcon = memo(() => ( + + + +)); +LinuxIcon.displayName = "LinuxIcon"; + interface MultiSelectProps { value?: Peer; onChange: React.Dispatch>; @@ -23,6 +33,13 @@ interface MultiSelectProps { disabled?: boolean; } +const searchPredicate = (item: Peer, query: string) => { + const lowerCaseQuery = query.toLowerCase(); + if (item.name.toLowerCase().includes(lowerCaseQuery)) return true; + if (item.hostname.toLowerCase().includes(lowerCaseQuery)) return true; + return item.ip.toLowerCase().startsWith(lowerCaseQuery); +}; + export function PeerSelector({ onChange, value, @@ -30,13 +47,16 @@ export function PeerSelector({ disabled = false, }: MultiSelectProps) { const { data: peers } = useFetchApi("/peers"); - - const [dropdownOptions, setDropdownOptions] = useState([]); - const searchRef = React.useRef(null); const [inputRef, { width }] = useElementSize(); - const [search, setSearch] = useState(""); - // Update dropdown options when peers change + const [unfilteredItems, setUnfilteredItems] = useState([]); + const [filteredItems, search, setSearch] = useSearch( + unfilteredItems, + searchPredicate, + { filter: true, debounce: 150 }, + ); + + // Update unfiltered items when peers change useEffect(() => { if (!peers) return; @@ -56,7 +76,7 @@ export function PeerSelector({ }); } - setDropdownOptions(unionBy(options, dropdownOptions, "id")); + setUnfilteredItems(unionBy(options, unfilteredItems, "id")); // eslint-disable-next-line react-hooks/exhaustive-deps }, [peers]); @@ -68,44 +88,11 @@ export function PeerSelector({ onChange(peer); setSearch(""); } + setOpen(false); }; - const peerNotFound = useMemo(() => { - const isSearching = search.length > 0; - - // Search peer by ip or name - const peerFound = - dropdownOptions.filter((item) => { - return ( - item.name.includes(search) || - item.hostname.includes(search) || - item.ip.includes(search) - ); - }).length > 0; - - return isSearching && !peerFound; - }, [search, dropdownOptions]); - const [open, setOpen] = useState(false); - const [slice, setSlice] = useState(10); - - useEffect(() => { - if (open) { - setTimeout(() => { - setSlice(dropdownOptions.length); - }, 100); - } else { - setSlice(10); - } - }, [open, dropdownOptions]); - - const LinuxIcon = ( - - - - ); - return (
- {LinuxIcon} +
@@ -150,7 +137,7 @@ export function PeerSelector({ "text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]" } > - + {value.ip}
@@ -168,113 +155,67 @@ export function PeerSelector({ style={{ width: width, }} - forceMount={true} align="start" side={"top"} sideOffset={10} > - { - const formatValue = trim(value.toLowerCase()); - const formatSearch = trim(search.toLowerCase()); - if (formatValue.includes(formatSearch)) return 1; - return 0; - }} - > - -
- -
-
- -
-
-
-
- -
-
-
+
+ -
- {dropdownOptions.length == 0 && !peerNotFound && ( -
- { - "Seems like you don't have any linux peers to assign as a routing peer." - } -
- )} - {peerNotFound && ( -
- There are no peers matching your search. -
- )} - - - {dropdownOptions.slice(0, slice).map((option) => { - return ( - { - togglePeer(option); - setOpen(false); - }} - > -
- {LinuxIcon} - -
+ {unfilteredItems.length == 0 && ( + + { + "Seems like you don't have any linux peers to assign as a routing peer." + } + + )} -
- - {option.ip} -
-
- ); - })} -
-
-
- - + {filteredItems.length == 0 && ( + + There are no peers matching your search. + + )} + + {filteredItems.length > 0 && ( + { + return ( + <> +
+ + +
+ +
+ + {option.ip} +
+ + ); + }} + /> + )} +
); diff --git a/src/components/ScrollArea.tsx b/src/components/ScrollArea.tsx index c608867..a1bc67a 100644 --- a/src/components/ScrollArea.tsx +++ b/src/components/ScrollArea.tsx @@ -4,30 +4,65 @@ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; import { cn } from "@utils/helpers"; import * as React from "react"; +type AdditionalScrollAreaProps = { + withoutViewport?: boolean; +}; + const ScrollArea = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & + AdditionalScrollAreaProps +>(({ className, children, withoutViewport = false, ...props }, ref) => ( - - {children} - + {withoutViewport ? ( + children + ) : ( + + {children} + + )} )); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; +type AdditionalScrollAreaViewportProps = { + disableOverflowY?: boolean; +}; + +const ScrollAreaViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + AdditionalScrollAreaViewportProps +>(({ disableOverflowY = true, ...props }, ref) => { + return ( + + ); +}); +ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName; + const ScrollBar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, orientation = "vertical", ...props }, ref) => ( , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/src/components/VirtualScrollAreaList.tsx b/src/components/VirtualScrollAreaList.tsx new file mode 100644 index 0000000..47f36bf --- /dev/null +++ b/src/components/VirtualScrollAreaList.tsx @@ -0,0 +1,132 @@ +import { + MemoizedScrollArea, + MemoizedScrollAreaViewport, +} from "@components/ScrollArea"; +import { cn } from "@utils/helpers"; +import * as React from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; + +type Props = { + items: T[]; + onSelect: (item: T) => void; + renderItem?: (item: T) => React.ReactNode; +}; + +export function VirtualScrollAreaList({ + items, + onSelect, + renderItem, +}: Props) { + const virtuosoRef = useRef(null); + const [selected, setSelected] = useState(0); + + useEffect(() => { + setSelected(0); + }, [items]); + + const scrollToItem = useCallback((index: number) => { + virtuosoRef.current?.scrollIntoView({ + index, + behavior: "auto", + align: "center", + }); + }, []); + + const navigation = useCallback( + (e: KeyboardEvent) => { + if (items.length === 0) return; + const length = items.length - 1; + if (e.code === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) { + e.preventDefault(); + const newSelected = selected === 0 ? length : selected - 1; + setSelected(newSelected); + scrollToItem(newSelected); + } else if (e.key === "ArrowDown" || e.key === "Tab") { + e.preventDefault(); + const newSelected = selected === length ? 0 : selected + 1; + setSelected(newSelected); + scrollToItem(newSelected); + } + if (e.key === "Enter") { + e.preventDefault(); + onSelect?.(items[selected]); + } + }, + [items, scrollToItem, selected], + ); + + useEffect(() => { + window.addEventListener("keydown", navigation); + return () => { + window.removeEventListener("keydown", navigation); + }; + }, [navigation]); + + const renderMemoizedItem = useMemo(() => renderItem, [renderItem]); + + return ( + + items[index].id as string} + context={{ selected, setSelected, onClick: onSelect }} + itemContent={(index, option, { selected, setSelected, onClick }) => { + return ( + setSelected(index)} + id={option.id} + onClick={() => onClick(option as T)} + ariaSelected={selected === index} + > + {renderMemoizedItem ? renderMemoizedItem(option) : option.id} + + ); + }} + style={{ height: 195 }} + components={{ + Scroller: MemoizedScrollAreaViewport, + }} + /> + + ); +} + +type ItemWrapperProps = { + children: React.ReactNode; + id?: string; + onMouseEnter?: () => void; + onClick?: () => void; + ariaSelected?: boolean; +}; + +export const VirtualScrollListItemWrapper = memo( + ({ id, children, onClick, onMouseEnter, ariaSelected }: ItemWrapperProps) => { + return ( +
+
+ {children} +
+
+ ); + }, +); +VirtualScrollListItemWrapper.displayName = "VirtualScrollListItemWrapper"; diff --git a/src/components/table/DataTable.tsx b/src/components/table/DataTable.tsx index 836c292..54f026d 100644 --- a/src/components/table/DataTable.tsx +++ b/src/components/table/DataTable.tsx @@ -1,6 +1,7 @@ "use client"; import SkeletonTable from "@components/skeletons/SkeletonTable"; import DataTableGlobalSearch from "@components/table/DataTableGlobalSearch"; +import { DataTableHeadingPortal } from "@components/table/DataTableHeadingPortal"; import { DataTablePagination } from "@components/table/DataTablePagination"; import DataTableResetFilterButton from "@components/table/DataTableResetFilterButton"; import { @@ -28,6 +29,7 @@ import { getSortedRowModel, PaginationState, Row, + RowSelectionState, SortingState, Table as TanStackTable, useReactTable, @@ -105,6 +107,7 @@ interface DataTableProps { aboveTable?: (table: TanStackTable) => React.ReactNode; searchPlaceholder?: string; columnVisibility?: VisibilityState; + setColumnVisibility?: React.Dispatch>; sorting?: SortingState; setSorting?: React.Dispatch>; text?: string; @@ -126,6 +129,11 @@ interface DataTableProps { rightSide?: (table: TanStackTable) => React.ReactNode; manualPagination?: boolean; showHeader?: boolean; + rowSelection?: RowSelectionState; + setRowSelection?: React.Dispatch>; + useRowId?: boolean; + headingTarget?: HTMLHeadingElement | null; + showResetFilterButton?: boolean; } export function DataTable(props: DataTableProps) { @@ -139,6 +147,7 @@ export function DataTableContent({ children, searchPlaceholder = "Search...", columnVisibility = {}, + setColumnVisibility, sorting = [], setSorting, text = "rows", @@ -159,6 +168,11 @@ export function DataTableContent({ rightSide, manualPagination = false, showHeader = true, + rowSelection, + setRowSelection, + useRowId, + headingTarget, + showResetFilterButton = true, }: DataTableProps) { const path = usePathname(); const [columnFilters, setColumnFilters] = useLocalStorage( @@ -176,9 +190,6 @@ export function DataTableContent({ pageSize: 10, }); - const [tableColumnVisibility, setColumnVisibility] = - React.useState(columnVisibility); - const hasInitialData = !!(data && data.length > 0); const table = useReactTable({ @@ -196,8 +207,9 @@ export function DataTableContent({ manualPagination: manualPagination, state: { sorting, + rowSelection: rowSelection ?? {}, columnFilters, - columnVisibility: tableColumnVisibility, + columnVisibility: columnVisibility, globalFilter: globalSearch, pagination: paginationState, }, @@ -207,6 +219,8 @@ export function DataTableContent({ pageSize: 10, }, }, + getRowId: useRowId ? (row) => row.id : undefined, + onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onPaginationChange: setPaginationState, onColumnFiltersChange: setColumnFilters, @@ -235,6 +249,7 @@ export function DataTableContent({ table.setPageIndex(0); setColumnFilters([]); setGlobalSearch(""); + setRowSelection?.({}); }; return ( @@ -248,11 +263,14 @@ export function DataTableContent({ setGlobalSearch={(val) => { table.setPageIndex(0); setGlobalSearch(val); + setRowSelection?.({}); }} placeholder={searchPlaceholder} /> {children && children(table)} - + {showResetFilterButton && ( + + )}
{rightSide && rightSide(table)} @@ -412,6 +430,11 @@ export function DataTableContent({
+
); } diff --git a/src/components/table/DataTableHeadingPortal.tsx b/src/components/table/DataTableHeadingPortal.tsx new file mode 100644 index 0000000..d503fbb --- /dev/null +++ b/src/components/table/DataTableHeadingPortal.tsx @@ -0,0 +1,73 @@ +import { Table } from "@tanstack/react-table"; +import * as React from "react"; +import { useRef } from "react"; +import { createPortal } from "react-dom"; + +type Props = { + table: Table | null; + headingTarget?: HTMLHeadingElement | null; + text: string; +}; +export const DataTableHeadingPortal = function ({ + table, + headingTarget, + text = "Items", +}: Props) { + const hasMounted = useRef(false); + + if (!headingTarget) return; + + if (!hasMounted.current) { + headingTarget.innerHTML = ""; + hasMounted.current = true; + } + + const totalItems = table?.getPreFilteredRowModel().rows.length; + const filteredItems = table?.getFilteredRowModel().rows.length; + + const hasAnyFiltersActive = + table && + !( + table?.getState().columnFilters.length <= 0 && + table?.getState().globalFilter === "" + ); + + return createPortal( + , + headingTarget, + ); +}; + +type HeadingProps = { + hasAnyFilterActive: boolean | null; + filteredItems?: number; + totalItems?: number; + text: string; +}; + +const Heading = ({ + hasAnyFilterActive, + filteredItems, + totalItems, + text, +}: HeadingProps) => { + if (!totalItems || totalItems == 1) { + return text; + } + + if (hasAnyFilterActive) { + return ( + <> + {filteredItems} of {totalItems}{" "} + {text} + + ); + } + + return `${totalItems} ${text}`; +}; diff --git a/src/components/ui/GroupBadge.tsx b/src/components/ui/GroupBadge.tsx index 2415889..141f2a9 100644 --- a/src/components/ui/GroupBadge.tsx +++ b/src/components/ui/GroupBadge.tsx @@ -25,7 +25,10 @@ export default function GroupBadge({ useHover={true} variant={"gray-ghost"} className={cn("transition-all group whitespace-nowrap", className)} - onClick={onClick} + onClick={(e) => { + e.preventDefault(); + onClick?.(); + }} > diff --git a/src/contexts/PeersProvider.tsx b/src/contexts/PeersProvider.tsx index cfab94d..d99e739 100644 --- a/src/contexts/PeersProvider.tsx +++ b/src/contexts/PeersProvider.tsx @@ -1,5 +1,5 @@ import useFetchApi from "@utils/api"; -import React from "react"; +import React, { useMemo } from "react"; import type { Peer } from "@/interfaces/Peer"; type Props = { @@ -9,15 +9,21 @@ type Props = { const PeerContext = React.createContext( {} as { peers: Peer[] | undefined; + isLoading: boolean; }, ); -export default function PeersProvider({ children }: Props) { - const { data: peers } = useFetchApi("/peers"); +export default function PeersProvider({ children }: Readonly) { + const { data: peers, isLoading } = useFetchApi("/peers"); - return ( - {children} - ); + const data = useMemo(() => { + return { + peers, + isLoading, + }; + }, [peers, isLoading]); + + return {children}; } export const usePeers = () => React.useContext(PeerContext); diff --git a/src/hooks/useOperatingSystem.ts b/src/hooks/useOperatingSystem.ts index 8ca85e7..1a8e6a5 100644 --- a/src/hooks/useOperatingSystem.ts +++ b/src/hooks/useOperatingSystem.ts @@ -2,6 +2,10 @@ import { OperatingSystem } from "@/interfaces/OperatingSystem"; +/** + * Get the operating system of the user based on the user agent of the browser + * This is used for the setup modal to show the correct installation guide + */ export default function useOperatingSystem() { const isBrowser = typeof window !== "undefined"; const userAgent = isBrowser ? navigator.userAgent.toLowerCase() : ""; @@ -9,10 +13,18 @@ export default function useOperatingSystem() { ? /(iP*)/g.test(navigator.userAgent) && navigator.maxTouchPoints > 2 : false; if (iOS) return OperatingSystem.IOS; + // For FreeBSD, we return Linux as we currently don't have an official installation guide for FreeBSD + if (userAgent.toLowerCase().includes("freebsd")) return OperatingSystem.LINUX; return getOperatingSystem(userAgent); } +/** + * Get the operating system based on a string (user agent, api response, etc.) + * Falls back to Linux if the operating system is not recognized + */ export const getOperatingSystem = (os: string) => { + if (os.toLowerCase().includes("freebsd")) + return OperatingSystem.FREEBSD as const; if (os.toLowerCase().includes("darwin")) return OperatingSystem.APPLE as const; if (os.toLowerCase().includes("mac")) return OperatingSystem.APPLE as const; diff --git a/src/hooks/usePortalElement.tsx b/src/hooks/usePortalElement.tsx new file mode 100644 index 0000000..9aa51c3 --- /dev/null +++ b/src/hooks/usePortalElement.tsx @@ -0,0 +1,12 @@ +import { useLayoutEffect, useRef, useState } from "react"; + +export function usePortalElement() { + const ref = useRef(null); + const [portalTarget, setPortalTarget] = useState(null); + + useLayoutEffect(() => { + setPortalTarget(ref.current); + }, []); + + return { ref, portalTarget, setPortalTarget }; +} diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts new file mode 100644 index 0000000..6e07b3f --- /dev/null +++ b/src/hooks/usePrevious.ts @@ -0,0 +1,13 @@ +import { useEffect, useRef } from "react"; + +const usePrevious = (value: T): T | undefined => { + const ref = useRef(); + + useEffect(() => { + ref.current = value; + }, [value]); + + return ref.current; +}; + +export default usePrevious; diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 0000000..1676f24 --- /dev/null +++ b/src/hooks/useSearch.ts @@ -0,0 +1,91 @@ +import { debounce as lodashDebounce, isEqual } from "lodash"; +import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; +import usePrevious from "./usePrevious"; + +export type Predicate = (item: T, query: string) => boolean; + +export interface Options { + initialQuery?: string; + filter?: boolean; + debounce?: number; +} + +function filterCollection( + collection: T[], + predicate: Predicate, + query: string, + filter: boolean, +): T[] { + if (query) { + return collection.filter((item) => predicate(item, query)); + } else { + return filter ? collection : []; + } +} + +export function useSearch( + collection: T[], + predicate: Predicate, + { debounce, filter = false, initialQuery = "" }: Options = {}, +): [ + T[], + string, + (event: ChangeEvent | string) => void, + (querty: string) => void, +] { + const isMounted = useRef(false); + const [query, setQuery] = useState(initialQuery); + const prevCollection = usePrevious(collection); + const prevPredicate = usePrevious(predicate); + const prevQuery = usePrevious(query); + const prevFilter = usePrevious(filter); + const [filteredCollection, setFilteredCollection] = useState(() => + filterCollection(collection, predicate, query, filter), + ); + + const handleChange = useCallback( + (event: ChangeEvent | string) => { + setQuery(typeof event === "string" ? event : event.target.value); + }, + [setQuery], + ); + + const debouncedFilterCollection = useCallback( + lodashDebounce( + ( + collection: T[], + predicate: Predicate, + query: string, + filter: boolean, + ) => { + if (isMounted.current) { + setFilteredCollection( + filterCollection(collection, predicate, query, filter), + ); + } + }, + debounce, + ), + [debounce], + ); + + useEffect(() => { + if ( + !isEqual(collection, prevCollection) || + !isEqual(predicate, prevPredicate) || + !isEqual(query, prevQuery) || + !isEqual(filter, prevFilter) + ) + debouncedFilterCollection(collection, predicate, query, filter); + }, [collection, predicate, query, filter]); + + useEffect(() => { + isMounted.current = true; + + return () => { + isMounted.current = false; + }; + }, []); + + return [filteredCollection, query, handleChange, setQuery]; +} diff --git a/src/interfaces/OperatingSystem.ts b/src/interfaces/OperatingSystem.ts index 7eef992..843f782 100644 --- a/src/interfaces/OperatingSystem.ts +++ b/src/interfaces/OperatingSystem.ts @@ -6,4 +6,5 @@ export enum OperatingSystem { DOCKER, IOS, UNKNOWN, + FREEBSD, } diff --git a/src/modules/access-control/AccessControlModal.tsx b/src/modules/access-control/AccessControlModal.tsx index 3324393..3ef7c65 100644 --- a/src/modules/access-control/AccessControlModal.tsx +++ b/src/modules/access-control/AccessControlModal.tsx @@ -272,6 +272,16 @@ export function AccessControlModalContent({ if (continuePostureChecksDisabled) return true; }, [name, continuePostureChecksDisabled]); + const handleProtocolChange = (p: Protocol) => { + setProtocol(p); + if (p == "icmp") { + setPorts([]); + } + if (p == "all") { + setPorts([]); + } + }; + return (