mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be79b9955 | ||
|
|
dc1adebd27 | ||
|
|
d76cbd1122 | ||
|
|
01330e0f58 | ||
|
|
e9ac1a1a23 |
No files matched your search
+103
-119
@@ -2,7 +2,6 @@
|
||||
|
||||
import Breadcrumbs from "@components/Breadcrumbs";
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import Card from "@components/Card";
|
||||
import HelpText from "@components/HelpText";
|
||||
import { Input } from "@components/Input";
|
||||
@@ -72,6 +71,7 @@ import ReverseProxiesProvider, {
|
||||
useReverseProxies,
|
||||
} from "@/contexts/ReverseProxiesProvider";
|
||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||
@@ -469,31 +469,55 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
const { update } = usePeer();
|
||||
const { mutate } = useSWRConfig();
|
||||
const [showEditIPModal, setShowEditIPModal] = useState(false);
|
||||
const [showEditIPv6Modal, setShowEditIPv6Modal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
|
||||
const countryText = useMemo(() => {
|
||||
return getRegionByPeer(peer);
|
||||
}, [getRegionByPeer, peer]);
|
||||
|
||||
const handleSaveIP = (newIP: string) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "NetBird Peer IP was successfully updated",
|
||||
promise: update({ ip: newIP }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPModal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IP...",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveIPv6 = (newIPv6: string) => {
|
||||
notify({
|
||||
title: peer.name,
|
||||
description: "NetBird Peer IPv6 was successfully updated",
|
||||
promise: update({ ipv6: newIPv6 }).then(() => {
|
||||
mutate("/peers/" + peer.id);
|
||||
setShowEditIPv6Modal(false);
|
||||
}),
|
||||
loadingMessage: "Updating peer IPv6...",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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}
|
||||
/>
|
||||
</Modal>
|
||||
<PeerEditIPModal
|
||||
version="v4"
|
||||
currentIP={peer.ip}
|
||||
open={showEditIPModal}
|
||||
onOpenChange={setShowEditIPModal}
|
||||
onSave={handleSaveIP}
|
||||
key={showEditIPModal ? "v4-open" : "v4-closed"}
|
||||
/>
|
||||
<PeerEditIPModal
|
||||
version="v6"
|
||||
currentIP={peer.ipv6 || ""}
|
||||
open={showEditIPv6Modal}
|
||||
onOpenChange={setShowEditIPv6Modal}
|
||||
onSave={handleSaveIPv6}
|
||||
key={showEditIPv6Modal ? "v6-open" : "v6-closed"}
|
||||
/>
|
||||
<Card className={"w-full xl:w-1/2"}>
|
||||
<Card.List>
|
||||
<Card.ListItem
|
||||
@@ -502,35 +526,48 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"NetBird IP Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} />
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
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>
|
||||
<EditableValue
|
||||
value={peer.ip}
|
||||
canEdit={permission.peers.update}
|
||||
onEdit={() => setShowEditIPModal(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{peer.ipv6 && (
|
||||
<Card.ListItem
|
||||
copy
|
||||
tooltip={false}
|
||||
copyText={"NetBird IPv6 Address"}
|
||||
label={
|
||||
<>
|
||||
<MapPin size={16} className={"shrink-0"} />
|
||||
NetBird IPv6 Address
|
||||
</>
|
||||
}
|
||||
valueToCopy={peer.ipv6}
|
||||
value={
|
||||
<EditableValue
|
||||
value={peer.ipv6}
|
||||
canEdit={permission.peers.update}
|
||||
onEdit={() => setShowEditIPv6Modal(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card.ListItem
|
||||
copy
|
||||
copyText={"Public IP Address"}
|
||||
label={
|
||||
<>
|
||||
<NetworkIcon size={16} />
|
||||
<NetworkIcon size={16} className={"shrink-0"} />
|
||||
Public IP Address
|
||||
</>
|
||||
}
|
||||
@@ -542,7 +579,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"DNS label"}
|
||||
label={
|
||||
<>
|
||||
<Globe size={16} />
|
||||
<Globe size={16} className={"shrink-0"} />
|
||||
Domain Name
|
||||
</>
|
||||
}
|
||||
@@ -560,7 +597,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
copyText={"Hostname"}
|
||||
label={
|
||||
<>
|
||||
<MonitorSmartphoneIcon size={16} />
|
||||
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
|
||||
Hostname
|
||||
</>
|
||||
}
|
||||
@@ -570,7 +607,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<FlagIcon size={16} />
|
||||
<FlagIcon size={16} className={"shrink-0"} />
|
||||
Region
|
||||
</>
|
||||
}
|
||||
@@ -600,7 +637,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Cpu size={16} />
|
||||
<Cpu size={16} className={"shrink-0"} />
|
||||
Operating System
|
||||
</>
|
||||
}
|
||||
@@ -611,7 +648,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<Barcode size={16} />
|
||||
<Barcode size={16} className={"shrink-0"} />
|
||||
Serial Number
|
||||
</>
|
||||
}
|
||||
@@ -623,7 +660,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<CalendarDays size={16} />
|
||||
<CalendarDays size={16} className={"shrink-0"} />
|
||||
Registered on
|
||||
</>
|
||||
}
|
||||
@@ -639,7 +676,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<History size={16} />
|
||||
<History size={16} className={"shrink-0"} />
|
||||
Last seen
|
||||
</>
|
||||
}
|
||||
@@ -656,7 +693,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
Agent Version
|
||||
</>
|
||||
}
|
||||
@@ -667,7 +704,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
|
||||
<Card.ListItem
|
||||
label={
|
||||
<>
|
||||
<NetBirdIcon size={16} />
|
||||
<NetBirdIcon size={16} className={"shrink-0"} />
|
||||
UI Version
|
||||
</>
|
||||
}
|
||||
@@ -765,82 +802,29 @@ function EditNameModal({ onSuccess, peer, initialName }: Readonly<ModalProps>) {
|
||||
);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
function EditableValue({
|
||||
value,
|
||||
canEdit,
|
||||
onEdit,
|
||||
}: {
|
||||
value: string;
|
||||
canEdit: boolean;
|
||||
onEdit: () => void;
|
||||
}) {
|
||||
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>
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<span>{value}</span>
|
||||
{canEdit && (
|
||||
<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();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<PencilIcon size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@utils/version";
|
||||
|
||||
export default function SSHPage() {
|
||||
const { peerId, username, port } = useSSHQueryParams();
|
||||
const { peerId, username, port, ipVersion } = useSSHQueryParams();
|
||||
|
||||
const {
|
||||
data: peer,
|
||||
@@ -48,6 +48,7 @@ export default function SSHPage() {
|
||||
peer={peer}
|
||||
username={username}
|
||||
port={port}
|
||||
ipVersion={ipVersion}
|
||||
/>
|
||||
) : (
|
||||
<LoadingMessage message={"Starting ssh session..."} />
|
||||
@@ -60,9 +61,10 @@ type Props = {
|
||||
username: string;
|
||||
port: string;
|
||||
peer: Peer;
|
||||
ipVersion: string | null;
|
||||
};
|
||||
|
||||
function SSHTerminal({ username, port, peer }: Props) {
|
||||
function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
||||
const client = useNetBirdClient();
|
||||
const connected = useRef(false);
|
||||
const sshConnectedOnce = useRef(false);
|
||||
@@ -81,9 +83,12 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const isClientDisconnected = client.status === NetBirdStatus.DISCONNECTED;
|
||||
const isClientConnecting = client.status === NetBirdStatus.CONNECTING;
|
||||
|
||||
// Use the FQDN when an IP version is specified so the dialer resolves to the correct address family.
|
||||
const sshHost = ipVersion ? peer.dns_label || peer.ip : peer.ip;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${username}@${peer.ip} - ${peer.hostname}`;
|
||||
}, [username, peer, client]);
|
||||
document.title = `${username}@${sshHost} - ${peer.hostname}`;
|
||||
}, [username, peer, client, sshHost]);
|
||||
|
||||
const handleReconnect = async () => {
|
||||
if (!peer?.id) return;
|
||||
@@ -97,9 +102,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Reconnection failed:", error);
|
||||
@@ -123,9 +129,10 @@ function SSHTerminal({ username, port, peer }: Props) {
|
||||
const rules = [`${protocol}/${aclPort}`];
|
||||
await client?.connectTemporary(peer.id, rules);
|
||||
const res = await ssh({
|
||||
hostname: peer.ip,
|
||||
hostname: sshHost,
|
||||
port: Number(port),
|
||||
username,
|
||||
ipVersion: ipVersion || undefined,
|
||||
});
|
||||
if (res === SSHStatus.CONNECTED) {
|
||||
sshConnectedOnce.current = true;
|
||||
|
||||
@@ -23,6 +23,7 @@ export const idpIcon = (
|
||||
zitadel: <ZitadelIcon size={size} />,
|
||||
authentik: <AuthentikIcon size={size} />,
|
||||
keycloak: <KeycloakIcon size={size} />,
|
||||
adfs: <MicrosoftIcon size={size} />,
|
||||
oidc: <KeyRound size={size} className="text-nb-gray-400" />,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,103 +3,36 @@ import { iconProperties, IconProps } from "@/assets/icons/IconProperties";
|
||||
export default function LoadingIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
width="135"
|
||||
height="140"
|
||||
viewBox="0 0 135 140"
|
||||
viewBox="0 0 919 669"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fillOpacity="0.8"
|
||||
{...iconProperties(props)}
|
||||
>
|
||||
<rect y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
<style>{`
|
||||
@keyframes nb-loader-pulse {
|
||||
0%, 100% { transform: scale(0.85); opacity: 0.35; }
|
||||
50% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.nb-loader-bird {
|
||||
animation: nb-loader-pulse 1.75s ease-in-out infinite;
|
||||
transform-origin: 50% 50%;
|
||||
transform-box: fill-box;
|
||||
}
|
||||
`}</style>
|
||||
<g className={"nb-loader-bird"}>
|
||||
<path
|
||||
d="M653.25 0C542.297 10.255 487.216 74.132 466.346 106.359L456.959 122.609C456.311 123.963 455.886 124.865 455.886 124.865L455.774 124.685L142.056 668.021H533.256L918.947 0H653.25Z"
|
||||
fill="#F68330"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
<path
|
||||
d="M533.256 668.02L0 102.296C0 102.296 603.149 -59.6172 661.827 445.893L533.256 668.02Z"
|
||||
fill="#F68330"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="30" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
<path
|
||||
d="M454.3 127.229L290.658 410.664L533.256 668.02L661.757 445.475C641.46 271.63 556.721 176.843 454.3 127.229Z"
|
||||
fill="#F35E32"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="60" width="15" height="140" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="90" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.25s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
<rect x="120" y="10" width="15" height="120" rx="6">
|
||||
<animate
|
||||
attributeName="height"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="120;110;100;90;80;70;60;50;40;140;120"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="y"
|
||||
begin="0.5s"
|
||||
dur="1s"
|
||||
values="10;15;20;25;30;35;40;45;50;0;10"
|
||||
calcMode="linear"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -50,11 +50,11 @@ function CardListItem({
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-4 last:border-b-0 items-center h-full",
|
||||
"flex justify-between px-4 border-b border-nb-gray-900 py-3.5 last:border-b-0 items-center h-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={"flex gap-2.5 items-center text-sm"}>{label}</div>
|
||||
<div className={"flex gap-2.5 items-center text-[0.84rem]"}>{label}</div>
|
||||
<div className={"flex flex-col gap-2"}>
|
||||
<CardTextItem
|
||||
label={label}
|
||||
@@ -100,7 +100,7 @@ const CardTextItem = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-right text-nb-gray-400 text-sm flex items-center gap-2",
|
||||
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
|
||||
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
|
||||
)}
|
||||
onClick={() =>
|
||||
|
||||
@@ -290,7 +290,7 @@ export function PeerGroupSelector({
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (tab === "groups") return placeholderForSearch;
|
||||
if (tab === "resources") return "Search resource...";
|
||||
if (tab === "peers") return "Search peer...";
|
||||
if (tab === "peers") return "Search peer by name or ip...";
|
||||
return "Search...";
|
||||
}, [tab, placeholderForSearch]);
|
||||
|
||||
@@ -537,9 +537,6 @@ export function PeerGroupSelector({
|
||||
const isSelected =
|
||||
values.find((group) => group.name == option.name) !=
|
||||
undefined;
|
||||
const peerCount =
|
||||
option.peers?.length ?? option?.peers_count ?? 0;
|
||||
|
||||
const isDisabled = disabledGroups
|
||||
? disabledGroups?.findIndex(
|
||||
(g) => g.id === option.id,
|
||||
@@ -968,7 +965,8 @@ const ResourcesList = ({
|
||||
const peersSearchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().includes(lowerCaseQuery);
|
||||
if (item.ip.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ipv6?.toLowerCase().includes(lowerCaseQuery) ?? false;
|
||||
};
|
||||
|
||||
const PeersList = ({
|
||||
|
||||
@@ -30,7 +30,8 @@ const searchPredicate = (item: Peer, query: string) => {
|
||||
const lowerCaseQuery = query.toLowerCase();
|
||||
if (item.name.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
if (item.hostname.toLowerCase().includes(lowerCaseQuery)) return true;
|
||||
return item.ip.toLowerCase().startsWith(lowerCaseQuery);
|
||||
if (item.ip.toLowerCase().startsWith(lowerCaseQuery)) return true;
|
||||
return !!item.ipv6?.toLowerCase().startsWith(lowerCaseQuery);
|
||||
};
|
||||
|
||||
export function PeerSelector({
|
||||
@@ -124,7 +125,6 @@ export function PeerSelector({
|
||||
"text-neutral-500 dark:text-nb-gray-300 font-medium flex items-center gap-1 font-mono text-[10px]"
|
||||
}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{value.ip}
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,7 +238,6 @@ export function PeerSelector({
|
||||
!isSupported && "opacity-50",
|
||||
)}
|
||||
>
|
||||
<MapPinIcon />
|
||||
{option.ip}
|
||||
</div>
|
||||
</FullTooltip>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function FullScreenLoading({ fullScreen = true }: Props) {
|
||||
fullScreen && "h-screen",
|
||||
)}
|
||||
>
|
||||
<LoadingIcon className="fill-netbird" size={44} />
|
||||
<LoadingIcon className="fill-netbird" size={68} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ const PeerContext = React.createContext(
|
||||
inactivityExpiration?: boolean;
|
||||
approval_required?: boolean;
|
||||
ip?: string;
|
||||
ipv6?: string;
|
||||
}) => Promise<Peer>;
|
||||
toggleSSH: (newState: boolean) => Promise<void>;
|
||||
setSSHInstructionsModal: (open: boolean) => void;
|
||||
@@ -80,6 +81,7 @@ export default function PeerProvider({
|
||||
inactivityExpiration?: boolean;
|
||||
approval_required?: boolean;
|
||||
ip?: string;
|
||||
ipv6?: string;
|
||||
}) => {
|
||||
return peerRequest.put(
|
||||
{
|
||||
@@ -99,6 +101,7 @@ export default function PeerProvider({
|
||||
? undefined
|
||||
: props.approval_required,
|
||||
ip: props.ip != undefined ? props.ip : undefined,
|
||||
ipv6: props.ipv6 != undefined ? props.ipv6 : undefined,
|
||||
},
|
||||
`/${peer.id}`,
|
||||
);
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface Account {
|
||||
auto_update_version: string;
|
||||
auto_update_always: boolean;
|
||||
local_auth_disabled?: boolean;
|
||||
ipv6_enabled_groups?: string[];
|
||||
network_range_v6?: string;
|
||||
};
|
||||
onboarding?: AccountOnboarding;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ export type SSOIdentityProviderType =
|
||||
| "pocketid"
|
||||
| "microsoft"
|
||||
| "authentik"
|
||||
| "keycloak";
|
||||
| "keycloak"
|
||||
| "adfs";
|
||||
|
||||
export const SSOIdentityProviderOptions: {
|
||||
value: SSOIdentityProviderType;
|
||||
@@ -59,6 +60,7 @@ export const SSOIdentityProviderOptions: {
|
||||
{ value: "pocketid", label: "PocketID" },
|
||||
{ value: "authentik", label: "Authentik" },
|
||||
{ value: "keycloak", label: "Keycloak" },
|
||||
{ value: "adfs", label: "Microsoft AD FS" },
|
||||
];
|
||||
|
||||
export const getSSOIdentityProviderLabelByType = (
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface Peer {
|
||||
id?: string;
|
||||
name: string;
|
||||
ip: string;
|
||||
ipv6?: string;
|
||||
connected: boolean;
|
||||
created_at?: Date;
|
||||
last_seen: Date;
|
||||
|
||||
@@ -22,11 +22,20 @@ export interface ReverseProxy {
|
||||
meta?: ReverseProxyMeta;
|
||||
}
|
||||
|
||||
export const CrowdSecMode = {
|
||||
OFF: "off",
|
||||
ENFORCE: "enforce",
|
||||
OBSERVE: "observe",
|
||||
} as const;
|
||||
|
||||
export type CrowdSecMode = (typeof CrowdSecMode)[keyof typeof CrowdSecMode];
|
||||
|
||||
export interface AccessRestrictions {
|
||||
allowed_cidrs?: string[];
|
||||
blocked_cidrs?: string[];
|
||||
allowed_countries?: string[];
|
||||
blocked_countries?: string[];
|
||||
crowdsec_mode?: CrowdSecMode;
|
||||
}
|
||||
|
||||
export interface ReverseProxyMeta {
|
||||
@@ -102,6 +111,7 @@ export interface ReverseProxyDomain {
|
||||
target_cluster?: string;
|
||||
supports_custom_ports?: boolean;
|
||||
require_subdomain?: boolean;
|
||||
supports_crowdsec?: boolean;
|
||||
}
|
||||
|
||||
export enum ReverseProxyDomainType {
|
||||
@@ -149,6 +159,7 @@ export interface ReverseProxyEvent {
|
||||
bytes_upload: number;
|
||||
bytes_download: number;
|
||||
protocol?: EventProtocol;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function isL4Event(event: ReverseProxyEvent): boolean {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useGroupContext } from "@/contexts/GroupProvider";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
import { GroupDetailsTableContainer } from "@/modules/groups/details/GroupDetailsTableContainer";
|
||||
import NetworkRoutesTable from "@/modules/route-group/NetworkRoutesTable";
|
||||
@@ -18,14 +19,16 @@ export const GroupNetworkRoutesSection = ({
|
||||
const { group } = useGroupContext();
|
||||
|
||||
return (
|
||||
<GroupDetailsTableContainer>
|
||||
<NetworkRoutesTable
|
||||
isGroupPage={true}
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
distributionGroups={[group]}
|
||||
/>
|
||||
</GroupDetailsTableContainer>
|
||||
<PeersProvider>
|
||||
<GroupDetailsTableContainer>
|
||||
<NetworkRoutesTable
|
||||
isGroupPage={true}
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
distributionGroups={[group]}
|
||||
/>
|
||||
</GroupDetailsTableContainer>
|
||||
</PeersProvider>
|
||||
);
|
||||
};
|
||||
@@ -34,7 +34,7 @@ export const NetworkRoutingPeersTabContent = ({
|
||||
|
||||
return {
|
||||
...router,
|
||||
search: `${peer?.name ?? ""} ${peer?.ip ?? ""} ${user?.name ?? ""} ${user?.id ?? ""} ${group?.name ?? ""}`,
|
||||
search: `${peer?.name ?? ""} ${peer?.ip ?? ""} ${peer?.ipv6 ?? ""} ${user?.name ?? ""} ${user?.id ?? ""} ${group?.name ?? ""}`,
|
||||
};
|
||||
});
|
||||
}, [users, peers, routers, groups]);
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import Button from "@components/Button";
|
||||
import { Callout } from "@components/Callout";
|
||||
import { Input } from "@components/Input";
|
||||
import {
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@components/modal/Modal";
|
||||
import ModalHeader from "@components/modal/ModalHeader";
|
||||
import cidr from "ip-cidr";
|
||||
import { trim } from "lodash";
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
type IPVersion = "v4" | "v6";
|
||||
|
||||
interface PeerEditIPModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (ip: string) => void;
|
||||
currentIP: string;
|
||||
version: IPVersion;
|
||||
}
|
||||
|
||||
const config: Record<
|
||||
IPVersion,
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
placeholder: string;
|
||||
errorMessage: string;
|
||||
validate: (ip: string) => boolean;
|
||||
}
|
||||
> = {
|
||||
v4: {
|
||||
title: "Edit Peer IP Address",
|
||||
description: "Update the NetBird IP address for this peer.",
|
||||
placeholder: "e.g., 100.64.0.15",
|
||||
errorMessage: "Please enter a valid IP, e.g., 100.64.0.15",
|
||||
validate: (ip: string) =>
|
||||
/^(?:(?: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]?)$/.test(
|
||||
ip,
|
||||
),
|
||||
},
|
||||
v6: {
|
||||
title: "Edit Peer IPv6 Address",
|
||||
description: "Update the NetBird IPv6 address for this peer.",
|
||||
placeholder: "e.g., fd00:1234::1",
|
||||
errorMessage: "Please enter a valid IPv6 address, e.g., fd00:1234::1",
|
||||
validate: (ip: string) => cidr.isValidAddress(ip) && ip.includes(":"),
|
||||
},
|
||||
};
|
||||
|
||||
export function PeerEditIPModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
currentIP,
|
||||
version,
|
||||
}: Readonly<PeerEditIPModalProps>) {
|
||||
const { title, description, placeholder, errorMessage, validate } =
|
||||
config[version];
|
||||
const [ip, setIP] = useState(currentIP);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
if (ip === currentIP) return true;
|
||||
const trimmed = trim(ip);
|
||||
return trimmed.length === 0 || !validate(trimmed);
|
||||
}, [ip, currentIP, validate]);
|
||||
|
||||
const error = useMemo(() => {
|
||||
if (ip === currentIP) return "";
|
||||
if (!validate(trim(ip))) return errorMessage;
|
||||
return "";
|
||||
}, [ip, currentIP, validate, errorMessage]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent maxWidthClass={"max-w-md"}>
|
||||
<form>
|
||||
<ModalHeader title={title} description={description} color={"blue"} />
|
||||
|
||||
<div className={"p-default flex flex-col gap-4"}>
|
||||
<div>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
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={() => onSave(trim(ip))}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { PeerAddressTooltipContent } from "@/modules/peers/PeerAddressTooltipCon
|
||||
type Props = {
|
||||
peer: Peer;
|
||||
};
|
||||
|
||||
export default function PeerAddressCell({ peer }: Props) {
|
||||
return (
|
||||
<FullTooltip
|
||||
|
||||
@@ -38,6 +38,21 @@ export const PeerAddressTooltipContent = ({ peer }: Props) => {
|
||||
</CopyToClipboardText>
|
||||
}
|
||||
/>
|
||||
{peer.ipv6 && (
|
||||
<ListItem
|
||||
icon={<MapPin size={14} />}
|
||||
label={"NetBird IPv6"}
|
||||
value={
|
||||
<CopyToClipboardText
|
||||
iconAlignment={"right"}
|
||||
message={"NetBird IPv6 has been copied to your clipboard"}
|
||||
alwaysShowIcon={true}
|
||||
>
|
||||
{peer.ipv6}
|
||||
</CopyToClipboardText>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ListItem
|
||||
icon={<NetworkIcon size={14} />}
|
||||
label={"Public IP"}
|
||||
|
||||
@@ -214,6 +214,10 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
|
||||
</PeerProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "ipv6",
|
||||
accessorFn: (row) => row.ipv6,
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
@@ -327,6 +331,7 @@ export default function PeersTable({
|
||||
connect: permission.peers.update,
|
||||
groups: permission.groups.read,
|
||||
os: false,
|
||||
ipv6: false,
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
getStartedCard={<NoPeersGettingStarted showBackground={true} />}
|
||||
|
||||
Loaded 20 of 34 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user