Add improvements to new networks features (#439)
Some checks failed
build and push / build_n_push (push) Has been cancelled
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Fix wrong ui state for routing peer modal in networks * Add confirmation dialog when blocking users * Keep peer sort order when switching pages * Update sidebar navigation order and remove deprecation notice * Fix issue when hovering over truncated text in a group badge closes the multiple groups popover * Update group text in network resource modal * Update networks page text * Fix line height * Add search to resource table * Switch networks flow to create first resources and then add routers * Add enabled toggle to routing peers * Add enabled toggle to network resources * Add resource group modal and adjust tables * Clarify networks * Fix not properly aligned horizontal scroll bar * Add option to install netbird after creating a setup key * Fix text for install netbird modal * Show resources count in group settings * Fix "no results" and "no routing peers" text showing at the same time * Fix wording * Fix resource policy count * Hide resource count when selection source groups * Extend networks routing peer modal with option to create a setup key and install netbird * Add option for horizontal stepper * Generate setup key when installing netbird from routing peer modal * Add confirm dialog to let the user know a one-off setup-key will be created. This avoids accidental clicking and later confusion on the setup keys page --------- Co-authored-by: Misha Bragin <bangvalo@gmail.com>
This commit is contained in:
347
package-lock.json
generated
347
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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={<NetworkRoutesIcon size={13} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>
|
||||
Network Routes <NetworkRoutesDeprecationInfo size={18} />
|
||||
</h1>
|
||||
<h1 ref={headingRef}>Network Routes</h1>
|
||||
<Paragraph>
|
||||
Network routes allow you to access other networks like LANs and
|
||||
VPCs without installing NetBird on every resource.
|
||||
|
||||
@@ -31,12 +31,15 @@ export default function Networks() {
|
||||
</Breadcrumbs>
|
||||
<h1 ref={headingRef}>Networks</h1>
|
||||
<Paragraph>
|
||||
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.
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
Learn more about
|
||||
<InlineLink href={"https://docs.netbird.io/how-to/networks"} target={"_blank"}>
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Networks
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
|
||||
@@ -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<MultiSelectProps>) {
|
||||
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({
|
||||
<AccessControlGroupCount group_id={option.id} />
|
||||
)}
|
||||
|
||||
<ResourcesCounter group={option} />
|
||||
{showResourceCounter && (
|
||||
<ResourcesCounter group={option} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
|
||||
@@ -166,15 +166,17 @@ export function PeerSelector({
|
||||
placeholder={"Search for peers by name or ip..."}
|
||||
/>
|
||||
|
||||
{unfilteredItems.length == 0 && (
|
||||
<DropdownInfoText>
|
||||
{
|
||||
"Seems like you don't have any linux peers to assign as a routing peer."
|
||||
}
|
||||
</DropdownInfoText>
|
||||
{unfilteredItems.length == 0 && !search && (
|
||||
<div className={"max-w-xs mx-auto"}>
|
||||
<DropdownInfoText>
|
||||
{
|
||||
"Seems like you don't have any Linux peers to assign as a routing peer."
|
||||
}
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredItems.length == 0 && (
|
||||
{filteredItems.length == 0 && search != "" && (
|
||||
<DropdownInfoText>
|
||||
There are no peers matching your search.
|
||||
</DropdownInfoText>
|
||||
|
||||
@@ -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) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative will-change-scroll webkit-scroll",
|
||||
className,
|
||||
"overflow-hidden",
|
||||
)}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
{withoutViewport ? (
|
||||
children
|
||||
) : (
|
||||
<ScrollAreaViewport disableOverflowY={false}>
|
||||
{children}
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaViewport>{children}</ScrollAreaViewport>
|
||||
)}
|
||||
<ScrollBar />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
<ScrollBar orientation="vertical" />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
type AdditionalScrollAreaViewportProps = {
|
||||
disableOverflowY?: boolean;
|
||||
};
|
||||
|
||||
const ScrollAreaViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport> &
|
||||
AdditionalScrollAreaViewportProps
|
||||
>(({ disableOverflowY = true, ...props }, ref) => {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={ref}
|
||||
className="h-full w-full rounded-[inherit] will-change-scroll webkit-scroll"
|
||||
{...props}
|
||||
style={
|
||||
disableOverflowY ? { overflowY: undefined, ...props.style } : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
ref={ref}
|
||||
className={cn("h-full w-full rounded-[inherit]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ScrollAreaViewport.displayName = ScrollAreaPrimitive.Viewport.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
@@ -63,14 +46,11 @@ const ScrollBar = React.forwardRef<
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
style={{ boxSizing: "unset", overflow: undefined }}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 border-t border-t-transparent p-[1px]",
|
||||
"flex select-none touch-none transition-colors",
|
||||
orientation === "vertical" && "h-full w-2.5 p-[1px]",
|
||||
orientation === "horizontal" && "w-full h-2.5 p-[1px] bottom-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -79,6 +59,7 @@ const ScrollBar = React.forwardRef<
|
||||
className={cn(
|
||||
"relative rounded-full bg-neutral-200 dark:bg-nb-gray-800",
|
||||
orientation === "vertical" && "flex-1",
|
||||
orientation === "horizontal" && "h-full",
|
||||
)}
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
|
||||
@@ -60,10 +60,12 @@ export default function SidebarItem({
|
||||
<li className={"px-4 cursor-pointer"}>
|
||||
<button
|
||||
className={classNames(
|
||||
"rounded-lg text-[.95rem] w-full ",
|
||||
"rounded-lg text-[.87rem] w-full ",
|
||||
"font-normal ",
|
||||
className,
|
||||
isChild ? "pl-7 pr-2 py-2 mt-1 mb-0.5" : "py-2 px-3",
|
||||
isChild
|
||||
? "pl-7 pr-2 py-[.45rem] mt-1 mb-0.5"
|
||||
: "py-[.45rem] px-3",
|
||||
isActive
|
||||
? "text-gray-900 bg-gray-200 dark:text-white dark:bg-nb-gray-900"
|
||||
: "text-gray-600 hover:bg-gray-200 dark:text-nb-gray-400 dark:hover:bg-nb-gray-900/50",
|
||||
|
||||
@@ -4,9 +4,18 @@ import React from "react";
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
horizontal?: boolean;
|
||||
};
|
||||
export default function Steps({ children, className }: Props) {
|
||||
return <div className={cn("pt-4", className)}>{children}</div>;
|
||||
export default function Steps({
|
||||
children,
|
||||
className,
|
||||
horizontal = false,
|
||||
}: Readonly<Props>) {
|
||||
return (
|
||||
<div className={cn("pt-4", horizontal && "flex", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-4 items-start min-w-full justify-start relative pb-6 -mx-1.5 group px-[2px]",
|
||||
"flex gap-4 items-start justify-start relative pb-6 -mx-1.5 group px-[2px]",
|
||||
center && "items-center",
|
||||
horizontal ? "flex-col items-center" : "min-w-full",
|
||||
)}
|
||||
>
|
||||
{line && (
|
||||
<span
|
||||
className={
|
||||
"h-full w-[2px] bg-nb-gray-100 dark:bg-nb-gray-800 absolute left-0 ml-[18px] z-0 transition-all"
|
||||
}
|
||||
className={cn(
|
||||
"bg-nb-gray-100 dark:bg-nb-gray-800 z-0 transition-all",
|
||||
horizontal
|
||||
? "w-full h-[2px] absolute mt-[16px] transform translate-x-1/2"
|
||||
: "h-full w-[2px] absolute left-0 ml-[18px]",
|
||||
)}
|
||||
></span>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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({
|
||||
>
|
||||
<FolderGit2 size={12} className={"shrink-0"} />
|
||||
|
||||
<TextWithTooltip text={group?.name || ""} maxChars={20} />
|
||||
<TruncatedText text={group?.name || ""} maxChars={20} />
|
||||
{children}
|
||||
{isNew && showNewBadge && (
|
||||
<span
|
||||
className={
|
||||
"text-[7px] relative top-[.25px] leading-[0] bg-green-900 border border-green-500/20 py-1.5 px-1 rounded-[3px] text-green-400"
|
||||
}
|
||||
>
|
||||
NEW
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isNew && showNewBadge && <NewBadge />}
|
||||
{showX && (
|
||||
<XIcon
|
||||
size={12}
|
||||
|
||||
16
src/components/ui/NewBadge.tsx
Normal file
16
src/components/ui/NewBadge.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as React from "react";
|
||||
|
||||
type Props = {
|
||||
text?: string;
|
||||
};
|
||||
export const NewBadge = ({ text = "NEW" }: Props) => {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
"text-[7px] relative top-[.25px] leading-[0] bg-green-900 border border-green-500/20 py-1.5 px-1 rounded-[3px] text-green-400"
|
||||
}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
78
src/components/ui/TruncatedText.tsx
Normal file
78
src/components/ui/TruncatedText.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={"w-full min-w-0 inline-block"}
|
||||
style={{
|
||||
maxWidth: `${maxChars - 2}ch`,
|
||||
}}
|
||||
>
|
||||
<div className={cn(className, "truncate")}>{text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HoverCard.Root
|
||||
openDelay={650}
|
||||
closeDelay={100}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<HoverCard.Trigger asChild={true}>
|
||||
<div
|
||||
className={"w-full min-w-0 inline-block"}
|
||||
style={{
|
||||
maxWidth: `${maxChars - 2}ch`,
|
||||
}}
|
||||
>
|
||||
<div className={cn(className, "truncate")}>{text}</div>
|
||||
</div>
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content
|
||||
onMouseLeave={() => 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",
|
||||
)}
|
||||
>
|
||||
<div className={"text-neutral-300 flex flex-col gap-1"}>
|
||||
<div className={"max-w-xs break-all whitespace-normal text-xs"}>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
</HoverCard.Root>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -281,6 +281,7 @@ export function AccessControlModalContent({
|
||||
onChange={setSourceGroups}
|
||||
values={sourceGroups}
|
||||
saveGroupAssignments={useSave}
|
||||
showResourceCounter={false}
|
||||
/>
|
||||
</div>
|
||||
<PolicyDirection
|
||||
@@ -391,9 +392,7 @@ export function AccessControlModalContent({
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-network-access"
|
||||
}
|
||||
href={"https://docs.netbird.io/how-to/manage-network-access"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Access Controls
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ResourceGroupModal
|
||||
network={currentNetwork}
|
||||
resource={currentResource}
|
||||
open={resourceGroupModal}
|
||||
onOpenChange={(state) => {
|
||||
setCurrentResource(undefined);
|
||||
setResourceGroupModal(state);
|
||||
}}
|
||||
onUpdated={() => {
|
||||
setResourceGroupModal(false);
|
||||
setCurrentResource(undefined);
|
||||
mutate("/groups");
|
||||
if (network) {
|
||||
mutate(`/networks/${network.id}/resources`);
|
||||
mutate(`/networks/${network.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<NetworkResourceModal
|
||||
network={currentNetwork}
|
||||
resource={currentResource}
|
||||
|
||||
@@ -60,10 +60,7 @@ export const NetworkInformationSquare = ({
|
||||
{name}
|
||||
</p>
|
||||
<DescriptionWithTooltip
|
||||
className={cn(
|
||||
"text-left",
|
||||
size == "lg" && "text-md leading-none mt-0.5",
|
||||
)}
|
||||
className={cn("text-left", size == "lg" && "text-md mt-0.5")}
|
||||
maxChars={24}
|
||||
text={description}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
label="Networks"
|
||||
collapsible
|
||||
exactPathMatch={false}
|
||||
>
|
||||
<SidebarItem label="Networks" isChild href={"/networks"} />
|
||||
<>
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
label={
|
||||
<div className={"flex items-center"}>
|
||||
Network Routes
|
||||
<NetworkRoutesDeprecationInfo />
|
||||
<div className={"flex items-center gap-2"}>
|
||||
Networks
|
||||
<NewBadge />
|
||||
</div>
|
||||
}
|
||||
isChild
|
||||
href={"/network-routes"}
|
||||
href={"/networks"}
|
||||
/>
|
||||
</SidebarItem>
|
||||
<SidebarItem
|
||||
icon={<NetworkRoutesIcon />}
|
||||
href={"/network-routes"}
|
||||
label={"Network Routes"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<boolean>(
|
||||
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({
|
||||
<div>
|
||||
<Label>Assigned Groups</Label>
|
||||
<HelpText>
|
||||
Control access to this resource by assigning it to groups
|
||||
Add this resource to groups and use them as destinations when
|
||||
creating policies
|
||||
</HelpText>
|
||||
<PeerGroupSelector onChange={setGroups} values={groups} />
|
||||
</div>
|
||||
<div className={"mt-3"}>
|
||||
<FancyToggleSwitch
|
||||
value={enabled}
|
||||
onChange={setEnabled}
|
||||
label={
|
||||
<>
|
||||
<Power size={15} />
|
||||
Enable Resource
|
||||
</>
|
||||
}
|
||||
helpText={"Use this switch to enable or disable the resource."}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink href={"https://docs.netbird.io/how-to/networks#resources"} target={"_blank"}>
|
||||
<InlineLink
|
||||
href={"https://docs.netbird.io/how-to/networks#resources"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Resources
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
|
||||
@@ -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 (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
<Button
|
||||
variant={"default-outline"}
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openResourceModal(network, resource);
|
||||
}}
|
||||
>
|
||||
<SquarePenIcon size={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
deleteResource(network, resource);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Remove
|
||||
</Button>
|
||||
<div className={"flex justify-end"}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
asChild={true}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button variant={"secondary"} className={"!px-3"}>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openResourceModal(network, resource);
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SquarePenIcon size={14} className={"shrink-0"} />
|
||||
Edit
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
deleteResource(network, resource);
|
||||
}}
|
||||
variant={"danger"}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
Remove
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
58
src/modules/networks/resources/ResourceEnabledCell.tsx
Normal file
58
src/modules/networks/resources/ResourceEnabledCell.tsx
Normal file
@@ -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<NetworkResource>(
|
||||
`/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 (
|
||||
<div className={"flex"}>
|
||||
<ToggleSwitch
|
||||
checked={isChecked}
|
||||
size={"small"}
|
||||
onClick={() => toggle(!isChecked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className={"flex"}>
|
||||
<button
|
||||
className={"flex cursor-pointer"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openResourceGroupModal(network, resource);
|
||||
}}
|
||||
>
|
||||
<MultipleGroups groups={resource?.groups as Group[]} />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
121
src/modules/networks/resources/ResourceGroupModal.tsx
Normal file
121
src/modules/networks/resources/ResourceGroupModal.tsx
Normal file
@@ -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 (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
{network && resource && (
|
||||
<ResourceGroupModalContent
|
||||
network={network}
|
||||
resource={resource}
|
||||
onUpdated={onUpdated}
|
||||
key={open ? "1" : "0"}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
type ModalProps = {
|
||||
onUpdated?: (r: NetworkResource) => void;
|
||||
network?: Network;
|
||||
resource?: NetworkResource;
|
||||
};
|
||||
|
||||
const ResourceGroupModalContent = ({
|
||||
resource,
|
||||
network,
|
||||
onUpdated,
|
||||
}: ModalProps) => {
|
||||
const update = useApiCall<NetworkResource>(
|
||||
`/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 (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
<ModalHeader
|
||||
icon={<FolderGit2 size={18} />}
|
||||
title={"Assigned Groups"}
|
||||
description={
|
||||
"Add this resource to groups and use them as destinations when creating policies"
|
||||
}
|
||||
color={"blue"}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className={"px-8 py-6 flex flex-col gap-8"}>
|
||||
<div>
|
||||
<PeerGroupSelector onChange={setGroups} values={groups} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={updateResource}
|
||||
disabled={!canSave}
|
||||
>
|
||||
Save Groups
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
};
|
||||
@@ -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<Props>) {
|
||||
const { network, openResourceModal } = useNetworksContext();
|
||||
|
||||
return (
|
||||
<div className={"flex gap-4 items-center"}>
|
||||
<button
|
||||
className={"flex gap-4 items-center group"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openResourceModal(network, resource);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md h-9 w-9 shrink-0 bg-nb-gray-900 transition-all",
|
||||
"group-hover:bg-nb-gray-800",
|
||||
)}
|
||||
>
|
||||
{resource.type === "host" && <WorkflowIcon size={15} />}
|
||||
{resource.type === "domain" && <GlobeIcon size={15} />}
|
||||
{resource.type === "subnet" && <NetworkIcon size={15} />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0 dark:text-neutral-300 text-neutral-500 font-light truncate">
|
||||
<span className={"font-normal truncate"}>{resource.name}</span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-0 text-neutral-300 font-light truncate",
|
||||
"group-hover:text-neutral-100",
|
||||
)}
|
||||
>
|
||||
<TextWithTooltip
|
||||
text={resource.name}
|
||||
maxChars={25}
|
||||
className={"font-normal"}
|
||||
/>
|
||||
<DescriptionWithTooltip
|
||||
className={cn("font-normal mt-0.5 ")}
|
||||
text={resource.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
<Button
|
||||
size={"xs"}
|
||||
variant={"secondary"}
|
||||
className={"min-w-[110px]"}
|
||||
className={"min-w-[100px]"}
|
||||
onClick={() => openPolicyModal(network, resource)}
|
||||
>
|
||||
<PlusCircle size={12} />
|
||||
|
||||
@@ -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<HTMLHeadingElement>();
|
||||
|
||||
const { openResourceModal } = useNetworksContext();
|
||||
|
||||
return (
|
||||
<div className={"py-7 px-8"}>
|
||||
<div className={"max-w-6xl"}>
|
||||
<div className={"flex justify-between items-center"}>
|
||||
<div className={"flex justify-between items-center mb-6"}>
|
||||
<div>
|
||||
<h2 ref={headingRef}>Resources</h2>
|
||||
<Paragraph>Add and manage resources for this network.</Paragraph>
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => openResourceModal(network)}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Add Resource
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Suspense
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
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 { Layers3Icon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { NetworkResource } from "@/interfaces/Network";
|
||||
import { useNetworksContext } from "@/modules/networks/NetworkProvider";
|
||||
import { ResourceActionCell } from "@/modules/networks/resources/ResourceActionCell";
|
||||
import ResourceAddressCell from "@/modules/networks/resources/ResourceAddressCell";
|
||||
import { ResourceEnabledCell } from "@/modules/networks/resources/ResourceEnabledCell";
|
||||
import { ResourceGroupCell } from "@/modules/networks/resources/ResourceGroupCell";
|
||||
import ResourceNameCell from "@/modules/networks/resources/ResourceNameCell";
|
||||
import { ResourcePolicyCell } from "@/modules/networks/resources/ResourcePolicyCell";
|
||||
@@ -22,7 +28,7 @@ type Props = {
|
||||
const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Resource</DataTableHeader>;
|
||||
},
|
||||
@@ -40,9 +46,20 @@ const NetworkResourceColumns: ColumnDef<NetworkResource>[] = [
|
||||
return <ResourceAddressCell resource={row.original} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Active</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <ResourceEnabledCell resource={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "groups",
|
||||
accessorKey: "id",
|
||||
accessorFn: (resource) => {
|
||||
let groups = resource?.groups as Group[];
|
||||
return groups.map((group) => group.name).join(", ");
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Groups</DataTableHeader>;
|
||||
},
|
||||
@@ -74,40 +91,56 @@ export default function ResourcesTable({
|
||||
resources,
|
||||
isLoading,
|
||||
headingTarget,
|
||||
}: Props) {
|
||||
}: Readonly<Props>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const { openResourceModal, network } = useNetworksContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
wrapperComponent={Card}
|
||||
wrapperProps={{ className: "mt-6 w-full" }}
|
||||
headingTarget={headingTarget}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
minimal={true}
|
||||
showSearchAndFilters={false}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={"Peers"}
|
||||
columns={NetworkResourceColumns}
|
||||
keepStateInLocalStorage={false}
|
||||
data={resources}
|
||||
searchPlaceholder={"Search by name, IP, owner or group..."}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={"This network has no resources"}
|
||||
description={
|
||||
"Add resources to this network to control what peers can access. Resources can be anything from a single IP, a subnet, or a domain."
|
||||
}
|
||||
icon={<Layers3Icon size={20} />}
|
||||
/>
|
||||
}
|
||||
columnVisibility={{}}
|
||||
paginationPaddingClassName={"px-0 pt-8"}
|
||||
/>
|
||||
</>
|
||||
<DataTable
|
||||
wrapperComponent={Card}
|
||||
wrapperProps={{ className: "mt-6 pb-2 w-full" }}
|
||||
headingTarget={headingTarget}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
minimal={true}
|
||||
showSearchAndFilters={true}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={"Resources"}
|
||||
columns={NetworkResourceColumns}
|
||||
keepStateInLocalStorage={false}
|
||||
data={resources}
|
||||
searchPlaceholder={"Search by name, address or group..."}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
className={"py-4"}
|
||||
title={"This network has no resources"}
|
||||
description={
|
||||
"Add resources to this network to control what peers can access. Resources can be anything from a single IP, a subnet, or a domain."
|
||||
}
|
||||
icon={<Layers3Icon size={20} />}
|
||||
/>
|
||||
}
|
||||
columnVisibility={{}}
|
||||
paginationPaddingClassName={"px-0 pt-8"}
|
||||
rightSide={() => (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => network && openResourceModal(network)}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Add Resource
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={!resources || resources?.length == 0}
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<boolean>(
|
||||
router ? router.masquerade : true,
|
||||
);
|
||||
const [enabled, setEnabled] = useState<boolean>(
|
||||
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 (
|
||||
<ModalContent maxWidthClass={"max-w-xl"}>
|
||||
<ModalHeader
|
||||
@@ -190,7 +206,7 @@ function RoutingPeerModalContent({
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Routers
|
||||
Routing Peers
|
||||
</TabsTrigger>
|
||||
|
||||
<TabsTrigger value={"settings"} className={"ml-auto"}>
|
||||
@@ -203,48 +219,86 @@ function RoutingPeerModalContent({
|
||||
Advanced Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value={"router"} className={"pb-8"}>
|
||||
<div className={"flex flex-col gap-4 px-8 "}>
|
||||
<SegmentedTabs value={type} onChange={setType}>
|
||||
<SegmentedTabs.List>
|
||||
<SegmentedTabs.Trigger value={"peer"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Routing Peers
|
||||
</SegmentedTabs.Trigger>
|
||||
<TabsContent value={"router"} className={"pb-6"}>
|
||||
<div className={"flex flex-col gap-4 px-8"}>
|
||||
<div className={"relative "}>
|
||||
<SegmentedTabs
|
||||
value={type}
|
||||
onChange={(state) => {
|
||||
setType(state);
|
||||
setRoutingPeer(undefined);
|
||||
setRoutingPeerGroups([]);
|
||||
}}
|
||||
>
|
||||
<SegmentedTabs.List>
|
||||
<SegmentedTabs.Trigger value={"peer"}>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Routing Peers
|
||||
</SegmentedTabs.Trigger>
|
||||
|
||||
<SegmentedTabs.Trigger value={"group"}>
|
||||
<FolderGit2 size={16} />
|
||||
Peer Group
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
<SegmentedTabs.Content value={"peer"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a single or multiple peers as a routing peers for the
|
||||
network.
|
||||
</HelpText>
|
||||
<PeerSelector onChange={setRoutingPeer} value={routingPeer} />
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
<SegmentedTabs.Content value={"group"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a peer group with Linux machines to be used as
|
||||
routing peers.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
max={1}
|
||||
onChange={setRoutingPeerGroups}
|
||||
values={routingPeerGroups}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
</SegmentedTabs>
|
||||
<SegmentedTabs.Trigger value={"group"}>
|
||||
<FolderGit2 size={16} />
|
||||
Peer Group
|
||||
</SegmentedTabs.Trigger>
|
||||
</SegmentedTabs.List>
|
||||
<SegmentedTabs.Content value={"peer"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a single or multiple Linux peers as routing peers
|
||||
for the network.
|
||||
</HelpText>
|
||||
<PeerSelector
|
||||
onChange={setRoutingPeer}
|
||||
value={routingPeer}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
<SegmentedTabs.Content value={"group"}>
|
||||
<div>
|
||||
<HelpText>
|
||||
Assign a peer group with Linux machines to be used as
|
||||
routing peers.
|
||||
</HelpText>
|
||||
<PeerGroupSelector
|
||||
max={1}
|
||||
onChange={setRoutingPeerGroups}
|
||||
values={routingPeerGroups}
|
||||
/>
|
||||
</div>
|
||||
</SegmentedTabs.Content>
|
||||
</SegmentedTabs>
|
||||
</div>
|
||||
|
||||
<div className={cn("flex justify-between items-center mt-3")}>
|
||||
<div>
|
||||
<Label>{"Don't have a routing peer?"}</Label>
|
||||
<HelpText className={""}>
|
||||
You can install NetBird with a setup key on one or more Linux
|
||||
machines to act as routing peers.
|
||||
</HelpText>
|
||||
</div>
|
||||
<InstallNetBirdWithSetupKeyButton
|
||||
name={`Routing Peer (${network.name})`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"settings"} className={"pb-4"}>
|
||||
<div className={"px-8 flex flex-col gap-6"}>
|
||||
<FancyToggleSwitch
|
||||
value={enabled}
|
||||
onChange={setEnabled}
|
||||
label={
|
||||
<>
|
||||
<Power size={15} />
|
||||
Enable Routing Peer
|
||||
</>
|
||||
}
|
||||
helpText={
|
||||
"Use this switch to enable or disable the routing peer."
|
||||
}
|
||||
/>
|
||||
<FancyToggleSwitch
|
||||
value={masquerade}
|
||||
onChange={setMasquerade}
|
||||
@@ -302,34 +356,128 @@ function RoutingPeerModalContent({
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
{tab == "router" && (
|
||||
<Button variant={"primary"} onClick={() => setTab("settings")}>
|
||||
Continue
|
||||
</Button>
|
||||
<>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
onClick={() => setTab("settings")}
|
||||
disabled={!canContinue}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{tab == "settings" && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={
|
||||
routingPeer == undefined && routingPeerGroups.length <= 0
|
||||
}
|
||||
onClick={router ? updateRouter : addRouter}
|
||||
>
|
||||
{router ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Routing Peer
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<>
|
||||
<Button variant={"secondary"} onClick={() => setTab("router")}>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={
|
||||
routingPeer == undefined && routingPeerGroups.length <= 0
|
||||
}
|
||||
onClick={router ? updateRouter : addRouter}
|
||||
>
|
||||
{router ? (
|
||||
<>Save Changes</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle size={16} />
|
||||
Add Routing Peer
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
type InstallNetBirdWithSetupKeyButtonProps = {
|
||||
name?: string;
|
||||
};
|
||||
|
||||
const InstallNetBirdWithSetupKeyButton = ({
|
||||
name,
|
||||
}: InstallNetBirdWithSetupKeyButtonProps) => {
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/setup-keys", true);
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const [installModal, setInstallModal] = useState(false);
|
||||
const [setupKey, setSetupKey] = useState<SetupKey>();
|
||||
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 (
|
||||
<>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
className={"ml-8"}
|
||||
onClick={createSetupKey}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={14} className={"animate-spin delay-1000"} />
|
||||
) : (
|
||||
<DownloadIcon size={14} />
|
||||
)}
|
||||
Install NetBird
|
||||
</Button>
|
||||
{setupKey && (
|
||||
<Modal
|
||||
open={installModal}
|
||||
onOpenChange={setInstallModal}
|
||||
key={setupKey.key}
|
||||
>
|
||||
<SetupModal
|
||||
showClose={true}
|
||||
setupKey={setupKey.key}
|
||||
showOnlyRoutingPeerOS={true}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,13 +44,11 @@ export const NetworkRoutingPeerName = ({ router }: Props) => {
|
||||
|
||||
if (routingPeerGroup) {
|
||||
return (
|
||||
<>
|
||||
<div className={"flex items-center gap-2 max-w-[295px] min-w-[295px]"}>
|
||||
<GroupBadge group={routingPeerGroup} />
|
||||
<ArrowRightIcon size={14} className={"shrink-0"} />
|
||||
<PeerBadge> {routingPeerGroup.peers_count} Peer(s)</PeerBadge>
|
||||
</div>
|
||||
</>
|
||||
<div className={"flex items-center gap-2 max-w-[295px] min-w-[295px]"}>
|
||||
<GroupBadge group={routingPeerGroup} />
|
||||
<ArrowRightIcon size={14} className={"shrink-0"} />
|
||||
<PeerBadge> {routingPeerGroup.peers_count} Peer(s)</PeerBadge>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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<NetworkRouter>[] = [
|
||||
sortingFn: "text",
|
||||
cell: ({ row }) => <NetworkRoutingPeerName router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "enabled",
|
||||
accessorKey: "enabled",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Active</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <RoutingPeersEnabledCell router={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "metric",
|
||||
accessorKey: "metric",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Metric</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <RouteMetricCell metric={row.original.metric} />,
|
||||
cell: ({ row }) => (
|
||||
<RouteMetricCell metric={row.original.metric} useHoverStyle={false} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "masquerade",
|
||||
@@ -58,7 +73,9 @@ export default function NetworkRoutingPeersTable({
|
||||
routers,
|
||||
isLoading,
|
||||
headingTarget,
|
||||
}: Props) {
|
||||
}: Readonly<Props>) {
|
||||
const { openAddRoutingPeerModal, network } = useNetworksContext();
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: "metric",
|
||||
@@ -69,7 +86,7 @@ export default function NetworkRoutingPeersTable({
|
||||
return (
|
||||
<DataTable
|
||||
wrapperComponent={Card}
|
||||
wrapperProps={{ className: "mt-6 w-full" }}
|
||||
wrapperProps={{ className: "mt-6 pb-2 w-full" }}
|
||||
headingTarget={headingTarget}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
@@ -77,11 +94,11 @@ export default function NetworkRoutingPeersTable({
|
||||
showSearchAndFilters={false}
|
||||
inset={false}
|
||||
tableClassName={"mt-0"}
|
||||
text={"Peers"}
|
||||
text={"Routing Peers"}
|
||||
columns={NetworkRouterColumns}
|
||||
keepStateInLocalStorage={false}
|
||||
data={routers}
|
||||
searchPlaceholder={"Search by name, IP, owner or group..."}
|
||||
searchPlaceholder={"Search by name..."}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={
|
||||
<NoResults
|
||||
@@ -95,6 +112,23 @@ export default function NetworkRoutingPeersTable({
|
||||
}
|
||||
columnVisibility={{}}
|
||||
paginationPaddingClassName={"px-0 pt-8"}
|
||||
/>
|
||||
rightSide={() => (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"ml-auto"}
|
||||
onClick={() => network && openAddRoutingPeerModal(network)}
|
||||
>
|
||||
<IconCirclePlus size={16} />
|
||||
Add Routing Peer
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
{(table) => (
|
||||
<DataTableRowsPerPage
|
||||
table={table}
|
||||
disabled={!routers || routers?.length == 0}
|
||||
/>
|
||||
)}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={"flex justify-end pr-4"}>
|
||||
<Button
|
||||
variant={"default-outline"}
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openAddRoutingPeerModal(network, router);
|
||||
}}
|
||||
>
|
||||
<SquarePenIcon size={16} />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={"danger-outline"}
|
||||
size={"sm"}
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
deleteRouter(network, router);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Remove
|
||||
</Button>
|
||||
<div className={"flex justify-end"}>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger
|
||||
asChild={true}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Button variant={"secondary"} className={"!px-3"}>
|
||||
<MoreVertical size={16} className={"shrink-0"} />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
openAddRoutingPeerModal(network, router);
|
||||
}}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<SquarePenIcon size={14} className={"shrink-0"} />
|
||||
Edit
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
if (!network) return;
|
||||
deleteRouter(network, router);
|
||||
}}
|
||||
variant={"danger"}
|
||||
>
|
||||
<div className={"flex gap-3 items-center"}>
|
||||
<Trash2 size={14} className={"shrink-0"} />
|
||||
Remove
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<NetworkRouter>(
|
||||
`/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 (
|
||||
<div className={"flex"}>
|
||||
<ToggleSwitch
|
||||
checked={isChecked}
|
||||
size={"small"}
|
||||
onClick={() => toggle(!isChecked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<Network>[] = [
|
||||
{
|
||||
accessorKey: "description",
|
||||
},
|
||||
{
|
||||
accessorKey: "routers",
|
||||
accessorFn: (network) => network?.routers?.length,
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Routing Peers</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <NetworkRoutingPeerCell network={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "resources",
|
||||
accessorFn: (network) => network?.resources?.length,
|
||||
@@ -62,6 +53,14 @@ export const NetworkTableColumns: ColumnDef<Network>[] = [
|
||||
},
|
||||
cell: ({ row }) => <NetworkPolicyCell network={row.original} />,
|
||||
},
|
||||
{
|
||||
accessorKey: "routers",
|
||||
accessorFn: (network) => network?.routers?.length,
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Routing Peers</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <NetworkRoutingPeerCell network={row.original} />,
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<NetworkProvider>
|
||||
<DataTable
|
||||
@@ -146,9 +131,7 @@ export default function NetworksTable({
|
||||
<>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/networks"
|
||||
}
|
||||
href={"https://docs.netbird.io/how-to/networks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Networks
|
||||
|
||||
@@ -63,7 +63,8 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
id: "name",
|
||||
accessorFn: (peer) => `${peer?.name}${peer?.dns_label}`,
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Name</DataTableHeader>;
|
||||
},
|
||||
@@ -94,6 +95,7 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
|
||||
accessorFn: (peer) => (peer.user ? peer.user?.email : "Unknown"),
|
||||
},
|
||||
{
|
||||
id: "dns_label",
|
||||
accessorKey: "dns_label",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Address</DataTableHeader>;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Props>) {
|
||||
return (
|
||||
<FullTooltip
|
||||
hoverButton={true}
|
||||
hoverButton={useHoverStyle}
|
||||
isAction={true}
|
||||
content={
|
||||
<div className={"text-xs max-w-xs flex gap-2 items-center"}>
|
||||
|
||||
@@ -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<GroupUsage>[] = [
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "resources_count",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<DataTableHeader
|
||||
column={column}
|
||||
tooltip={
|
||||
<div className={"text-sm normal-case"}>Network Resources</div>
|
||||
}
|
||||
>
|
||||
<Layers3Icon size={12} />
|
||||
</DataTableHeader>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<GroupsCountCell
|
||||
icon={<Layers3Icon size={10} />}
|
||||
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<GroupUsage>[] = [
|
||||
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<Props>) {
|
||||
const groups = useGroupsUsage();
|
||||
const path = usePathname();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Props>) {
|
||||
const [successModal, setSuccessModal] = useState(false);
|
||||
const [setupKey, setSetupKey] = useState<SetupKey>();
|
||||
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({
|
||||
<>
|
||||
<Modal open={open} onOpenChange={setOpen} key={open ? 1 : 0}>
|
||||
{children && <ModalTrigger asChild>{children}</ModalTrigger>}
|
||||
<SetupKeyModalContent onSuccess={handleSuccess} />
|
||||
<SetupKeyModalContent onSuccess={handleSuccess} predefinedName={name} />
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={installModal}
|
||||
onOpenChange={(state) => {
|
||||
setInstallModal(state);
|
||||
setOpen(false);
|
||||
}}
|
||||
key={installModal ? 2 : 3}
|
||||
>
|
||||
<SetupModal
|
||||
showClose={true}
|
||||
setupKey={setupKey?.key}
|
||||
showOnlyRoutingPeerOS={showOnlyRoutingPeerOS}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={successModal}
|
||||
onOpenChange={(open) => {
|
||||
@@ -118,10 +137,10 @@ export default function SetupKeyModal({
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
data-cy={"setup-key-copy"}
|
||||
onClick={() => copy(copyMessage)}
|
||||
onClick={() => setInstallModal(true)}
|
||||
>
|
||||
<CopyIcon size={14} />
|
||||
Copy to clipboard
|
||||
<DownloadIcon size={14} />
|
||||
Install NetBird
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
@@ -133,13 +152,17 @@ export default function SetupKeyModal({
|
||||
|
||||
type ModalProps = {
|
||||
onSuccess?: (setupKey: SetupKey) => void;
|
||||
predefinedName?: string;
|
||||
};
|
||||
|
||||
export function SetupKeyModalContent({ onSuccess }: Readonly<ModalProps>) {
|
||||
export function SetupKeyModalContent({
|
||||
onSuccess,
|
||||
predefinedName = "",
|
||||
}: Readonly<ModalProps>) {
|
||||
const setupKeyRequest = useApiCall<SetupKey>("/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");
|
||||
|
||||
@@ -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<Props>) {
|
||||
return (
|
||||
<TabsContent value={String(OperatingSystem.DOCKER)}>
|
||||
<TabsContentPadding>
|
||||
@@ -35,14 +44,20 @@ export default function DockerTab() {
|
||||
</div>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={2}>
|
||||
<p>Run NetBird container</p>
|
||||
<p>
|
||||
Run NetBird container
|
||||
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
|
||||
</p>
|
||||
<Code>
|
||||
<Code.Line>docker run --rm -d \</Code.Line>
|
||||
<Code.Line> --cap-add=NET_ADMIN \</Code.Line>
|
||||
<Code.Line>
|
||||
{" "}
|
||||
-e NB_SETUP_KEY=
|
||||
<span className={"text-netbird"}>SETUP_KEY</span> \
|
||||
<span className={"text-netbird"}>
|
||||
{setupKey ?? "SETUP_KEY"}
|
||||
</span>{" "}
|
||||
\
|
||||
</Code.Line>
|
||||
<Code.Line> -v netbird-client:/etc/netbird \</Code.Line>
|
||||
{GRPC_API_ORIGIN && (
|
||||
|
||||
@@ -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";
|
||||
|
||||
export default function LinuxTab() {
|
||||
type Props = {
|
||||
setupKey?: string;
|
||||
showSetupKeyInfo?: boolean;
|
||||
};
|
||||
|
||||
export default function LinuxTab({
|
||||
setupKey,
|
||||
showSetupKeyInfo = false,
|
||||
}: Readonly<Props>) {
|
||||
return (
|
||||
<TabsContent value={String(OperatingSystem.LINUX)}>
|
||||
<TabsContentPadding>
|
||||
@@ -27,9 +39,15 @@ export default function LinuxTab() {
|
||||
<Code>curl -fsSL https://pkgs.netbird.io/install.sh | sh</Code>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={2} line={false}>
|
||||
<p>Run NetBird and log in the browser</p>
|
||||
<p>
|
||||
Run NetBird {!setupKey && "and log in the browser"}
|
||||
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
|
||||
</p>
|
||||
<Code>
|
||||
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
@@ -78,9 +96,15 @@ export default function LinuxTab() {
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={3} line={false}>
|
||||
<p>Run NetBird and log in the browser</p>
|
||||
<p>
|
||||
Run NetBird {!setupKey && "and log in the browser"}
|
||||
{showSetupKeyInfo && <RoutingPeerSetupKeyInfo />}
|
||||
</p>
|
||||
<Code>
|
||||
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
|
||||
@@ -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<Props>) {
|
||||
return (
|
||||
<TabsContent value={String(OperatingSystem.APPLE)}>
|
||||
<TabsContentPadding>
|
||||
@@ -98,15 +102,29 @@ export default function MacOSTab() {
|
||||
</Steps.Step>
|
||||
)}
|
||||
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
|
||||
<p>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
Click on "Connect" from the NetBird icon in your system tray
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
|
||||
<p>Sign up using your email address</p>
|
||||
</Steps.Step>
|
||||
{setupKey ? (
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2} line={false}>
|
||||
<p>Open Terminal and run NetBird</p>
|
||||
<Code>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
) : (
|
||||
<>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
|
||||
<p>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
Click on "Connect" from the NetBird icon in your system tray
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
|
||||
<p>Sign up using your email address</p>
|
||||
</Steps.Step>
|
||||
</>
|
||||
)}
|
||||
</Steps>
|
||||
</TabsContentPadding>
|
||||
<Separator />
|
||||
@@ -125,9 +143,12 @@ export default function MacOSTab() {
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={2} line={false}>
|
||||
<p>Run NetBird and log in the browser</p>
|
||||
<p>Run NetBird {!setupKey && "and log in the browser"}</p>
|
||||
<Code>
|
||||
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
@@ -179,9 +200,12 @@ export default function MacOSTab() {
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={4} line={false}>
|
||||
<p>Run NetBird and log in the browser</p>
|
||||
<p>Run NetBird {!setupKey && "and log in the browser"}</p>
|
||||
<Code>
|
||||
<Code.Line>{getNetBirdUpCommand()}</Code.Line>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
|
||||
@@ -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<Props>) {
|
||||
return (
|
||||
<ModalContent showClose={showClose}>
|
||||
<SetupModalContent user={user} />
|
||||
<SetupModalContent
|
||||
user={user}
|
||||
setupKey={setupKey}
|
||||
showOnlyRoutingPeerOS={showOnlyRoutingPeerOS}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
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<SetupModalContentProps>) {
|
||||
const os = useOperatingSystem();
|
||||
const [isFirstRun] = useLocalStorage<boolean>("netbird-first-run", true);
|
||||
const pathname = usePathname();
|
||||
@@ -60,24 +78,33 @@ export function SetupModalContent({
|
||||
return (
|
||||
<>
|
||||
{header && (
|
||||
<div className={"text-center pb-8 pt-4 px-8"}>
|
||||
<h2 className={"text-3xl max-w-lg mx-auto"}>
|
||||
<div className={"text-center pb-5 pt-4 px-8"}>
|
||||
<h2
|
||||
className={cn(
|
||||
"max-w-lg mx-auto",
|
||||
setupKey ? "text-2xl" : "text-3xl",
|
||||
)}
|
||||
>
|
||||
{isFirstRun && !isInstallPage ? (
|
||||
<>
|
||||
Hello {user?.given_name || "there"}! 👋 <br />
|
||||
{`It's time to add your first device.`}
|
||||
</>
|
||||
) : (
|
||||
<>Install NetBird</>
|
||||
<>Install NetBird{setupKey && " with Setup Key"}</>
|
||||
)}
|
||||
</h2>
|
||||
<Paragraph className={"max-w-xs mx-auto mt-3"}>
|
||||
To get started, install NetBird and log in with your email account.
|
||||
<Paragraph
|
||||
className={cn("mx-auto mt-3", setupKey ? "max-w-sm" : "max-w-xs")}
|
||||
>
|
||||
{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."}
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue={String(os)}>
|
||||
<Tabs defaultValue={String(setupKey ? OperatingSystem.LINUX : os)}>
|
||||
<TabsList justify={tabAlignment} className={"pt-2 px-3"}>
|
||||
<TabsTrigger value={String(OperatingSystem.LINUX)}>
|
||||
<ShellIcon
|
||||
@@ -87,38 +114,49 @@ export function SetupModalContent({
|
||||
/>
|
||||
Linux
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
|
||||
<WindowsIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Windows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.APPLE)}>
|
||||
<AppleIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
macOS
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.IOS)}>
|
||||
<IOSIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
iOS
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
|
||||
<AndroidIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Android
|
||||
</TabsTrigger>
|
||||
|
||||
{!showOnlyRoutingPeerOS && (
|
||||
<>
|
||||
<TabsTrigger value={String(OperatingSystem.WINDOWS)}>
|
||||
<WindowsIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Windows
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.APPLE)}>
|
||||
<AppleIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
macOS
|
||||
</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!setupKey && (
|
||||
<>
|
||||
<TabsTrigger value={String(OperatingSystem.IOS)}>
|
||||
<IOSIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
iOS
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={String(OperatingSystem.ANDROID)}>
|
||||
<AndroidIcon
|
||||
className={
|
||||
"fill-nb-gray-500 group-data-[state=active]/trigger:fill-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Android
|
||||
</TabsTrigger>
|
||||
</>
|
||||
)}
|
||||
|
||||
<TabsTrigger value={String(OperatingSystem.DOCKER)}>
|
||||
<DockerIcon
|
||||
className={
|
||||
@@ -128,12 +166,25 @@ export function SetupModalContent({
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<LinuxTab />
|
||||
<WindowsTab />
|
||||
<MacOSTab />
|
||||
<AndroidTab />
|
||||
<IOSTab />
|
||||
<DockerTab />
|
||||
|
||||
<LinuxTab
|
||||
setupKey={setupKey}
|
||||
showSetupKeyInfo={showOnlyRoutingPeerOS}
|
||||
/>
|
||||
<WindowsTab setupKey={setupKey} />
|
||||
<MacOSTab setupKey={setupKey} />
|
||||
|
||||
{!setupKey && (
|
||||
<>
|
||||
<AndroidTab />
|
||||
<IOSTab />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DockerTab
|
||||
setupKey={setupKey}
|
||||
showSetupKeyInfo={showOnlyRoutingPeerOS}
|
||||
/>
|
||||
</Tabs>
|
||||
{footer && (
|
||||
<ModalFooter variant={"setup"}>
|
||||
@@ -158,3 +209,32 @@ export function SetupModalContent({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type SetupKeyParameterProps = {
|
||||
setupKey?: string;
|
||||
};
|
||||
|
||||
export const SetupKeyParameter = ({ setupKey }: SetupKeyParameterProps) => {
|
||||
return (
|
||||
setupKey && (
|
||||
<>
|
||||
{" "}
|
||||
--setup-key <span className={"text-netbird"}>{setupKey}</span>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const RoutingPeerSetupKeyInfo = () => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex gap-2 mt-1 items-center text-xs text-nb-gray-300 font-normal mb-1"
|
||||
}
|
||||
>
|
||||
This setup key can be used only once within the next 24 hours.
|
||||
<br />
|
||||
When expired, the same key can not be used again.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Props>) {
|
||||
return (
|
||||
<TabsContent value={String(OperatingSystem.WINDOWS)}>
|
||||
<TabsContentPadding>
|
||||
@@ -44,15 +49,29 @@ export default function WindowsTab() {
|
||||
</Steps.Step>
|
||||
)}
|
||||
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
|
||||
<p>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
Click on "Connect" from the NetBird icon in your system tray
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
|
||||
<p>Sign up using your email address</p>
|
||||
</Steps.Step>
|
||||
{setupKey ? (
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2} line={false}>
|
||||
<p>Open Command-line and run NetBird</p>
|
||||
<Code>
|
||||
<Code.Line>
|
||||
{getNetBirdUpCommand()}
|
||||
<SetupKeyParameter setupKey={setupKey} />
|
||||
</Code.Line>
|
||||
</Code>
|
||||
</Steps.Step>
|
||||
) : (
|
||||
<>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 3 : 2}>
|
||||
<p>
|
||||
{/* eslint-disable-next-line react/no-unescaped-entities */}
|
||||
Click on "Connect" from the NetBird icon in your system tray
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={GRPC_API_ORIGIN ? 4 : 3} line={false}>
|
||||
<p>Sign up using your email address</p>
|
||||
</Steps.Step>
|
||||
</>
|
||||
)}
|
||||
</Steps>
|
||||
</TabsContentPadding>
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user