diff --git a/package-lock.json b/package-lock.json
index 6781ccea..75aed861 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
@@ -921,6 +922,352 @@
}
}
},
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.4.tgz",
+ "integrity": "sha512-QSUUnRA3PQ2UhvoCv3eYvMnCAgGQW+sTu86QPuNb+ZMi+ZENd6UWpiXbcWDQ4AEaKF9KKpCHBeaJz9Rw6lRlaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.3",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "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-hover-card/node_modules/@radix-ui/primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
+ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
+ "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1"
+ },
+ "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-hover-card/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
+ "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "license": "MIT",
+ "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-hover-card/node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "license": "MIT",
+ "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-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz",
+ "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "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-hover-card/node_modules/@radix-ui/react-popper": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
+ "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "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-hover-card/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
+ "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1",
+ "@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-hover-card/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
+ "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@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-hover-card/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
+ "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.1"
+ },
+ "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-hover-card/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
+ "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1"
+ },
+ "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-hover-card/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==",
+ "license": "MIT",
+ "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-hover-card/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==",
+ "license": "MIT",
+ "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-hover-card/node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "license": "MIT",
+ "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-hover-card/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==",
+ "license": "MIT",
+ "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-hover-card/node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "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-hover-card/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==",
+ "license": "MIT",
+ "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-hover-card/node_modules/@radix-ui/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-id": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
diff --git a/package.json b/package.json
index 5dc826a4..b63bac9c 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
diff --git a/src/app/(dashboard)/network-routes/page.tsx b/src/app/(dashboard)/network-routes/page.tsx
index c4646cc8..e5b00981 100644
--- a/src/app/(dashboard)/network-routes/page.tsx
+++ b/src/app/(dashboard)/network-routes/page.tsx
@@ -14,7 +14,6 @@ import PeersProvider from "@/contexts/PeersProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { Route } from "@/interfaces/Route";
import PageContainer from "@/layouts/PageContainer";
-import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
import useGroupedRoutes from "@/modules/route-group/useGroupedRoutes";
const NetworkRoutesTable = lazy(
@@ -40,9 +39,7 @@ export default function NetworkRoutes() {
icon={ }
/>
-
- Network Routes
-
+ Network Routes
Network routes allow you to access other networks like LANs and
VPCs without installing NetBird on every resource.
diff --git a/src/app/(dashboard)/networks/page.tsx b/src/app/(dashboard)/networks/page.tsx
index d3b274f5..07683348 100644
--- a/src/app/(dashboard)/networks/page.tsx
+++ b/src/app/(dashboard)/networks/page.tsx
@@ -31,12 +31,15 @@ export default function Networks() {
Networks
- Networks allow you to access other resources like LANs and VPCs
- without installing NetBird on every device.
+ Networks allow you to access internal resources in LANs and VPCs without
+ installing NetBird on every machine.
Learn more about
-
+
Networks
diff --git a/src/components/PeerGroupSelector.tsx b/src/components/PeerGroupSelector.tsx
index 904ed397..dfaf6a9b 100644
--- a/src/components/PeerGroupSelector.tsx
+++ b/src/components/PeerGroupSelector.tsx
@@ -30,7 +30,7 @@ import * as React from "react";
import { Fragment, useEffect, useMemo, useState } from "react";
import { useGroups } from "@/contexts/GroupsProvider";
import { useElementSize } from "@/hooks/useElementSize";
-import type {Group, GroupPeer, GroupResource} from "@/interfaces/Group";
+import type { Group, GroupPeer, GroupResource } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
import type { Peer } from "@/interfaces/Peer";
@@ -48,6 +48,7 @@ interface MultiSelectProps {
showRoutes?: boolean;
disabledGroups?: Group[];
dataCy?: string;
+ showResourceCounter?: boolean;
}
export function PeerGroupSelector({
onChange,
@@ -63,6 +64,7 @@ export function PeerGroupSelector({
showRoutes = false,
disabledGroups,
dataCy = "group-selector-dropdown",
+ showResourceCounter = true,
}: Readonly) {
const { groups, dropdownOptions, setDropdownOptions, addDropdownOptions } =
useGroups();
@@ -105,20 +107,34 @@ export function PeerGroupSelector({
const groupPeers: GroupPeer[] | undefined =
(group?.peers as GroupPeer[]) || [];
const groupResources: GroupResource[] | undefined =
- (group?.resources as GroupResource[]) || [];
+ (group?.resources as GroupResource[]) || [];
if (peer) groupPeers?.push({ id: peer?.id as string, name: peer?.name });
if (!group && !option) {
- addDropdownOptions([{ name: name, peers: groupPeers, resources: groupResources }]);
+ addDropdownOptions([
+ { name: name, peers: groupPeers, resources: groupResources },
+ ]);
}
if (max == 1 && values.length == 1) {
- onChange([{ name: name, id: group?.id, peers: groupPeers, resources: groupResources }]);
+ onChange([
+ {
+ name: name,
+ id: group?.id,
+ peers: groupPeers,
+ resources: groupResources,
+ },
+ ]);
} else {
onChange((previous) => [
...previous,
- { name: name, id: group?.id, peers: groupPeers, resources: groupResources },
+ {
+ name: name,
+ id: group?.id,
+ peers: groupPeers,
+ resources: groupResources,
+ },
]);
}
@@ -396,7 +412,9 @@ export function PeerGroupSelector({
)}
-
+ {showResourceCounter && (
+
+ )}
- {unfilteredItems.length == 0 && (
-
- {
- "Seems like you don't have any linux peers to assign as a routing peer."
- }
-
+ {unfilteredItems.length == 0 && !search && (
+
+
+ {
+ "Seems like you don't have any Linux peers to assign as a routing peer."
+ }
+
+
)}
- {filteredItems.length == 0 && (
+ {filteredItems.length == 0 && search != "" && (
There are no peers matching your search.
diff --git a/src/components/ScrollArea.tsx b/src/components/ScrollArea.tsx
index 1f5551aa..4f8c6925 100644
--- a/src/components/ScrollArea.tsx
+++ b/src/components/ScrollArea.tsx
@@ -1,5 +1,3 @@
-"use client";
-
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@utils/helpers";
import * as React from "react";
@@ -15,46 +13,31 @@ const ScrollArea = React.forwardRef<
>(({ className, children, withoutViewport = false, ...props }, ref) => (
{withoutViewport ? (
children
) : (
-
- {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 (
-
- );
-});
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
const ScrollBar = React.forwardRef<
@@ -63,14 +46,11 @@ const ScrollBar = React.forwardRef<
>(({ className, orientation = "vertical", ...props }, ref) => (
diff --git a/src/components/SidebarItem.tsx b/src/components/SidebarItem.tsx
index 1836b61a..067d40c2 100644
--- a/src/components/SidebarItem.tsx
+++ b/src/components/SidebarItem.tsx
@@ -60,10 +60,12 @@ export default function SidebarItem({
{children};
+export default function Steps({
+ children,
+ className,
+ horizontal = false,
+}: Readonly) {
+ return (
+
+ {children}
+
+ );
}
type StepProps = {
@@ -14,21 +23,32 @@ type StepProps = {
step: number;
line?: boolean;
center?: boolean;
+ horizontal?: boolean;
};
-const Step = ({ children, step, line = true, center = false }: StepProps) => {
+const Step = ({
+ children,
+ step,
+ line = true,
+ center = false,
+ horizontal,
+}: StepProps) => {
return (
{line && (
)}
diff --git a/src/components/ui/GroupBadge.tsx b/src/components/ui/GroupBadge.tsx
index 641fdc10..93e6f9b1 100644
--- a/src/components/ui/GroupBadge.tsx
+++ b/src/components/ui/GroupBadge.tsx
@@ -1,5 +1,6 @@
import Badge from "@components/Badge";
-import TextWithTooltip from "@components/ui/TextWithTooltip";
+import { NewBadge } from "@components/ui/NewBadge";
+import TruncatedText from "@components/ui/TruncatedText";
import { cn } from "@utils/helpers";
import { FolderGit2, XIcon } from "lucide-react";
import * as React from "react";
@@ -37,18 +38,9 @@ export default function GroupBadge({
>
-
+
{children}
- {isNew && showNewBadge && (
-
- NEW
-
- )}
-
+ {isNew && showNewBadge &&
}
{showX && (
{
+ return (
+
+ {text}
+
+ );
+};
diff --git a/src/components/ui/TruncatedText.tsx b/src/components/ui/TruncatedText.tsx
new file mode 100644
index 00000000..66a36c82
--- /dev/null
+++ b/src/components/ui/TruncatedText.tsx
@@ -0,0 +1,78 @@
+import * as HoverCard from "@radix-ui/react-hover-card";
+import { cn } from "@utils/helpers";
+import React, { useMemo, useState } from "react";
+
+type Props = {
+ text?: string;
+ className?: string;
+ maxChars?: number;
+ hideTooltip?: boolean;
+};
+
+export default function TruncatedText({
+ text,
+ className,
+ maxChars = 40,
+ hideTooltip = false,
+}: Props) {
+ const charCount = useMemo(() => {
+ if (!text) return 0;
+ return text.length;
+ }, [text]);
+
+ const isDisabled = charCount <= maxChars || hideTooltip;
+
+ const [open, setOpen] = useState(false);
+
+ if (isDisabled) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ setOpen(false)}
+ onMouseEnter={() => setOpen(false)}
+ alignOffset={20}
+ sideOffset={4}
+ className={cn(
+ "z-[9999] overflow-hidden rounded-md border border-neutral-200 bg-white text-sm text-neutral-950 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-nb-gray-930 dark:bg-nb-gray-940 dark:text-neutral-50",
+ className,
+ "px-3 py-1.5",
+ )}
+ >
+
+
+
+
+ );
+}
diff --git a/src/interfaces/Network.ts b/src/interfaces/Network.ts
index 1e9f37aa..e7cd5601 100644
--- a/src/interfaces/Network.ts
+++ b/src/interfaces/Network.ts
@@ -16,6 +16,7 @@ export interface NetworkRouter {
peer_groups?: string[];
metric: number;
masquerade: boolean;
+ enabled: boolean;
}
export interface NetworkResource {
@@ -25,4 +26,5 @@ export interface NetworkResource {
address: string;
groups?: string[] | Group[];
type?: "domain" | "host" | "subnet";
+ enabled: boolean;
}
diff --git a/src/modules/access-control/AccessControlModal.tsx b/src/modules/access-control/AccessControlModal.tsx
index 5bc6d573..246b12cc 100644
--- a/src/modules/access-control/AccessControlModal.tsx
+++ b/src/modules/access-control/AccessControlModal.tsx
@@ -281,6 +281,7 @@ export function AccessControlModalContent({
onChange={setSourceGroups}
values={sourceGroups}
saveGroupAssignments={useSave}
+ showResourceCounter={false}
/>
Learn more about
Access Controls
diff --git a/src/modules/networks/NetworkProvider.tsx b/src/modules/networks/NetworkProvider.tsx
index d214d05f..171f5a33 100644
--- a/src/modules/networks/NetworkProvider.tsx
+++ b/src/modules/networks/NetworkProvider.tsx
@@ -10,6 +10,7 @@ import { Network, NetworkResource, NetworkRouter } from "@/interfaces/Network";
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
import NetworkModal from "@/modules/networks/NetworkModal";
import NetworkResourceModal from "@/modules/networks/resources/NetworkResourceModal";
+import { ResourceGroupModal } from "@/modules/networks/resources/ResourceGroupModal";
import NetworkRoutingPeerModal from "@/modules/networks/routing-peers/NetworkRoutingPeerModal";
type Props = {
@@ -23,6 +24,10 @@ const NetworksContext = React.createContext(
openEditNetworkModal: (network: Network) => void;
openCreateNetworkModal: () => void;
openResourceModal: (network: Network, resource?: NetworkResource) => void;
+ openResourceGroupModal: (
+ network: Network,
+ resource?: NetworkResource,
+ ) => void;
openPolicyModal: (network?: Network, resource?: NetworkResource) => void;
deleteNetwork: (network: Network) => void;
deleteResource: (network: Network, resource: NetworkResource) => void;
@@ -49,6 +54,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
const [routingPeerModal, setRoutingPeerModal] = useState(false);
const [networkModal, setNetworkModal] = useState(false);
const [resourceModal, setResourceModal] = useState(false);
+ const [resourceGroupModal, setResourceGroupModal] = useState(false);
const [policyModal, setPolicyModal] = useState(false);
const openAddRoutingPeerModal = (
@@ -76,6 +82,15 @@ export const NetworkProvider = ({ children, network }: Props) => {
setResourceModal(true);
};
+ const openResourceGroupModal = (
+ network: Network,
+ resource?: NetworkResource,
+ ) => {
+ setCurrentNetwork(network);
+ resource && setCurrentResource(resource);
+ setResourceGroupModal(true);
+ };
+
const openPolicyModal = (network?: Network, resource?: NetworkResource) => {
setPolicyDefaultSettings({
destinationGroups: resource?.groups,
@@ -217,6 +232,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
openEditNetworkModal,
openCreateNetworkModal,
openResourceModal,
+ openResourceGroupModal,
openPolicyModal,
deleteNetwork,
deleteResource,
@@ -232,7 +248,7 @@ export const NetworkProvider = ({ children, network }: Props) => {
network={currentNetwork}
onCreated={async (network) => {
mutate("/networks");
- await askForRoutingPeer(network);
+ await askForResource(network);
}}
onUpdated={() => {
mutate("/networks");
@@ -250,13 +266,15 @@ export const NetworkProvider = ({ children, network }: Props) => {
initialDestinationGroups={policyDefaultSettings?.destinationGroups}
initialName={policyDefaultSettings?.name}
initialDescription={policyDefaultSettings?.description}
- onSuccess={(p) => {
+ onSuccess={async (p) => {
setPolicyModal(false);
setPolicyDefaultSettings(undefined);
mutate("/networks");
if (network) {
mutate(`/networks/${network.id}/resources`);
mutate(`/networks/${network.id}`);
+ } else {
+ currentNetwork && (await askForRoutingPeer(currentNetwork));
}
}}
/>
@@ -275,8 +293,6 @@ export const NetworkProvider = ({ children, network }: Props) => {
if (network) {
mutate(`/networks/${currentNetwork.id}/routers`);
mutate(`/networks/${network.id}`);
- } else {
- await askForResource(currentNetwork);
}
}}
onUpdated={async () => {
@@ -294,6 +310,26 @@ export const NetworkProvider = ({ children, network }: Props) => {
setRoutingPeerModal(state);
}}
/>
+
+ {
+ setCurrentResource(undefined);
+ setResourceGroupModal(state);
+ }}
+ onUpdated={() => {
+ setResourceGroupModal(false);
+ setCurrentResource(undefined);
+ mutate("/groups");
+ if (network) {
+ mutate(`/networks/${network.id}/resources`);
+ mutate(`/networks/${network.id}`);
+ }
+ }}
+ />
+
diff --git a/src/modules/networks/misc/NetworkNavigation.tsx b/src/modules/networks/misc/NetworkNavigation.tsx
index 590da45a..9d7ec8dd 100644
--- a/src/modules/networks/misc/NetworkNavigation.tsx
+++ b/src/modules/networks/misc/NetworkNavigation.tsx
@@ -1,27 +1,26 @@
import SidebarItem from "@components/SidebarItem";
+import { NewBadge } from "@components/ui/NewBadge";
import * as React from "react";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
-import { NetworkRoutesDeprecationInfo } from "@/modules/networks/misc/NetworkRoutesDeprecationInfo";
export const NetworkNavigation = () => {
return (
- }
- label="Networks"
- collapsible
- exactPathMatch={false}
- >
-
+ <>
}
label={
-
- Network Routes
-
+
+ Networks
+
}
- isChild
+ href={"/networks"}
+ />
+
}
href={"/network-routes"}
+ label={"Network Routes"}
/>
-
+ >
);
};
diff --git a/src/modules/networks/resources/NetworkResourceModal.tsx b/src/modules/networks/resources/NetworkResourceModal.tsx
index da2d1b81..08c86f9b 100644
--- a/src/modules/networks/resources/NetworkResourceModal.tsx
+++ b/src/modules/networks/resources/NetworkResourceModal.tsx
@@ -1,6 +1,7 @@
"use client";
import Button from "@components/Button";
+import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
@@ -17,7 +18,12 @@ import Paragraph from "@components/Paragraph";
import { PeerGroupSelector } from "@components/PeerGroupSelector";
import Separator from "@components/Separator";
import { useApiCall } from "@utils/api";
-import { ExternalLinkIcon, PlusCircle, WorkflowIcon } from "lucide-react";
+import {
+ ExternalLinkIcon,
+ PlusCircle,
+ Power,
+ WorkflowIcon,
+} from "lucide-react";
import React, { useMemo, useState } from "react";
import { Network, NetworkResource } from "@/interfaces/Network";
import useGroupHelper from "@/modules/groups/useGroupHelper";
@@ -79,6 +85,9 @@ export function ResourceModalContent({
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
initial: resource?.groups || [],
});
+ const [enabled, setEnabled] = useState
(
+ resource ? resource.enabled : true,
+ );
const createResource = async () => {
const savedGroups = await saveGroups();
@@ -91,6 +100,7 @@ export function ResourceModalContent({
description,
address,
groups: savedGroups.map((g) => g.id),
+ enabled,
}).then((r) => {
onCreated?.(r);
}),
@@ -108,6 +118,7 @@ export function ResourceModalContent({
description,
address,
groups: savedGroups.map((g) => g.id),
+ enabled,
}).then((r) => {
onUpdated?.(r);
}),
@@ -151,17 +162,34 @@ export function ResourceModalContent({
Assigned Groups
- Control access to this resource by assigning it to groups
+ Add this resource to groups and use them as destinations when
+ creating policies
+
+
+
+ Enable Resource
+ >
+ }
+ helpText={"Use this switch to enable or disable the resource."}
+ />
+
Learn more about
-
+
Resources
diff --git a/src/modules/networks/resources/ResourceActionCell.tsx b/src/modules/networks/resources/ResourceActionCell.tsx
index 39b5f944..aa1f93cf 100644
--- a/src/modules/networks/resources/ResourceActionCell.tsx
+++ b/src/modules/networks/resources/ResourceActionCell.tsx
@@ -1,5 +1,11 @@
import Button from "@components/Button";
-import { SquarePenIcon, Trash2 } from "lucide-react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@components/DropdownMenu";
+import { MoreVertical, SquarePenIcon, Trash2 } from "lucide-react";
import * as React from "react";
import { NetworkResource } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
@@ -11,29 +17,45 @@ export const ResourceActionCell = ({ resource }: Props) => {
const { deleteResource, network, openResourceModal } = useNetworksContext();
return (
-
-
{
- if (!network) return;
- openResourceModal(network, resource);
- }}
- >
-
- Edit
-
-
{
- if (!network) return;
- deleteResource(network, resource);
- }}
- >
-
- Remove
-
+
+
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ >
+
+
+
+
+
+ {
+ if (!network) return;
+ openResourceModal(network, resource);
+ }}
+ >
+
+
+ Edit
+
+
+ {
+ if (!network) return;
+ deleteResource(network, resource);
+ }}
+ variant={"danger"}
+ >
+
+
+ Remove
+
+
+
+
);
};
diff --git a/src/modules/networks/resources/ResourceEnabledCell.tsx b/src/modules/networks/resources/ResourceEnabledCell.tsx
new file mode 100644
index 00000000..dd24c119
--- /dev/null
+++ b/src/modules/networks/resources/ResourceEnabledCell.tsx
@@ -0,0 +1,58 @@
+import { notify } from "@components/Notification";
+import { ToggleSwitch } from "@components/ToggleSwitch";
+import { useApiCall } from "@utils/api";
+import * as React from "react";
+import { useMemo } from "react";
+import { useSWRConfig } from "swr";
+import { Group } from "@/interfaces/Group";
+import { NetworkResource } from "@/interfaces/Network";
+import { useNetworksContext } from "@/modules/networks/NetworkProvider";
+
+type Props = {
+ resource: NetworkResource;
+};
+export const ResourceEnabledCell = ({ resource }: Props) => {
+ const { mutate } = useSWRConfig();
+ const { network } = useNetworksContext();
+
+ const update = useApiCall
(
+ `/networks/${network?.id}/resources/${resource?.id}`,
+ ).put;
+
+ const toggle = async (enabled: boolean) => {
+ notify({
+ title: `Update Resource`,
+ description: `'${resource?.name}' is now ${
+ enabled ? "enabled" : "disabled"
+ }`,
+ loadingMessage: "Updating resource...",
+ duration: 1200,
+ promise: update({
+ ...resource,
+ groups: resource.groups
+ ?.map((g) => {
+ let group = g as Group;
+ return group.id;
+ })
+ .filter((g) => g !== undefined),
+ enabled,
+ }).then(() => {
+ mutate(`/networks/${network?.id}/resources`);
+ }),
+ });
+ };
+
+ const isChecked = useMemo(() => {
+ return resource.enabled;
+ }, [resource]);
+
+ return (
+
+ toggle(!isChecked)}
+ />
+
+ );
+};
diff --git a/src/modules/networks/resources/ResourceGroupCell.tsx b/src/modules/networks/resources/ResourceGroupCell.tsx
index 1b6c1d8b..752cbc24 100644
--- a/src/modules/networks/resources/ResourceGroupCell.tsx
+++ b/src/modules/networks/resources/ResourceGroupCell.tsx
@@ -2,14 +2,23 @@ import MultipleGroups from "@components/ui/MultipleGroups";
import * as React from "react";
import { Group } from "@/interfaces/Group";
import { NetworkResource } from "@/interfaces/Network";
+import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
resource?: NetworkResource;
};
export const ResourceGroupCell = ({ resource }: Props) => {
+ const { network, openResourceGroupModal } = useNetworksContext();
+
return (
-
+ {
+ if (!network) return;
+ openResourceGroupModal(network, resource);
+ }}
+ >
-
+
);
};
diff --git a/src/modules/networks/resources/ResourceGroupModal.tsx b/src/modules/networks/resources/ResourceGroupModal.tsx
new file mode 100644
index 00000000..2c44b3b8
--- /dev/null
+++ b/src/modules/networks/resources/ResourceGroupModal.tsx
@@ -0,0 +1,121 @@
+import Button from "@components/Button";
+import {
+ Modal,
+ ModalClose,
+ ModalContent,
+ ModalFooter,
+} from "@components/modal/Modal";
+import ModalHeader from "@components/modal/ModalHeader";
+import { notify } from "@components/Notification";
+import { PeerGroupSelector } from "@components/PeerGroupSelector";
+import Separator from "@components/Separator";
+import { useApiCall } from "@utils/api";
+import { FolderGit2 } from "lucide-react";
+import * as React from "react";
+import { useMemo } from "react";
+import { Network, NetworkResource } from "@/interfaces/Network";
+import useGroupHelper from "@/modules/groups/useGroupHelper";
+
+type ResourceGroupModalProps = {
+ resource?: NetworkResource;
+ network?: Network;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onUpdated?: (r: NetworkResource) => void;
+};
+export const ResourceGroupModal = ({
+ resource,
+ network,
+ open,
+ onOpenChange,
+ onUpdated,
+}: ResourceGroupModalProps) => {
+ return (
+
+ {network && resource && (
+
+ )}
+
+ );
+};
+
+type ModalProps = {
+ onUpdated?: (r: NetworkResource) => void;
+ network?: Network;
+ resource?: NetworkResource;
+};
+
+const ResourceGroupModalContent = ({
+ resource,
+ network,
+ onUpdated,
+}: ModalProps) => {
+ const update = useApiCall(
+ `/networks/${network?.id}/resources/${resource?.id}`,
+ ).put;
+
+ const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
+ initial: resource?.groups || [],
+ });
+
+ const updateResource = async () => {
+ const savedGroups = await saveGroups();
+ notify({
+ title: "Update Resource",
+ description: `'${resource?.name}' groups updated`,
+ loadingMessage: "Updating resource groups...",
+ promise: update({
+ ...resource,
+ groups: savedGroups.map((g) => g.id),
+ }).then((r) => {
+ onUpdated?.(r);
+ }),
+ });
+ };
+
+ const canSave = useMemo(() => {
+ return groups.length > 0;
+ }, [groups]);
+
+ return (
+
+ }
+ title={"Assigned Groups"}
+ description={
+ "Add this resource to groups and use them as destinations when creating policies"
+ }
+ color={"blue"}
+ />
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ Save Groups
+
+
+
+
+ );
+};
diff --git a/src/modules/networks/resources/ResourceNameCell.tsx b/src/modules/networks/resources/ResourceNameCell.tsx
index 17f2e8e7..90286b76 100644
--- a/src/modules/networks/resources/ResourceNameCell.tsx
+++ b/src/modules/networks/resources/ResourceNameCell.tsx
@@ -1,32 +1,52 @@
import DescriptionWithTooltip from "@components/ui/DescriptionWithTooltip";
+import TextWithTooltip from "@components/ui/TextWithTooltip";
import { cn } from "@utils/helpers";
import { GlobeIcon, NetworkIcon, WorkflowIcon } from "lucide-react";
import React from "react";
import { NetworkResource } from "@/interfaces/Network";
+import { useNetworksContext } from "@/modules/networks/NetworkProvider";
type Props = {
resource: NetworkResource;
};
export default function ResourceNameCell({ resource }: Readonly) {
+ const { network, openResourceModal } = useNetworksContext();
+
return (
-
+
{
+ if (!network) return;
+ openResourceModal(network, resource);
+ }}
+ >
{resource.type === "host" && }
{resource.type === "domain" && }
{resource.type === "subnet" && }
-
+
);
}
diff --git a/src/modules/networks/resources/ResourcePolicyCell.tsx b/src/modules/networks/resources/ResourcePolicyCell.tsx
index f0f0d2ff..e48c8cfa 100644
--- a/src/modules/networks/resources/ResourcePolicyCell.tsx
+++ b/src/modules/networks/resources/ResourcePolicyCell.tsx
@@ -22,13 +22,10 @@ export const ResourcePolicyCell = ({ resource }: Props) => {
const resourceGroups = resource?.groups as Group[];
return policies?.filter((policy) => {
if (!policy.enabled) return false;
- const sourcePolicyGroups = policy.rules
- ?.map((rule) => rule?.sources)
- .flat() as Group[];
const destinationPolicyGroups = policy.rules
?.map((rule) => rule?.destinations)
.flat() as Group[];
- const policyGroups = [...sourcePolicyGroups, ...destinationPolicyGroups];
+ const policyGroups = [...destinationPolicyGroups];
return resourceGroups.some((resourceGroup) =>
policyGroups.some((policyGroup) => policyGroup.id === resourceGroup.id),
);
@@ -89,7 +86,7 @@ export const ResourcePolicyCell = ({ resource }: Props) => {
openPolicyModal(network, resource)}
>
diff --git a/src/modules/networks/resources/ResourcesSection.tsx b/src/modules/networks/resources/ResourcesSection.tsx
index 80f30b74..bb87aef9 100644
--- a/src/modules/networks/resources/ResourcesSection.tsx
+++ b/src/modules/networks/resources/ResourcesSection.tsx
@@ -1,15 +1,12 @@
-import Button from "@components/Button";
import Paragraph from "@components/Paragraph";
import SkeletonTable, {
SkeletonTableHeader,
} from "@components/skeletons/SkeletonTable";
import { usePortalElement } from "@hooks/usePortalElement";
-import { IconCirclePlus } from "@tabler/icons-react";
import useFetchApi from "@utils/api";
import * as React from "react";
import { Suspense } from "react";
import { Network, NetworkResource } from "@/interfaces/Network";
-import { useNetworksContext } from "@/modules/networks/NetworkProvider";
import ResourcesTable from "@/modules/networks/resources/ResourcesTable";
type ResourcesSectionProps = {
@@ -23,27 +20,14 @@ export const ResourcesSection = ({ network }: ResourcesSectionProps) => {
const { ref: headingRef, portalTarget } =
usePortalElement();
- const { openResourceModal } = useNetworksContext();
-
return (
-
+
Resources
Add and manage resources for this network.
-
-
- openResourceModal(network)}
- >
-
- Add Resource
-
-
-
[] = [
{
id: "id",
- accessorKey: "id",
+ accessorKey: "name",
header: ({ column }) => {
return Resource ;
},
@@ -40,9 +46,20 @@ const NetworkResourceColumns: ColumnDef[] = [
return ;
},
},
+ {
+ id: "enabled",
+ accessorKey: "enabled",
+ header: ({ column }) => {
+ return Active ;
+ },
+ cell: ({ row }) => ,
+ },
{
id: "groups",
- accessorKey: "id",
+ accessorFn: (resource) => {
+ let groups = resource?.groups as Group[];
+ return groups.map((group) => group.name).join(", ");
+ },
header: ({ column }) => {
return Groups ;
},
@@ -74,40 +91,56 @@ export default function ResourcesTable({
resources,
isLoading,
headingTarget,
-}: Props) {
+}: Readonly) {
const [sorting, setSorting] = useState([]);
+ const { openResourceModal, network } = useNetworksContext();
return (
- <>
- }
- />
- }
- columnVisibility={{}}
- paginationPaddingClassName={"px-0 pt-8"}
- />
- >
+ }
+ />
+ }
+ columnVisibility={{}}
+ paginationPaddingClassName={"px-0 pt-8"}
+ rightSide={() => (
+ network && openResourceModal(network)}
+ >
+
+ Add Resource
+
+ )}
+ >
+ {(table) => (
+
+ )}
+
);
}
diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx
index 94dda20a..16651981 100644
--- a/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx
+++ b/src/modules/networks/routing-peers/NetworkRoutingPeerModal.tsx
@@ -24,18 +24,25 @@ import { cn } from "@utils/helpers";
import { uniqBy } from "lodash";
import {
ArrowDownWideNarrow,
+ DownloadIcon,
ExternalLinkIcon,
FolderGit2,
+ Loader2,
MonitorSmartphoneIcon,
PlusCircle,
+ Power,
Settings2,
Share2Icon,
VenetianMask,
} from "lucide-react";
import React, { useState } from "react";
+import { useSWRConfig } from "swr";
+import { useDialog } from "@/contexts/DialogProvider";
import { Network, NetworkRouter } from "@/interfaces/Network";
import { Peer } from "@/interfaces/Peer";
+import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
+import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
network: Network;
@@ -112,6 +119,9 @@ function RoutingPeerModalContent({
const [masquerade, setMasquerade] = useState(
router ? router.masquerade : true,
);
+ const [enabled, setEnabled] = useState(
+ router ? router.enabled : true,
+ );
const [metric, setMetric] = useState(
router?.metric ? router.metric.toString() : "9999",
);
@@ -137,6 +147,7 @@ function RoutingPeerModalContent({
? createdGroups.map((g) => g.id)
: undefined,
metric: parseInt(metric),
+ enabled,
masquerade,
}).then((r) => {
onCreated?.(r);
@@ -165,6 +176,7 @@ function RoutingPeerModalContent({
? createdGroups.map((g) => g.id)
: undefined,
metric: parseInt(metric),
+ enabled,
masquerade,
}).then((r) => {
onUpdated?.(r);
@@ -172,6 +184,10 @@ function RoutingPeerModalContent({
});
};
+ const [setupKeyModal, setSetupKeyModal] = useState(false);
+
+ const canContinue = routingPeer !== undefined || routingPeerGroups.length > 0;
+
return (
- Routers
+ Routing Peers
@@ -203,48 +219,86 @@ function RoutingPeerModalContent({
Advanced Settings
-
-
-
-
-
-
- Routing Peers
-
-
-
-
- Peer Group
-
-
-
-
-
- Assign a single or multiple peers as a routing peers for the
- network.
-
-
-
-
-
-
-
- Assign a peer group with Linux machines to be used as
- routing peers.
-
-
-
-
-
+
+
+
+
{
+ setType(state);
+ setRoutingPeer(undefined);
+ setRoutingPeerGroups([]);
+ }}
+ >
+
+
+
+ Routing Peers
+
+
+
+
+ Peer Group
+
+
+
+
+
+ Assign a single or multiple Linux peers as routing peers
+ for the network.
+
+
+
+
+
+
+
+ Assign a peer group with Linux machines to be used as
+ routing peers.
+
+
+
+
+
+
+
+
+
+ {"Don't have a routing peer?"}
+
+ You can install NetBird with a setup key on one or more Linux
+ machines to act as routing peers.
+
+
+
+
+
+
+ Enable Routing Peer
+ >
+ }
+ helpText={
+ "Use this switch to enable or disable the routing peer."
+ }
+ />
-
- Cancel
-
{tab == "router" && (
-
setTab("settings")}>
- Continue
-
+ <>
+
+ Cancel
+
+
setTab("settings")}
+ disabled={!canContinue}
+ >
+ Continue
+
+ >
)}
{tab == "settings" && (
-
- {router ? (
- <>Save Changes>
- ) : (
- <>
-
- Add Routing Peer
- >
- )}
-
+ <>
+
setTab("router")}>
+ Back
+
+
+
+ {router ? (
+ <>Save Changes>
+ ) : (
+ <>
+
+ Add Routing Peer
+ >
+ )}
+
+ >
)}
);
}
+
+type InstallNetBirdWithSetupKeyButtonProps = {
+ name?: string;
+};
+
+const InstallNetBirdWithSetupKeyButton = ({
+ name,
+}: InstallNetBirdWithSetupKeyButtonProps) => {
+ const setupKeyRequest = useApiCall("/setup-keys", true);
+ const { mutate } = useSWRConfig();
+ const { confirm } = useDialog();
+
+ const [installModal, setInstallModal] = useState(false);
+ const [setupKey, setSetupKey] = useState();
+ const [isLoading, setIsLoading] = useState(false);
+
+ const createSetupKey = async () => {
+ const choice = await confirm({
+ title: `Create a Setup Key?`,
+ description:
+ "If you continue, a one-off setup key will be automatically created and you will be able to install NetBird on a Linux machine.",
+ confirmText: "Continue",
+ cancelText: "Cancel",
+ type: "default",
+ });
+ if (!choice) return;
+
+ const loadingTimeout = setTimeout(() => setIsLoading(true), 1000);
+
+ await setupKeyRequest
+ .post({
+ name,
+ type: "one-off",
+ expires_in: 24 * 60 * 60, // 1 day expiration
+ revoked: false,
+ auto_groups: [],
+ usage_limit: 1,
+ ephemeral: false,
+ })
+ .then((setupKey) => {
+ setInstallModal(true);
+ setSetupKey(setupKey);
+ mutate("/setup-keys");
+ })
+ .finally(() => {
+ setIsLoading(false);
+ clearTimeout(loadingTimeout);
+ });
+ };
+
+ return (
+ <>
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+ Install NetBird
+
+ {setupKey && (
+
+
+
+ )}
+ >
+ );
+};
diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx
index 03511e2c..092f5064 100644
--- a/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx
+++ b/src/modules/networks/routing-peers/NetworkRoutingPeerName.tsx
@@ -44,13 +44,11 @@ export const NetworkRoutingPeerName = ({ router }: Props) => {
if (routingPeerGroup) {
return (
- <>
-
-
-
-
{routingPeerGroup.peers_count} Peer(s)
-
- >
+
+
+
+
{routingPeerGroup.peers_count} Peer(s)
+
);
}
};
diff --git a/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx b/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx
index fc4ca60d..9e801ad9 100644
--- a/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx
+++ b/src/modules/networks/routing-peers/NetworkRoutingPeersTable.tsx
@@ -1,14 +1,19 @@
+import Button from "@components/Button";
import Card from "@components/Card";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
+import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
+import { IconCirclePlus } from "@tabler/icons-react";
import { ColumnDef, SortingState } from "@tanstack/react-table";
import * as React from "react";
import { useState } from "react";
import PeerIcon from "@/assets/icons/PeerIcon";
import { NetworkRouter } from "@/interfaces/Network";
+import { useNetworksContext } from "@/modules/networks/NetworkProvider";
import { NetworkRoutingPeerName } from "@/modules/networks/routing-peers/NetworkRoutingPeerName";
import { RoutingPeersActionCell } from "@/modules/networks/routing-peers/RoutingPeersActionCell";
+import { RoutingPeersEnabledCell } from "@/modules/networks/routing-peers/RoutingPeersEnabledCell";
import { RoutingPeersMasqueradeCell } from "@/modules/networks/routing-peers/RoutingPeersMasqueradeCell";
import RouteMetricCell from "@/modules/routes/RouteMetricCell";
@@ -28,13 +33,23 @@ const NetworkRouterColumns: ColumnDef[] = [
sortingFn: "text",
cell: ({ row }) => ,
},
+ {
+ id: "enabled",
+ accessorKey: "enabled",
+ header: ({ column }) => {
+ return Active ;
+ },
+ cell: ({ row }) => ,
+ },
{
id: "metric",
accessorKey: "metric",
header: ({ column }) => {
return Metric ;
},
- cell: ({ row }) => ,
+ cell: ({ row }) => (
+
+ ),
},
{
id: "masquerade",
@@ -58,7 +73,9 @@ export default function NetworkRoutingPeersTable({
routers,
isLoading,
headingTarget,
-}: Props) {
+}: Readonly) {
+ const { openAddRoutingPeerModal, network } = useNetworksContext();
+
const [sorting, setSorting] = useState([
{
id: "metric",
@@ -69,7 +86,7 @@ export default function NetworkRoutingPeersTable({
return (
+ rightSide={() => (
+ network && openAddRoutingPeerModal(network)}
+ >
+
+ Add Routing Peer
+
+ )}
+ >
+ {(table) => (
+
+ )}
+
);
}
diff --git a/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx b/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx
index 91b9e001..3c64415c 100644
--- a/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx
+++ b/src/modules/networks/routing-peers/RoutingPeersActionCell.tsx
@@ -1,5 +1,11 @@
import Button from "@components/Button";
-import { SquarePenIcon, Trash2 } from "lucide-react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@components/DropdownMenu";
+import { MoreVertical, SquarePenIcon, Trash2 } from "lucide-react";
import * as React from "react";
import { NetworkRouter } from "@/interfaces/Network";
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
@@ -12,29 +18,45 @@ export const RoutingPeersActionCell = ({ router }: Props) => {
useNetworksContext();
return (
-
-
{
- if (!network) return;
- openAddRoutingPeerModal(network, router);
- }}
- >
-
- Edit
-
-
{
- if (!network) return;
- deleteRouter(network, router);
- }}
- >
-
- Remove
-
+
+
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ >
+
+
+
+
+
+ {
+ if (!network) return;
+ openAddRoutingPeerModal(network, router);
+ }}
+ >
+
+
+ Edit
+
+
+ {
+ if (!network) return;
+ deleteRouter(network, router);
+ }}
+ variant={"danger"}
+ >
+
+
+ Remove
+
+
+
+
);
};
diff --git a/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx b/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx
new file mode 100644
index 00000000..debd5925
--- /dev/null
+++ b/src/modules/networks/routing-peers/RoutingPeersEnabledCell.tsx
@@ -0,0 +1,48 @@
+import { notify } from "@components/Notification";
+import { ToggleSwitch } from "@components/ToggleSwitch";
+import { useApiCall } from "@utils/api";
+import * as React from "react";
+import { useMemo } from "react";
+import { useSWRConfig } from "swr";
+import { NetworkRouter } from "@/interfaces/Network";
+import { useNetworksContext } from "@/modules/networks/NetworkProvider";
+
+type Props = {
+ router: NetworkRouter;
+};
+export const RoutingPeersEnabledCell = ({ router }: Props) => {
+ const { mutate } = useSWRConfig();
+ const { network } = useNetworksContext();
+
+ const update = useApiCall
(
+ `/networks/${network?.id}/routers/${router?.id}`,
+ ).put;
+
+ const toggle = async (enabled: boolean) => {
+ notify({
+ title: "Network Routing Peer",
+ description: `Routing peer is now ${enabled ? "enabled" : "disabled"}`,
+ loadingMessage: "Updating routing peer...",
+ promise: update({
+ ...router,
+ enabled,
+ }).then(() => {
+ mutate(`/networks/${network?.id}/routers`);
+ }),
+ });
+ };
+
+ const isChecked = useMemo(() => {
+ return router.enabled;
+ }, [router]);
+
+ return (
+
+ toggle(!isChecked)}
+ />
+
+ );
+};
diff --git a/src/modules/networks/table/NetworksTable.tsx b/src/modules/networks/table/NetworksTable.tsx
index dda7f662..f3177a65 100644
--- a/src/modules/networks/table/NetworksTable.tsx
+++ b/src/modules/networks/table/NetworksTable.tsx
@@ -13,7 +13,6 @@ import { usePathname } from "next/navigation";
import React from "react";
import { useSWRConfig } from "swr";
import NetworkRoutesIcon from "@/assets/icons/NetworkRoutesIcon";
-import { useDialog } from "@/contexts/DialogProvider";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import { Network } from "@/interfaces/Network";
import {
@@ -38,14 +37,6 @@ export const NetworkTableColumns: ColumnDef[] = [
{
accessorKey: "description",
},
- {
- accessorKey: "routers",
- accessorFn: (network) => network?.routers?.length,
- header: ({ column }) => {
- return Routing Peers ;
- },
- cell: ({ row }) => ,
- },
{
accessorKey: "resources",
accessorFn: (network) => network?.resources?.length,
@@ -62,6 +53,14 @@ export const NetworkTableColumns: ColumnDef[] = [
},
cell: ({ row }) => ,
},
+ {
+ accessorKey: "routers",
+ accessorFn: (network) => network?.routers?.length,
+ header: ({ column }) => {
+ return Routing Peers ;
+ },
+ cell: ({ row }) => ,
+ },
{
accessorKey: "id",
header: "",
@@ -94,20 +93,6 @@ export default function NetworksTable({
],
);
- const { confirm } = useDialog();
-
- const showConfirm = async () => {
- const choice = await confirm({
- title: `Do you want to add a resource to 'Office Network' now?`,
- description:
- "Peers will be able to access your network resources once you add them.",
- confirmText: "Add Resource",
- cancelText: "Later",
- type: "default",
- });
- if (!choice) return;
- };
-
return (
Learn more about
Networks
diff --git a/src/modules/peers/PeersTable.tsx b/src/modules/peers/PeersTable.tsx
index 3f98c381..29f8a45e 100644
--- a/src/modules/peers/PeersTable.tsx
+++ b/src/modules/peers/PeersTable.tsx
@@ -63,7 +63,8 @@ const PeersTableColumns: ColumnDef[] = [
enableHiding: false,
},
{
- accessorKey: "name",
+ id: "name",
+ accessorFn: (peer) => `${peer?.name}${peer?.dns_label}`,
header: ({ column }) => {
return Name ;
},
@@ -94,6 +95,7 @@ const PeersTableColumns: ColumnDef[] = [
accessorFn: (peer) => (peer.user ? peer.user?.email : "Unknown"),
},
{
+ id: "dns_label",
accessorKey: "dns_label",
header: ({ column }) => {
return Address ;
@@ -193,6 +195,10 @@ export default function PeersTable({ peers, isLoading, headingTarget }: Props) {
id: "connected",
desc: true,
},
+ {
+ id: "name",
+ desc: false,
+ },
{
id: "last_seen",
desc: true,
diff --git a/src/modules/routes/RouteMetricCell.tsx b/src/modules/routes/RouteMetricCell.tsx
index 6b72677c..a805de93 100644
--- a/src/modules/routes/RouteMetricCell.tsx
+++ b/src/modules/routes/RouteMetricCell.tsx
@@ -3,11 +3,15 @@ import { ArrowUpDown, InfoIcon } from "lucide-react";
type Props = {
metric?: number;
+ useHoverStyle?: boolean;
};
-export default function RouteMetricCell({ metric }: Props) {
+export default function RouteMetricCell({
+ metric,
+ useHoverStyle = true,
+}: Readonly) {
return (
diff --git a/src/modules/settings/GroupsTable.tsx b/src/modules/settings/GroupsTable.tsx
index d58ab8c2..e95c0742 100644
--- a/src/modules/settings/GroupsTable.tsx
+++ b/src/modules/settings/GroupsTable.tsx
@@ -4,7 +4,7 @@ import DataTableHeader from "@components/table/DataTableHeader";
import { DataTableRowsPerPage } from "@components/table/DataTableRowsPerPage";
import NoResults from "@components/ui/NoResults";
import { ColumnDef, SortingState } from "@tanstack/react-table";
-import { FolderGit2Icon } from "lucide-react";
+import { FolderGit2Icon, Layers3Icon } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
@@ -138,6 +138,29 @@ export const GroupsTableColumns: ColumnDef[] = [
/>
),
},
+ {
+ accessorKey: "resources_count",
+ header: ({ column }) => {
+ return (
+ Network Resources
+ }
+ >
+
+
+ );
+ },
+ cell: ({ row }) => (
+ }
+ groupName={row.original.name}
+ text={"Network Resource(s)"}
+ count={row.original.resources_count}
+ />
+ ),
+ },
{
accessorKey: "users_count",
header: ({ column }) => {
@@ -172,7 +195,8 @@ export const GroupsTableColumns: ColumnDef[] = [
row.policies_count > 0 ||
row.routes_count > 0 ||
row.setup_keys_count > 0 ||
- row.users_count > 0
+ row.users_count > 0 ||
+ row.resources_count > 0
);
},
},
@@ -189,7 +213,7 @@ type Props = {
headingTarget?: HTMLHeadingElement | null;
};
-export default function GroupsTable({ headingTarget }: Props) {
+export default function GroupsTable({ headingTarget }: Readonly) {
const groups = useGroupsUsage();
const path = usePathname();
diff --git a/src/modules/settings/useGroupsUsage.tsx b/src/modules/settings/useGroupsUsage.tsx
index 6e085d56..0551addf 100644
--- a/src/modules/settings/useGroupsUsage.tsx
+++ b/src/modules/settings/useGroupsUsage.tsx
@@ -16,6 +16,7 @@ export interface GroupUsage {
routes_count: number;
setup_keys_count: number;
users_count: number;
+ resources_count: number;
}
export default function useGroupsUsage() {
@@ -126,6 +127,7 @@ export default function useGroupsUsage() {
id: group.id,
name: group.name,
peers_count: group.peers_count,
+ resources_count: group.resources_count,
policies_count: policyCount,
nameservers_count: nameserverCount,
routes_count: routeCount,
diff --git a/src/modules/setup-keys/SetupKeyModal.tsx b/src/modules/setup-keys/SetupKeyModal.tsx
index 3192cff1..97291d4a 100644
--- a/src/modules/setup-keys/SetupKeyModal.tsx
+++ b/src/modules/setup-keys/SetupKeyModal.tsx
@@ -23,7 +23,7 @@ import { cn } from "@utils/helpers";
import { trim } from "lodash";
import {
AlarmClock,
- CopyIcon,
+ DownloadIcon,
ExternalLinkIcon,
MonitorSmartphoneIcon,
PlusCircle,
@@ -32,25 +32,28 @@ import {
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SetupKeysIcon from "@/assets/icons/SetupKeysIcon";
-import useCopyToClipboard from "@/hooks/useCopyToClipboard";
import { SetupKey } from "@/interfaces/SetupKey";
import useGroupHelper from "@/modules/groups/useGroupHelper";
+import SetupModal from "@/modules/setup-netbird-modal/SetupModal";
type Props = {
children?: React.ReactNode;
open: boolean;
setOpen: (open: boolean) => void;
+ name?: string;
+ showOnlyRoutingPeerOS?: boolean;
};
const copyMessage = "Setup-Key was copied to your clipboard!";
export default function SetupKeyModal({
children,
open,
setOpen,
+ name,
+ showOnlyRoutingPeerOS,
}: Readonly) {
const [successModal, setSuccessModal] = useState(false);
const [setupKey, setSetupKey] = useState();
- const [, copy] = useCopyToClipboard(setupKey?.key);
-
+ const [installModal, setInstallModal] = useState(false);
const handleSuccess = (setupKey: SetupKey) => {
setSetupKey(setupKey);
setSuccessModal(true);
@@ -60,8 +63,24 @@ export default function SetupKeyModal({
<>
{children && {children} }
-
+
+
+ {
+ setInstallModal(state);
+ setOpen(false);
+ }}
+ key={installModal ? 2 : 3}
+ >
+
+
+
{
@@ -118,10 +137,10 @@ export default function SetupKeyModal({
variant={"primary"}
className={"w-full"}
data-cy={"setup-key-copy"}
- onClick={() => copy(copyMessage)}
+ onClick={() => setInstallModal(true)}
>
-
- Copy to clipboard
+
+ Install NetBird
@@ -133,13 +152,17 @@ export default function SetupKeyModal({
type ModalProps = {
onSuccess?: (setupKey: SetupKey) => void;
+ predefinedName?: string;
};
-export function SetupKeyModalContent({ onSuccess }: Readonly) {
+export function SetupKeyModalContent({
+ onSuccess,
+ predefinedName = "",
+}: Readonly) {
const setupKeyRequest = useApiCall("/setup-keys", true);
const { mutate } = useSWRConfig();
- const [name, setName] = useState("");
+ const [name, setName] = useState(predefinedName);
const [reusable, setReusable] = useState(false);
const [usageLimit, setUsageLimit] = useState("");
const [expiresIn, setExpiresIn] = useState("7");
diff --git a/src/modules/setup-netbird-modal/DockerTab.tsx b/src/modules/setup-netbird-modal/DockerTab.tsx
index eb35b000..e6cd059d 100644
--- a/src/modules/setup-netbird-modal/DockerTab.tsx
+++ b/src/modules/setup-netbird-modal/DockerTab.tsx
@@ -9,8 +9,17 @@ import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
+import { RoutingPeerSetupKeyInfo } from "@/modules/setup-netbird-modal/SetupModal";
-export default function DockerTab() {
+type Props = {
+ setupKey?: string;
+ showSetupKeyInfo?: boolean;
+};
+
+export default function DockerTab({
+ setupKey,
+ showSetupKeyInfo = false,
+}: Readonly) {
return (
@@ -35,14 +44,20 @@ export default function DockerTab() {
- Run NetBird container
+
+ Run NetBird container
+ {showSetupKeyInfo && }
+
docker run --rm -d \
--cap-add=NET_ADMIN \
{" "}
-e NB_SETUP_KEY=
- SETUP_KEY \
+
+ {setupKey ?? "SETUP_KEY"}
+ {" "}
+ \
-v netbird-client:/etc/netbird \
{GRPC_API_ORIGIN && (
diff --git a/src/modules/setup-netbird-modal/LinuxTab.tsx b/src/modules/setup-netbird-modal/LinuxTab.tsx
index 9fcadaae..581e82d5 100644
--- a/src/modules/setup-netbird-modal/LinuxTab.tsx
+++ b/src/modules/setup-netbird-modal/LinuxTab.tsx
@@ -13,8 +13,20 @@ import { getNetBirdUpCommand } from "@utils/netbird";
import { TerminalSquareIcon } from "lucide-react";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
+import {
+ RoutingPeerSetupKeyInfo,
+ SetupKeyParameter,
+} from "@/modules/setup-netbird-modal/SetupModal";
+
+type Props = {
+ setupKey?: string;
+ showSetupKeyInfo?: boolean;
+};
-export default function LinuxTab() {
+export default function LinuxTab({
+ setupKey,
+ showSetupKeyInfo = false,
+}: Readonly) {
return (
@@ -27,9 +39,15 @@ export default function LinuxTab() {
curl -fsSL https://pkgs.netbird.io/install.sh | sh
- Run NetBird and log in the browser
+
+ Run NetBird {!setupKey && "and log in the browser"}
+ {showSetupKeyInfo && }
+
- {getNetBirdUpCommand()}
+
+ {getNetBirdUpCommand()}
+
+
@@ -78,9 +96,15 @@ export default function LinuxTab() {
- Run NetBird and log in the browser
+
+ Run NetBird {!setupKey && "and log in the browser"}
+ {showSetupKeyInfo && }
+
- {getNetBirdUpCommand()}
+
+ {getNetBirdUpCommand()}
+
+
diff --git a/src/modules/setup-netbird-modal/MacOSTab.tsx b/src/modules/setup-netbird-modal/MacOSTab.tsx
index a9b5964c..7e4f1525 100644
--- a/src/modules/setup-netbird-modal/MacOSTab.tsx
+++ b/src/modules/setup-netbird-modal/MacOSTab.tsx
@@ -23,8 +23,12 @@ import {
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
+import { SetupKeyParameter } from "@/modules/setup-netbird-modal/SetupModal";
-export default function MacOSTab() {
+type Props = {
+ setupKey?: string;
+};
+export default function MacOSTab({ setupKey }: Readonly
) {
return (
@@ -98,15 +102,29 @@ export default function MacOSTab() {
)}
-
-
- {/* eslint-disable-next-line react/no-unescaped-entities */}
- Click on "Connect" from the NetBird icon in your system tray
-
-
-
- Sign up using your email address
-
+ {setupKey ? (
+
+ Open Terminal and run NetBird
+
+
+ {getNetBirdUpCommand()}
+
+
+
+
+ ) : (
+ <>
+
+
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
+ Click on "Connect" from the NetBird icon in your system tray
+
+
+
+ Sign up using your email address
+
+ >
+ )}
@@ -125,9 +143,12 @@ export default function MacOSTab() {
- Run NetBird and log in the browser
+ Run NetBird {!setupKey && "and log in the browser"}
- {getNetBirdUpCommand()}
+
+ {getNetBirdUpCommand()}
+
+
@@ -179,9 +200,12 @@ export default function MacOSTab() {
- Run NetBird and log in the browser
+ Run NetBird {!setupKey && "and log in the browser"}
- {getNetBirdUpCommand()}
+
+ {getNetBirdUpCommand()}
+
+
diff --git a/src/modules/setup-netbird-modal/SetupModal.tsx b/src/modules/setup-netbird-modal/SetupModal.tsx
index e7a4d591..99923104 100644
--- a/src/modules/setup-netbird-modal/SetupModal.tsx
+++ b/src/modules/setup-netbird-modal/SetupModal.tsx
@@ -5,6 +5,7 @@ import { ModalContent, ModalFooter } from "@components/modal/Modal";
import Paragraph from "@components/Paragraph";
import SmallParagraph from "@components/SmallParagraph";
import { Tabs, TabsList, TabsTrigger } from "@components/Tabs";
+import { cn } from "@utils/helpers";
import { ExternalLinkIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import React from "react";
@@ -31,27 +32,44 @@ type OidcUserInfo = {
type Props = {
showClose?: boolean;
user?: OidcUserInfo;
+ setupKey?: string;
+ showOnlyRoutingPeerOS?: boolean;
};
-export default function SetupModal({ showClose = true, user }: Props) {
+export default function SetupModal({
+ showClose = true,
+ user,
+ setupKey,
+ showOnlyRoutingPeerOS = false,
+}: Readonly) {
return (
-
+
);
}
+type SetupModalContentProps = {
+ user?: OidcUserInfo;
+ header?: boolean;
+ footer?: boolean;
+ tabAlignment?: "center" | "start" | "end";
+ setupKey?: string;
+ showOnlyRoutingPeerOS?: boolean;
+};
+
export function SetupModalContent({
user,
header = true,
footer = true,
tabAlignment = "center",
-}: {
- user?: OidcUserInfo;
- header?: boolean;
- footer?: boolean;
- tabAlignment?: "center" | "start" | "end";
-}) {
+ setupKey,
+ showOnlyRoutingPeerOS,
+}: Readonly) {
const os = useOperatingSystem();
const [isFirstRun] = useLocalStorage("netbird-first-run", true);
const pathname = usePathname();
@@ -60,24 +78,33 @@ export function SetupModalContent({
return (
<>
{header && (
-
-
+
+
{isFirstRun && !isInstallPage ? (
<>
Hello {user?.given_name || "there"}! 👋
{`It's time to add your first device.`}
>
) : (
- <>Install NetBird>
+ <>Install NetBird{setupKey && " with Setup Key"}>
)}
-
- To get started, install NetBird and log in with your email account.
+
+ {setupKey
+ ? "To get started, install and run NetBird with the setup key as a parameter."
+ : "To get started, install NetBird and log in with your email account."}
)}
-
+
Linux
-
-
- Windows
-
-
-
- macOS
-
-
-
- iOS
-
-
-
- Android
-
+
+ {!showOnlyRoutingPeerOS && (
+ <>
+
+
+ Windows
+
+
+
+ macOS
+
+ >
+ )}
+
+ {!setupKey && (
+ <>
+
+
+ iOS
+
+
+
+ Android
+
+ >
+ )}
+
-
-
-
-
-
-
+
+
+
+
+
+ {!setupKey && (
+ <>
+
+
+ >
+ )}
+
+
{footer && (
@@ -158,3 +209,32 @@ export function SetupModalContent({
>
);
}
+
+type SetupKeyParameterProps = {
+ setupKey?: string;
+};
+
+export const SetupKeyParameter = ({ setupKey }: SetupKeyParameterProps) => {
+ return (
+ setupKey && (
+ <>
+ {" "}
+ --setup-key {setupKey}
+ >
+ )
+ );
+};
+
+export const RoutingPeerSetupKeyInfo = () => {
+ return (
+
+ This setup key can be used only once within the next 24 hours.
+
+ When expired, the same key can not be used again.
+
+ );
+};
diff --git a/src/modules/setup-netbird-modal/WindowsTab.tsx b/src/modules/setup-netbird-modal/WindowsTab.tsx
index 49bc7d0a..1a6a01ba 100644
--- a/src/modules/setup-netbird-modal/WindowsTab.tsx
+++ b/src/modules/setup-netbird-modal/WindowsTab.tsx
@@ -2,13 +2,18 @@ import Button from "@components/Button";
import Code from "@components/Code";
import Steps from "@components/Steps";
import TabsContentPadding, { TabsContent } from "@components/Tabs";
-import { GRPC_API_ORIGIN } from "@utils/netbird";
+import { getNetBirdUpCommand, GRPC_API_ORIGIN } from "@utils/netbird";
import { DownloadIcon, PackageOpenIcon } from "lucide-react";
import Link from "next/link";
import React from "react";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
+import { SetupKeyParameter } from "@/modules/setup-netbird-modal/SetupModal";
-export default function WindowsTab() {
+type Props = {
+ setupKey?: string;
+};
+
+export default function WindowsTab({ setupKey }: Readonly) {
return (
@@ -44,15 +49,29 @@ export default function WindowsTab() {
)}
-
-
- {/* eslint-disable-next-line react/no-unescaped-entities */}
- Click on "Connect" from the NetBird icon in your system tray
-
-
-
- Sign up using your email address
-
+ {setupKey ? (
+
+ Open Command-line and run NetBird
+
+
+ {getNetBirdUpCommand()}
+
+
+
+
+ ) : (
+ <>
+
+
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
+ Click on "Connect" from the NetBird icon in your system tray
+
+
+
+ Sign up using your email address
+
+ >
+ )}