Compare commits

..
4 Commits
19 changed files with 1362 additions and 385 deletions

No files matched your search

+655 -7
View File
File diff suppressed because it is too large. Load diff
+74 -68
View File
@@ -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
View File
@@ -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>
);
}
+6 -1
View File
@@ -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(
+4 -1
View File
@@ -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}`,
);
+2
View File
@@ -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");
+1
View File
@@ -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;
};
}
+1
View File
@@ -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[];
+20 -2
View File
@@ -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
*/
+130 -146
View File
@@ -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"}>
+16 -12
View File
@@ -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;
);
};
+19 -6
View File
@@ -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>
+53
View File
@@ -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>
);
}
+16
View File
@@ -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}
+15
View File
@@ -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}
+18
View File
@@ -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}
+78 -11
View File
@@ -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}