mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d4e491611 | ||
|
|
9b1f920863 | ||
|
|
7c7f0a0f10 | ||
|
|
76541c701c |
No files matched your search
Generated
+74
-68
@@ -513,9 +513,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz",
|
||||
"integrity": "sha512-PAmWhJfJQlP+kxZwCjrVd9QnR5x0R3u0mTXTiZDgSd4h5LdXmjxCCWbN9kq6hkZBOax8Rm3xDW5HagWyJuT37g==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.32.tgz",
|
||||
"integrity": "sha512-n9mQdigI6iZ/DF6pCTwMKeWgF2e8lg7qgt5M7HXMLtyhZYMnf/u905M18sSpPmHL9MKp9JHo56C6jrD2EvWxng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
@@ -529,9 +529,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -578,9 +578,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.28.tgz",
|
||||
"integrity": "sha512-kzGChl9setxYWpk3H6fTZXXPFFjg7urptLq5o5ZgYezCrqlemKttwMT5iFyx/p1e/JeglTwDFRtb923gTJ3R1w==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.32.tgz",
|
||||
"integrity": "sha512-osHXveM70zC+ilfuFa/2W6a1XQxJTvEhzEycnjUaVE8kpUS09lDpiDDX2YLdyFCzoUbvbo5r0X1Kp4MllIOShw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -594,9 +594,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.28.tgz",
|
||||
"integrity": "sha512-z6FXYHDJlFOzVEOiiJ/4NG8aLCeayZdcRSMjPDysW297Up6r22xw6Ea9AOwQqbNsth8JNgIK8EkWz2IDwaLQcw==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.32.tgz",
|
||||
"integrity": "sha512-P9NpCAJuOiaHHpqtrCNncjqtSBi1f6QUdHK/+dNabBIXB2RUFWL19TY1Hkhu74OvyNQEYEzzMJCMQk5agjw1Qg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -610,9 +610,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.28.tgz",
|
||||
"integrity": "sha512-9ARHLEQXhAilNJ7rgQX8xs9aH3yJSj888ssSjJLeldiZKR4D7N08MfMqljk77fAwZsWwsrp8ohHsMvurvv9liQ==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.32.tgz",
|
||||
"integrity": "sha512-v7JaO0oXXt6d+cFjrrKqYnR2ubrD+JYP7nQVRZgeo5uNE5hkCpWnHmXm9vy3g6foMO8SPwL0P3MPw1c+BjbAzA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -626,9 +626,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.28.tgz",
|
||||
"integrity": "sha512-p6gvatI1nX41KCizEe6JkF0FS/cEEF0u23vKDpl+WhPe/fCTBeGkEBh7iW2cUM0rvquPVwPWdiUR6Ebr/kQWxQ==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.32.tgz",
|
||||
"integrity": "sha512-tA6sIKShXtSJBTH88i0DRd6I9n3ZTirmwpwAqH5zdJoQF7/wlJXR8DkPmKwYl5mFWhEKr5IIa3LfpMW9RRwKmQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -642,9 +642,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.28.tgz",
|
||||
"integrity": "sha512-nsiSnz2wO6GwMAX2o0iucONlVL7dNgKUqt/mDTATGO2NY59EO/ZKnKEr80BJFhuA5UC1KZOMblJHWZoqIJddpA==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.32.tgz",
|
||||
"integrity": "sha512-7S1GY4TdnlGVIdeXXKQdDkfDysoIVFMD0lJuVVMeb3eoVjrknQ0JNN7wFlhCvea0hEk0Sd4D1hedVChDKfV2jw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -658,9 +658,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.28.tgz",
|
||||
"integrity": "sha512-+IuGQKoI3abrXFqx7GtlvNOpeExUH1mTIqCrh1LGFf8DnlUcTmOOCApEnPJUSLrSbzOdsF2ho2KhnQoO0I1RDw==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.32.tgz",
|
||||
"integrity": "sha512-OHHC81P4tirVa6Awk6eCQ6RBfWl8HpFsZtfEkMpJ5GjPsJ3nhPe6wKAJUZ/piC8sszUkAgv3fLflgzPStIwfWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -674,9 +674,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.28.tgz",
|
||||
"integrity": "sha512-l61WZ3nevt4BAnGksUVFKy2uJP5DPz2E0Ma/Oklvo3sGj9sw3q7vBWONFRgz+ICiHpW5mV+mBrkB3XEubMrKaA==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.32.tgz",
|
||||
"integrity": "sha512-rORQjXsAFeX6TLYJrCG5yoIDj+NKq31Rqwn8Wpn/bkPNy5rTHvOXkW8mLFonItS7QC6M+1JIIcLe+vOCTOYpvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -690,9 +690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.28.tgz",
|
||||
"integrity": "sha512-+Kcp1T3jHZnJ9v9VTJ/yf1t/xmtFAc/Sge4v7mVc1z+NYfYzisi8kJ9AsY8itbgq+WgEwMtOpiLLJsUy2qnXZw==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.32.tgz",
|
||||
"integrity": "sha512-jHUeDPVHrgFltqoAqDB6g6OStNnFxnc7Aks3p0KE0FbwAvRg6qWKYF5mSTdCTxA3axoSAUwxYdILzXJfUwlHhA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -706,9 +706,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.28.tgz",
|
||||
"integrity": "sha512-1gCmpvyhz7DkB1srRItJTnmR2UwQPAUXXIg9r0/56g3O8etGmwlX68skKXJOp9EejW3hhv7nSQUJ2raFiz4MoA==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.32.tgz",
|
||||
"integrity": "sha512-2N0lSoU4GjfLSO50wvKpMQgKd4HdI2UHEhQPPPnlgfBJlOgJxkjpkYBqzk08f1gItBB6xF/n+ykso2hgxuydsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2809,9 +2809,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3396,9 +3396,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -4532,14 +4533,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
|
||||
"integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.2",
|
||||
"has-tostringtag": "^1.0.0",
|
||||
"hasown": "^2.0.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -5310,14 +5313,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5683,12 +5688,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -6783,12 +6789,12 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "14.2.28",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.28.tgz",
|
||||
"integrity": "sha512-QLEIP/kYXynIxtcKB6vNjtWLVs3Y4Sb+EClTC/CSVzdLD1gIuItccpu/n1lhmduffI32iPGEK2cLLxxt28qgYA==",
|
||||
"version": "14.2.32",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-14.2.32.tgz",
|
||||
"integrity": "sha512-fg5g0GZ7/nFc09X8wLe6pNSU8cLWbLRG3TZzPJ1BJvi2s9m7eF991se67wliM9kR5yLHRkyGKU49MMx58s3LJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "14.2.28",
|
||||
"@next/env": "14.2.32",
|
||||
"@swc/helpers": "0.5.5",
|
||||
"busboy": "1.6.0",
|
||||
"caniuse-lite": "^1.0.30001579",
|
||||
@@ -6803,15 +6809,15 @@
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "14.2.28",
|
||||
"@next/swc-darwin-x64": "14.2.28",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.28",
|
||||
"@next/swc-linux-arm64-musl": "14.2.28",
|
||||
"@next/swc-linux-x64-gnu": "14.2.28",
|
||||
"@next/swc-linux-x64-musl": "14.2.28",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.28",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.28",
|
||||
"@next/swc-win32-x64-msvc": "14.2.28"
|
||||
"@next/swc-darwin-arm64": "14.2.32",
|
||||
"@next/swc-darwin-x64": "14.2.32",
|
||||
"@next/swc-linux-arm64-gnu": "14.2.32",
|
||||
"@next/swc-linux-arm64-musl": "14.2.32",
|
||||
"@next/swc-linux-x64-gnu": "14.2.32",
|
||||
"@next/swc-linux-x64-musl": "14.2.32",
|
||||
"@next/swc-win32-arm64-msvc": "14.2.32",
|
||||
"@next/swc-win32-ia32-msvc": "14.2.32",
|
||||
"@next/swc-win32-x64-msvc": "14.2.32"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.1.0",
|
||||
@@ -8565,9 +8571,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmp": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
"integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
|
||||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
|
||||
"integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
+250
-127
@@ -2,6 +2,7 @@
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import Card from "@components/Card";
|
||||
import FancyToggleSwitch from "@components/FancyToggleSwitch";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
@@ -219,7 +220,7 @@ const PeerGeneralInformation = () => {
|
||||
<ModalTrigger>
|
||||
<div
|
||||
className={
|
||||
"flex items-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 py-2 px-3 rounded-md cursor-pointer"
|
||||
"flex h-8 w-8 items-center justify-center gap-2 dark:text-neutral-300 text-neutral-500 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
}
|
||||
>
|
||||
<PencilIcon size={16} />
|
||||
@@ -324,7 +325,7 @@ const PeerGeneralInformation = () => {
|
||||
}
|
||||
interactive={false}
|
||||
className={"w-full block"}
|
||||
disabled={!permission.peers.update}
|
||||
disabled={permission.peers.update}
|
||||
>
|
||||
<FancyToggleSwitch
|
||||
value={ssh}
|
||||
@@ -415,158 +416,200 @@ const PeerOverviewTabs = () => {
|
||||
|
||||
function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
const { isLoading, getRegionByPeer } = useCountries();
|
||||
const { update } = usePeer();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
return (
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"NetBird IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
NetBird IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.ip}
|
||||
<>
|
||||
<Modal open={showEditIPModal} onOpenChange={setShowEditIPModal}>
|
||||
<EditIPModal
|
||||
onSuccess={(newIP) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "Peer IP was successfully updated",
|
||||
promise: update({ ip: newIP }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPModal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IP...",
|
||||
});
|
||||
}}
|
||||
peer={peer}
|
||||
key={showEditIPModal ? 1 : 0}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP-Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP-Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
className={
|
||||
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
|
||||
? "items-start"
|
||||
: ""
|
||||
}
|
||||
value={peer.dns_label}
|
||||
extraText={peer?.extra_dns_labels}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
tooltip={false}
|
||||
value={
|
||||
isEmpty(peer.country_code) ? (
|
||||
"Unknown"
|
||||
) : (
|
||||
</Modal>
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
copy
|
||||
tooltip={false}
|
||||
copyText={"NetBird IP Address"}
|
||||
label={
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton width={140} />
|
||||
) : (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<div className={"border-0 border-nb-gray-800 rounded-full"}>
|
||||
<RoundedFlag country={peer.country_code} size={12} />
|
||||
</div>
|
||||
{countryText}
|
||||
</div>
|
||||
)}
|
||||
<MapPin size={16} />
|
||||
NetBird IP Address
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
valueToCopy={peer.ip}
|
||||
value={
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{peer.ip}</span>
|
||||
{permission.peers.update && (
|
||||
<button
|
||||
className="flex w-7 h-7 items-center justify-center gap-2 text-nb-gray-400 hover:text-neutral-100 transition-all hover:bg-nb-gray-800/60 rounded-md cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setShowEditIPModal(true);
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
value={peer.os}
|
||||
/>
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
Public IP Address
|
||||
</>
|
||||
}
|
||||
value={peer.connection_ip}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
className={
|
||||
peer?.extra_dns_labels && peer.extra_dns_labels.length > 0
|
||||
? "items-start"
|
||||
: ""
|
||||
}
|
||||
value={peer.dns_label}
|
||||
extraText={peer?.extra_dns_labels}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
value={peer.hostname}
|
||||
/>
|
||||
|
||||
{peer.serial_number && peer.serial_number !== "" && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
<FlagIcon size={16} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
value={peer.serial_number}
|
||||
tooltip={false}
|
||||
value={
|
||||
isEmpty(peer.country_code) ? (
|
||||
"Unknown"
|
||||
) : (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton width={140} />
|
||||
) : (
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<div
|
||||
className={"border-0 border-nb-gray-800 rounded-full"}
|
||||
>
|
||||
<RoundedFlag country={peer.country_code} size={12} />
|
||||
</div>
|
||||
{countryText}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
value={
|
||||
peer.connected
|
||||
? "just now"
|
||||
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
value={peer.os}
|
||||
/>
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
value={peer.version}
|
||||
/>
|
||||
{peer.serial_number && peer.serial_number !== "" && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
value={peer.serial_number}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
value={
|
||||
peer.connected
|
||||
? "just now"
|
||||
: dayjs(peer.last_seen).format("D MMMM, YYYY [at] h:mm A") +
|
||||
" (" +
|
||||
dayjs().to(peer.last_seen) +
|
||||
")"
|
||||
}
|
||||
/>
|
||||
|
||||
{peer.ui_version && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
UI Version
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
value={peer.ui_version?.replace("netbird-desktop-ui/", "")}
|
||||
value={peer.version}
|
||||
/>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
|
||||
{peer.ui_version && (
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
UI Version
|
||||
</>
|
||||
}
|
||||
value={peer.ui_version?.replace("netbird-desktop-ui/", "")}
|
||||
/>
|
||||
)}
|
||||
</Card.List>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -654,3 +697,83 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditIPModalProps {
|
||||
onSuccess: (ip: string) => void;
|
||||
peer: Peer;
|
||||
}
|
||||
|
||||
function EditIPModal({ onSuccess, peer }: Readonly<EditIPModalProps>) {
|
||||
const [ip, setIP] = useState(peer.ip);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const validateIP = (ipAddress: string) => {
|
||||
const ipRegex =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
return ipRegex.test(ipAddress);
|
||||
};
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (ip === peer.ip) return true;
|
||||
const trimmedIP = trim(ip);
|
||||
return trimmedIP.length === 0 || !validateIP(ip);
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
React.useEffect(() => {
|
||||
switch (true) {
|
||||
case ip === peer.ip:
|
||||
setError("");
|
||||
break;
|
||||
case !validateIP(ip):
|
||||
setError("Please enter a valid IP, e.g., 100.64.0.15");
|
||||
break;
|
||||
default:
|
||||
setError("");
|
||||
break;
|
||||
}
|
||||
}, [ip, peer.ip]);
|
||||
|
||||
return (
|
||||
<ModalContent maxWidthClass={"max-w-md"}>
|
||||
<form>
|
||||
<ModalHeader
|
||||
title={"Edit Peer IP Address"}
|
||||
description={"Update the NetBird IP address for this peer."}
|
||||
color={"blue"}
|
||||
/>
|
||||
|
||||
<div className={"p-default flex flex-col gap-4"}>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={"e.g., 100.64.0.15"}
|
||||
value={ip}
|
||||
onChange={(e) => setIP(e.target.value)}
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Callout>Changes take effect when the peer reconnects.</Callout>
|
||||
</div>
|
||||
|
||||
<ModalFooter className={"items-center"} separator={false}>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
onClick={() => onSuccess(ip)}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ function CardList({ children }: Props) {
|
||||
type CardListItemProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
valueToCopy?: string;
|
||||
className?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
@@ -39,6 +40,7 @@ type CardListItemProps = {
|
||||
function CardListItem({
|
||||
label,
|
||||
value,
|
||||
valueToCopy,
|
||||
className,
|
||||
copy = false,
|
||||
copyText,
|
||||
@@ -57,6 +59,7 @@ function CardListItem({
|
||||
<CardTextItem
|
||||
label={label}
|
||||
value={value}
|
||||
valueToCopy={valueToCopy}
|
||||
copy={copy}
|
||||
copyText={copyText}
|
||||
tooltip={tooltip}
|
||||
@@ -79,6 +82,7 @@ function CardListItem({
|
||||
type CardTextItemProps = {
|
||||
label: React.ReactNode;
|
||||
value: React.ReactNode;
|
||||
valueToCopy?: string;
|
||||
copy?: boolean;
|
||||
copyText?: string;
|
||||
tooltip?: boolean;
|
||||
@@ -88,10 +92,11 @@ const CardTextItem = ({
|
||||
label,
|
||||
value,
|
||||
copy = false,
|
||||
valueToCopy,
|
||||
copyText,
|
||||
tooltip = true,
|
||||
}: CardTextItemProps) => {
|
||||
const [, copyToClipBoard] = useCopyToClipboard(value as string);
|
||||
const [, copyToClipBoard] = useCopyToClipboard(valueToCopy ?? `${value}`);
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -26,6 +26,7 @@ const PeerContext = React.createContext(
|
||||
loginExpiration?: boolean;
|
||||
inactivityExpiration?: boolean;
|
||||
approval_required?: boolean;
|
||||
ip?: string;
|
||||
}) => Promise<Peer>;
|
||||
openSSHDialog: () => Promise<boolean>;
|
||||
deletePeer: () => void;
|
||||
@@ -36,7 +37,7 @@ const PeerContext = React.createContext(
|
||||
export default function PeerProvider({ children, peer }: Props) {
|
||||
const user = usePeerUser(peer);
|
||||
const { peerGroups, isLoading } = usePeerGroups(peer);
|
||||
const peerRequest = useApiCall<Peer>("/peers");
|
||||
const peerRequest = useApiCall<Peer>("/peers", true);
|
||||
const { confirm } = useDialog();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
@@ -68,6 +69,7 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
loginExpiration?: boolean;
|
||||
inactivityExpiration?: boolean;
|
||||
approval_required?: boolean;
|
||||
ip?: string;
|
||||
}) => {
|
||||
return peerRequest.put(
|
||||
{
|
||||
@@ -86,6 +88,7 @@ export default function PeerProvider({ children, peer }: Props) {
|
||||
props?.approval_required == undefined
|
||||
? undefined
|
||||
: props.approval_required,
|
||||
ip: props.ip != undefined ? props.ip : undefined,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
|
||||
: toUpdate.access_control_groups ??
|
||||
route.access_control_groups ??
|
||||
undefined,
|
||||
skip_auto_apply: toUpdate.skip_auto_apply ?? route.skip_auto_apply ?? true,
|
||||
},
|
||||
`/${route.id}`,
|
||||
)
|
||||
@@ -94,6 +95,7 @@ export default function RoutesProvider({ children }: Readonly<Props>) {
|
||||
masquerade: route.masquerade,
|
||||
groups: route.groups || [],
|
||||
access_control_groups: route?.access_control_groups || undefined,
|
||||
skip_auto_apply: route.skip_auto_apply ?? true,
|
||||
})
|
||||
.then((route) => {
|
||||
mutate("/routes");
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface Account {
|
||||
regular_users_view_blocked: boolean;
|
||||
routing_peer_dns_resolution_enabled: boolean;
|
||||
dns_domain: string;
|
||||
network_range?: string;
|
||||
lazy_connection_enabled: boolean;
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export interface Route {
|
||||
groups: string[];
|
||||
keep_route?: boolean;
|
||||
access_control_groups?: string[];
|
||||
skip_auto_apply?: boolean;
|
||||
// Frontend only
|
||||
peer_groups?: string[];
|
||||
routesGroups?: string[];
|
||||
|
||||
@@ -63,11 +63,12 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "setupkey.peer.add")
|
||||
if (event.activity_code == "peer.setupkey.add")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> <PeerConnectionInfo meta={m} /> was added
|
||||
with the NetBird IP <Value>{m.ip}</Value>
|
||||
with the NetBird IP <Value>{m.ip}</Value> using the setup key{" "}
|
||||
<Value>{m.setup_key_name}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -340,6 +341,14 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "peer.ip.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Peer <Value>{m.name}</Value> IP address was updated from{" "}
|
||||
<Value>{m.old_ip}</Value> to <Value>{m.ip}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Group
|
||||
*/
|
||||
@@ -378,6 +387,15 @@ export default function ActivityDescription({ event }: Props) {
|
||||
if (event.activity_code == "account.setting.peer.login.expiration.disable")
|
||||
return <div className={"inline"}>Global login expiration was disabled</div>;
|
||||
|
||||
if (event.activity_code == "account.network.range.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Account network range was updated from{" "}
|
||||
<Value>{m.old_network_range}</Value> to{" "}
|
||||
<Value>{m.new_network_range}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Nameserver
|
||||
*/
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import Button from "@components/Button";
|
||||
import { CommandItem } from "@components/Command";
|
||||
import { DropdownInfoText } from "@components/DropdownInfoText";
|
||||
import { DropdownInput } from "@components/DropdownInput";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/Popover";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { SmallBadge } from "@components/ui/SmallBadge";
|
||||
import TextWithTooltip from "@components/ui/TextWithTooltip";
|
||||
import { cn, generateColorFromString } from "@utils/helpers";
|
||||
import { Command, CommandGroup, CommandInput, CommandList } from "cmdk";
|
||||
import { sortBy, trim, uniqBy } from "lodash";
|
||||
import { ChevronsUpDown, Cog, SearchIcon, UserCircle2 } from "lucide-react";
|
||||
import { VirtualScrollAreaList } from "@components/VirtualScrollAreaList";
|
||||
import { useSearch } from "@hooks/useSearch";
|
||||
import { generateColorFromString } from "@utils/helpers";
|
||||
import { sortBy, uniqBy } from "lodash";
|
||||
import { ChevronsUpDown, Cog, UserCircle2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useElementSize } from "@/hooks/useElementSize";
|
||||
import { SmallUserAvatar } from "@/modules/users/SmallUserAvatar";
|
||||
|
||||
@@ -29,17 +29,35 @@ export type UserSelectOption = {
|
||||
external?: boolean;
|
||||
};
|
||||
|
||||
const searchPredicate = (item: UserSelectOption, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (
|
||||
item.email === "NetBird" &&
|
||||
"NetBird System".toLowerCase().includes(lowerCaseQuery)
|
||||
)
|
||||
return true;
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
if (item.email?.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.id.toLowerCase().startsWith(lowerCaseQuery);
|
||||
};
|
||||
|
||||
export function UsersDropdownSelector({
|
||||
onChange,
|
||||
value,
|
||||
disabled = false,
|
||||
popoverWidth = 250,
|
||||
options,
|
||||
}: Props) {
|
||||
const searchRef = React.useRef<HTMLInputElement>(null);
|
||||
}: Readonly<Props>) {
|
||||
const [filteredItems, search, setSearch] = useSearch(
|
||||
options.concat({
|
||||
id: "all-users",
|
||||
name: "All Users",
|
||||
email: "Include all users",
|
||||
}),
|
||||
searchPredicate,
|
||||
{ filter: true, debounce: 150 },
|
||||
);
|
||||
const [inputRef, { width }] = useElementSize<HTMLButtonElement>();
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const search = useDebounce(searchInput, 500);
|
||||
|
||||
const toggle = (item: string | undefined) => {
|
||||
const isSelected = value == item;
|
||||
@@ -47,7 +65,7 @@ export function UsersDropdownSelector({
|
||||
onChange && onChange(undefined);
|
||||
} else {
|
||||
onChange && onChange(item);
|
||||
setSearchInput("");
|
||||
setSearch("");
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
@@ -55,11 +73,17 @@ export function UsersDropdownSelector({
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const sortedOptions = useMemo(() => {
|
||||
return sortBy(
|
||||
uniqBy(options, (o) => o.email),
|
||||
const sorted = sortBy(
|
||||
uniqBy(filteredItems, (o) => o.email),
|
||||
["external", "name"],
|
||||
);
|
||||
}, [options]);
|
||||
const allUsersIndex = sorted.findIndex((o) => o.id === "all-users");
|
||||
if (allUsersIndex > -1) {
|
||||
const allUsers = sorted.splice(allUsersIndex, 1)[0];
|
||||
sorted.unshift(allUsers);
|
||||
}
|
||||
return sorted;
|
||||
}, [filteredItems]);
|
||||
|
||||
const selectedUser = useMemo(() => {
|
||||
return options.find((user) => user.email == value);
|
||||
@@ -71,7 +95,7 @@ export function UsersDropdownSelector({
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setTimeout(() => {
|
||||
setSearchInput("");
|
||||
setSearch("");
|
||||
}, 100);
|
||||
}
|
||||
setOpen(isOpen);
|
||||
@@ -137,145 +161,105 @@ export function UsersDropdownSelector({
|
||||
side={"bottom"}
|
||||
sideOffset={10}
|
||||
>
|
||||
<Command
|
||||
className={"w-full flex"}
|
||||
loop
|
||||
filter={(value, search) => {
|
||||
const formatValue = trim(value.toLowerCase());
|
||||
const formatSearch = trim(search.toLowerCase());
|
||||
if (formatValue.includes(formatSearch)) return 1;
|
||||
return 0;
|
||||
}}
|
||||
>
|
||||
<CommandList className={"w-full"}>
|
||||
<div className={"relative"}>
|
||||
<CommandInput
|
||||
className={cn(
|
||||
"min-h-[42px] w-full relative",
|
||||
"border-b-0 border-t-0 border-r-0 border-l-0 border-neutral-200 dark:border-nb-gray-700 items-center",
|
||||
"bg-transparent text-sm outline-none focus-visible:outline-none ring-0 focus-visible:ring-0",
|
||||
"dark:placeholder:text-nb-gray-400 font-light placeholder:text-neutral-500 pl-10",
|
||||
)}
|
||||
ref={searchRef}
|
||||
value={searchInput}
|
||||
onValueChange={setSearchInput}
|
||||
placeholder={"Search user..."}
|
||||
/>
|
||||
<div
|
||||
className={
|
||||
"absolute left-0 top-0 h-full flex items-center pl-4"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center"}>
|
||||
<SearchIcon size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<DropdownInput
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
placeholder={"Search user..."}
|
||||
hideEnterIcon={true}
|
||||
/>
|
||||
|
||||
<ScrollArea
|
||||
className={
|
||||
"max-h-[380px] overflow-y-hidden flex flex-col gap-1 pl-2 py-2 pr-3"
|
||||
}
|
||||
>
|
||||
<CommandGroup>
|
||||
<div className={"grid grid-cols-1 gap-1"}>
|
||||
<CommandItem
|
||||
value={undefined}
|
||||
className={"py-1 px-2"}
|
||||
onSelect={() => {
|
||||
toggle(undefined);
|
||||
}}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className={"flex items-center gap-2"}>
|
||||
{options.length == 0 && !search && (
|
||||
<div className={"max-w-xs mx-auto"}>
|
||||
<DropdownInfoText>
|
||||
{"No users available to select."}
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredItems.length == 0 && search != "" && (
|
||||
<div className={"px-10"}>
|
||||
<DropdownInfoText>
|
||||
There are no users matching your search.
|
||||
</DropdownInfoText>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sortedOptions.length > 0 && (
|
||||
<VirtualScrollAreaList
|
||||
items={sortedOptions}
|
||||
estimatedItemHeight={52}
|
||||
maxHeight={380}
|
||||
scrollAreaClassName={"pt-0"}
|
||||
onSelect={(item) => {
|
||||
if (item.id === "all-users") {
|
||||
toggle(undefined);
|
||||
return;
|
||||
}
|
||||
toggle(item.email);
|
||||
}}
|
||||
renderItem={(user) => {
|
||||
const isSystemUser = user.email === "NetBird";
|
||||
|
||||
return (
|
||||
<div className={"flex items-center gap-2 w-full"}>
|
||||
{user.id === "all-users" ? (
|
||||
<div
|
||||
className={
|
||||
"w-7 h-7 rounded-full flex items-center justify-center uppercase text-[9px] font-medium bg-sky-400 text-white"
|
||||
"w-7 h-7 shrink-0 rounded-full flex items-center justify-center uppercase text-[9px] font-medium bg-sky-400 text-white"
|
||||
}
|
||||
>
|
||||
<UserCircle2 size={16} />
|
||||
</div>
|
||||
) : (
|
||||
<SmallUserAvatar
|
||||
name={user?.name}
|
||||
email={user?.email}
|
||||
id={user?.id}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={"flex flex-col text-xs"}>
|
||||
<span className={"text-nb-gray-200"}>All Users</span>
|
||||
<span className={"text-nb-gray-400 font-light"}>
|
||||
Include all users
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
||||
{sortedOptions.map((user) => {
|
||||
const isSystemUser = user.email === "NetBird";
|
||||
const searchValue = isSystemUser
|
||||
? "NetBird System"
|
||||
: user.name + " " + user.id + " " + user.email;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
value={searchValue}
|
||||
className={"py-1 px-2"}
|
||||
onSelect={() => {
|
||||
toggle(user.email);
|
||||
}}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
<div className={"flex flex-col text-xs w-full"}>
|
||||
<span
|
||||
className={
|
||||
"text-nb-gray-200 flex items-center gap-1.5 w-full"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center gap-2 w-full"}>
|
||||
<SmallUserAvatar
|
||||
name={user?.name}
|
||||
email={user?.email}
|
||||
id={user?.id}
|
||||
/>
|
||||
|
||||
<div className={"flex flex-col text-xs w-full"}>
|
||||
<span
|
||||
className={
|
||||
"text-nb-gray-200 flex items-center gap-1.5 w-full"
|
||||
}
|
||||
>
|
||||
<TextWithTooltip
|
||||
text={
|
||||
isSystemUser
|
||||
? "System"
|
||||
: user?.name || user?.id
|
||||
}
|
||||
maxChars={20}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"text-nb-gray-400 font-light flex items-center gap-1"
|
||||
}
|
||||
>
|
||||
<TextWithTooltip
|
||||
text={user?.email || "NetBird"}
|
||||
maxChars={20}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{user.external && (
|
||||
<span
|
||||
className={"flex items-center ml-auto relative"}
|
||||
>
|
||||
<SmallBadge
|
||||
text={"External"}
|
||||
variant={"sky"}
|
||||
className={
|
||||
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<TextWithTooltip
|
||||
text={
|
||||
isSystemUser ? "System" : user?.name || user?.id
|
||||
}
|
||||
maxChars={20}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
"text-nb-gray-400 font-light flex items-center gap-1"
|
||||
}
|
||||
>
|
||||
<TextWithTooltip
|
||||
text={user?.email || "NetBird"}
|
||||
maxChars={20}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{user.external && (
|
||||
<span className={"flex items-center ml-auto relative"}>
|
||||
<SmallBadge
|
||||
text={"External"}
|
||||
variant={"sky"}
|
||||
className={
|
||||
"text-[8.5px] py-[0.15rem] px-[.32rem] leading-none rounded-full -top-0"
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ type Props = {
|
||||
|
||||
export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
||||
const [modal, setModal] = useState(false);
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
const exitNodeInfo = useHasExitNodes(peer);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
return (
|
||||
@@ -25,7 +25,7 @@ export const ExitNodeDropdownButton = ({ peer }: Props) => {
|
||||
disabled={!permission.routes.create}
|
||||
>
|
||||
<div className={"flex gap-3 items-center w-full"}>
|
||||
{hasExitNodes ? (
|
||||
{exitNodeInfo.hasExitNode ? (
|
||||
<>
|
||||
<IconCirclePlus size={14} className={"shrink-0"} />
|
||||
<div className={"flex justify-between items-center w-full"}>
|
||||
|
||||
@@ -8,18 +8,22 @@ type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
export const ExitNodePeerIndicator = ({ peer }: Props) => {
|
||||
const hasExitNode = useHasExitNodes(peer);
|
||||
const exitNodeInfo = useHasExitNodes(peer);
|
||||
|
||||
return hasExitNode ? (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
This peer is an exit node. Traffic from the configured distribution
|
||||
groups will be routed through this peer.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconDirectionSign size={15} className={"text-yellow-400 shrink-0"} />
|
||||
if (!exitNodeInfo.hasExitNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipContent = exitNodeInfo.skipAutoApply === false
|
||||
? "This peer is an auto-applied exit node. Traffic from the configured distribution groups will be routed through this peer."
|
||||
: "This peer is an exit node. Traffic from the configured distribution groups will be routed through this peer.";
|
||||
|
||||
return (
|
||||
<FullTooltip content={<div className={"text-xs max-w-xs"}>{tooltipContent}</div>}>
|
||||
<IconDirectionSign
|
||||
size={15}
|
||||
className={`shrink-0 ${exitNodeInfo.skipAutoApply === false ? "text-green-400" : "text-yellow-400"}`}
|
||||
/>
|
||||
</FullTooltip>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
@@ -3,7 +3,12 @@ import { useLoggedInUser } from "@/contexts/UsersProvider";
|
||||
import { Peer } from "@/interfaces/Peer";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
|
||||
export const useHasExitNodes = (peer?: Peer) => {
|
||||
export interface ExitNodeInfo {
|
||||
hasExitNode: boolean;
|
||||
skipAutoApply?: boolean;
|
||||
}
|
||||
|
||||
export const useHasExitNodes = (peer?: Peer): ExitNodeInfo => {
|
||||
const { isOwnerOrAdmin } = useLoggedInUser();
|
||||
const { data: routes } = useFetchApi<Route[]>(
|
||||
`/routes`,
|
||||
@@ -11,9 +16,17 @@ export const useHasExitNodes = (peer?: Peer) => {
|
||||
true,
|
||||
isOwnerOrAdmin,
|
||||
);
|
||||
return peer
|
||||
? routes?.some(
|
||||
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
|
||||
) || false
|
||||
: false;
|
||||
|
||||
if (!peer || !routes) {
|
||||
return { hasExitNode: false };
|
||||
}
|
||||
|
||||
const exitNodeRoute = routes.find(
|
||||
(route) => route?.peer === peer.id && route?.network === "0.0.0.0/0",
|
||||
);
|
||||
|
||||
return {
|
||||
hasExitNode: !!exitNodeRoute,
|
||||
skipAutoApply: exitNodeRoute?.skip_auto_apply,
|
||||
};
|
||||
};
|
||||
@@ -17,7 +17,7 @@ type Props = {
|
||||
|
||||
export const PeerNetworkRoutesSection = ({ peer }: Props) => {
|
||||
const { peerRoutes, isLoading } = usePeerRoutes({ peer });
|
||||
const hasExitNodes = useHasExitNodes(peer);
|
||||
const exitNodeInfo = useHasExitNodes(peer);
|
||||
const { ref: headingRef, portalTarget } =
|
||||
usePortalElement<HTMLHeadingElement>();
|
||||
|
||||
@@ -34,7 +34,7 @@ export const PeerNetworkRoutesSection = ({ peer }: Props) => {
|
||||
</div>
|
||||
<div className={"inline-flex gap-4 justify-end"}>
|
||||
<div className={"gap-4 flex"}>
|
||||
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
|
||||
<AddExitNodeButton peer={peer} firstTime={!exitNodeInfo.hasExitNode} />
|
||||
<AddRouteDropdownButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ToggleSwitch } from "@components/ToggleSwitch";
|
||||
import React, { useMemo } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useRoutes } from "@/contexts/RoutesProvider";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
|
||||
type Props = {
|
||||
route: Route;
|
||||
};
|
||||
|
||||
export default function RouteAutoApplyCell({ route }: Readonly<Props>) {
|
||||
const { permission } = usePermissions();
|
||||
const { updateRoute } = useRoutes();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const isExitNode = useMemo(() => route.network === "0.0.0.0/0", [route]);
|
||||
|
||||
const isChecked = useMemo(() => {
|
||||
// Checked means Auto Apply is ON, which maps to skip_auto_apply === false
|
||||
return route.skip_auto_apply === false;
|
||||
}, [route]);
|
||||
|
||||
const update = async (checked: boolean) => {
|
||||
// When toggled ON (checked === true), we want skip_auto_apply = false
|
||||
const nextSkipAutoApply = !checked;
|
||||
updateRoute(
|
||||
route,
|
||||
{ skip_auto_apply: nextSkipAutoApply },
|
||||
() => {
|
||||
mutate("/routes");
|
||||
},
|
||||
checked
|
||||
? "Auto Apply was enabled for the route"
|
||||
: "Auto Apply was disabled for the route",
|
||||
);
|
||||
};
|
||||
|
||||
if (!isExitNode) return null;
|
||||
|
||||
return (
|
||||
<div className={"flex items-center"}>
|
||||
<ToggleSwitch
|
||||
checked={isChecked}
|
||||
size={"small"}
|
||||
onClick={() => update(!isChecked)}
|
||||
disabled={!permission.routes.update}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ export function RouteModalContent({
|
||||
const [enabled, setEnabled] = useState<boolean>(true);
|
||||
const [metric, setMetric] = useState("9999");
|
||||
const [masquerade, setMasquerade] = useState<boolean>(true);
|
||||
const [isForced, setIsForced] = useState<boolean>(true);
|
||||
|
||||
const isNonLinuxRoutingPeer = useMemo(() => {
|
||||
if (!routingPeer) return false;
|
||||
@@ -304,6 +305,7 @@ export function RouteModalContent({
|
||||
masquerade: useSinglePeer && isNonLinuxRoutingPeer ? true : masquerade,
|
||||
groups: groupIds,
|
||||
access_control_groups: accessControlGroupIds || undefined,
|
||||
skip_auto_apply: !isForced,
|
||||
},
|
||||
onSuccess,
|
||||
);
|
||||
@@ -718,6 +720,20 @@ export function RouteModalContent({
|
||||
helpText={"Use this switch to enable or disable the route."}
|
||||
/>
|
||||
|
||||
{exitNode && (
|
||||
<FancyToggleSwitch
|
||||
value={isForced}
|
||||
onChange={setIsForced}
|
||||
label={
|
||||
<>
|
||||
<IconDirectionSign size={15} />
|
||||
Auto Apply Route
|
||||
</>
|
||||
}
|
||||
helpText={"Automatically apply this exit node to your distribution groups. This requires NetBird client v0.55.0 or higher."}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!exitNode && (
|
||||
<RoutingPeerMasqueradeSwitch
|
||||
value={masquerade}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GroupedRoute, Route } from "@/interfaces/Route";
|
||||
import RouteAccessControlGroups from "@/modules/routes/RouteAccessControlGroups";
|
||||
import RouteActionCell from "@/modules/routes/RouteActionCell";
|
||||
import RouteActiveCell from "@/modules/routes/RouteActiveCell";
|
||||
import RouteAutoApplyCell from "@/modules/routes/RouteAutoApplyCell";
|
||||
import RouteDistributionGroupsCell from "@/modules/routes/RouteDistributionGroupsCell";
|
||||
import RouteMetricCell from "@/modules/routes/RouteMetricCell";
|
||||
import RoutePeerCell from "@/modules/routes/RoutePeerCell";
|
||||
@@ -77,6 +78,15 @@ export const RouteTableColumns: ColumnDef<Route>[] = [
|
||||
},
|
||||
cell: ({ row }) => <RouteAccessControlGroups route={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "skipAutoApply",
|
||||
accessorKey: "skip_auto_apply",
|
||||
header: ({ column }) => {
|
||||
return <DataTableHeader column={column}>Auto Apply</DataTableHeader>;
|
||||
},
|
||||
cell: ({ row }) => <RouteAutoApplyCell route={row.original} />,
|
||||
sortingFn: "basic",
|
||||
},
|
||||
{
|
||||
id: "group_names",
|
||||
accessorFn: (row) => {
|
||||
@@ -104,6 +114,10 @@ export default function RouteTable({ row }: Props) {
|
||||
desc: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const hasAtLeastOneExitNode = useMemo(() => {
|
||||
return row.routes?.some((route) => route.network === "0.0.0.0/0");
|
||||
}, [row.routes]);
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (!row.routes) return [];
|
||||
@@ -144,6 +158,7 @@ export default function RouteTable({ row }: Props) {
|
||||
domains: false,
|
||||
domain_search: false,
|
||||
network: false,
|
||||
skipAutoApply: !!hasAtLeastOneExitNode,
|
||||
}}
|
||||
setSorting={setSorting}
|
||||
columns={RouteTableColumns}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { Textarea } from "@components/Textarea";
|
||||
import { DomainsTooltip } from "@components/ui/DomainListBadge";
|
||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||
import { IconDirectionSign } from "@tabler/icons-react";
|
||||
import { cn } from "@utils/helpers";
|
||||
import { uniqBy } from "lodash";
|
||||
import {
|
||||
@@ -193,6 +194,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
const [masquerade, setMasquerade] = useState<boolean>(
|
||||
route?.masquerade ?? true,
|
||||
);
|
||||
const [isForced, setIsForced] = useState<boolean>(route?.skip_auto_apply === false);
|
||||
|
||||
// Refs to manage focus on tab change
|
||||
const networkRangeRef = useRef<HTMLInputElement>(null);
|
||||
@@ -257,6 +259,7 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
masquerade: useSinglePeer && isNonLinuxRoutingPeer ? true : masquerade,
|
||||
groups: groupIds,
|
||||
access_control_groups: accessControlGroupIds || undefined,
|
||||
skip_auto_apply: !isForced,
|
||||
},
|
||||
(r) => {
|
||||
onSuccess && onSuccess(r);
|
||||
@@ -456,6 +459,21 @@ function RouteUpdateModalContent({ onSuccess, route, cell }: ModalProps) {
|
||||
}
|
||||
helpText={"Use this switch to enable or disable the route."}
|
||||
/>
|
||||
|
||||
{isExitNode && (
|
||||
<FancyToggleSwitch
|
||||
value={isForced}
|
||||
onChange={setIsForced}
|
||||
label={
|
||||
<>
|
||||
<IconDirectionSign size={15} />
|
||||
Auto Apply Route
|
||||
</>
|
||||
}
|
||||
helpText={"Automatically apply this exit node to your distribution groups. This requires NetBird client v0.55.0 or higher."}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isExitNode && (
|
||||
<RoutingPeerMasqueradeSwitch
|
||||
value={masquerade}
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as Tabs from "@radix-ui/react-tabs";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { validator } from "@utils/helpers";
|
||||
import { isNetBirdHosted } from "@utils/netbird";
|
||||
import cidr from "ip-cidr";
|
||||
import { ExternalLinkIcon, GlobeIcon, NetworkIcon } from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
@@ -34,6 +35,9 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
const [customDNSDomain, setCustomDNSDomain] = useState(
|
||||
account.settings.dns_domain || "",
|
||||
);
|
||||
const [networkRange, setNetworkRange] = useState(
|
||||
account.settings.network_range || "",
|
||||
);
|
||||
|
||||
const toggleNetworkDNSSetting = async (toggle: boolean) => {
|
||||
notify({
|
||||
@@ -57,25 +61,37 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
});
|
||||
};
|
||||
|
||||
const { hasChanges, updateRef } = useHasChanges([customDNSDomain]);
|
||||
const { hasChanges, updateRef } = useHasChanges([
|
||||
customDNSDomain,
|
||||
networkRange,
|
||||
]);
|
||||
|
||||
const saveChanges = async () => {
|
||||
const updatedSettings = {
|
||||
...account.settings,
|
||||
};
|
||||
|
||||
if (customDNSDomain !== "" || account.settings.dns_domain) {
|
||||
updatedSettings.dns_domain = customDNSDomain;
|
||||
}
|
||||
|
||||
if (networkRange !== "") {
|
||||
updatedSettings.network_range = networkRange;
|
||||
}
|
||||
|
||||
notify({
|
||||
title: "Custom DNS Domain",
|
||||
description: `Custom DNS Domain successfully updated.`,
|
||||
title: "Network Settings",
|
||||
description: `Network settings successfully updated.`,
|
||||
promise: saveRequest
|
||||
.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
dns_domain: customDNSDomain || "",
|
||||
},
|
||||
settings: updatedSettings,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
updateRef([customDNSDomain]);
|
||||
updateRef([customDNSDomain, networkRange]);
|
||||
}),
|
||||
loadingMessage: "Updating Custom DNS domain...",
|
||||
loadingMessage: "Updating network settings...",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -90,6 +106,24 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
}
|
||||
}, [customDNSDomain]);
|
||||
|
||||
const networkRangeError = useMemo(() => {
|
||||
if (networkRange == "") {
|
||||
if (account.settings.network_range) {
|
||||
return "Network range cannot be empty";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const validCIDR = cidr.isValidCIDR(networkRange);
|
||||
if (!validCIDR) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
} catch (error) {
|
||||
return "Please enter a valid IPv4 CIDR range, e.g. 100.64.0.0/16 or 192.168.1.0/24";
|
||||
}
|
||||
}, [networkRange, account.settings.network_range]);
|
||||
|
||||
return (
|
||||
<Tabs.Content value={"networks"}>
|
||||
<div className={"p-default py-6 max-w-2xl"}>
|
||||
@@ -112,7 +146,12 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
</div>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={!hasChanges || !permission.settings.update}
|
||||
disabled={
|
||||
!hasChanges ||
|
||||
!permission.settings.update ||
|
||||
!!domainError ||
|
||||
!!networkRangeError
|
||||
}
|
||||
onClick={saveChanges}
|
||||
>
|
||||
Save Changes
|
||||
@@ -130,7 +169,8 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
<Label>DNS Domain</Label>
|
||||
<HelpText>
|
||||
Specify a custom peer DNS domain for your network. This should
|
||||
not point to a domain that is already in use elsewhere, to avoid overriding DNS results.
|
||||
not point to a domain that is already in use elsewhere, to
|
||||
avoid overriding DNS results.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
@@ -149,6 +189,33 @@ export default function NetworkSettingsTab({ account }: Readonly<Props>) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
|
||||
}
|
||||
>
|
||||
<div className={"min-w-[330px]"}>
|
||||
<Label>Network Range</Label>
|
||||
<HelpText>
|
||||
Specify a custom IPv4 range for your network in CIDR format.
|
||||
All peer IPs will be re-allocated when changed.
|
||||
</HelpText>
|
||||
</div>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
placeholder={"e.g. 100.64.0.0/16"}
|
||||
errorTooltip={true}
|
||||
errorTooltipPosition={"top"}
|
||||
error={networkRangeError}
|
||||
value={networkRange}
|
||||
disabled={!permission.settings.update}
|
||||
onChange={(e) => setNetworkRange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FancyToggleSwitch
|
||||
value={routingPeerDNSSetting}
|
||||
onChange={toggleNetworkDNSSetting}
|
||||
|
||||
Reference in new issue
Block a user