Compare commits

..
2 Commits
Author SHA1 Message Date
Eduard Gert 7c7f0a0f10 Add network range (#482) 2025-08-13 14:18:59 +02:00
braginini 76541c701c Update LICENSE 2025-08-05 11:19:13 +02:00
8 changed files with 1144 additions and 295 deletions

No files matched your search

+655 -7
View File
File diff suppressed because it is too large. Load diff
+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}`,
);
+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;
};
}
+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>
);
+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}