Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 103 additions & 119 deletions src/app/(dashboard)/peer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand All @@ -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)}
/>
Comment on lines +535 to +560
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.tsx" -o -name "*.ts" | head -20

Repository: netbirdio/dashboard

Length of output: 897


🏁 Script executed:

rg -l "EditableValue" --type ts --type tsx | head -10

Repository: netbirdio/dashboard

Length of output: 90


🏁 Script executed:

rg -n "function EditableValue|const EditableValue" --type ts --type tsx

Repository: netbirdio/dashboard

Length of output: 90


🏁 Script executed:

fd -t f "page.tsx" | grep peer

Repository: netbirdio/dashboard

Length of output: 215


🏁 Script executed:

rg "function EditableValue|const EditableValue|export.*EditableValue" -A 3

Repository: netbirdio/dashboard

Length of output: 253


🏁 Script executed:

rg "EditableValue" --no-heading | head -30

Repository: netbirdio/dashboard

Length of output: 249


🏁 Script executed:

rg "EditableValue" -l

Repository: netbirdio/dashboard

Length of output: 96


🏁 Script executed:

cat src/app/\(dashboard\)/peer/page.tsx | head -100

Repository: netbirdio/dashboard

Length of output: 3849


🏁 Script executed:

find . -name "*.tsx" -o -name "*.ts" | xargs grep -l "EditableValue" | head -10

Repository: netbirdio/dashboard

Length of output: 98


🏁 Script executed:

wc -l src/app/\(dashboard\)/peer/page.tsx

Repository: netbirdio/dashboard

Length of output: 100


🏁 Script executed:

sed -n '800,850p' src/app/\(dashboard\)/peer/page.tsx

Repository: netbirdio/dashboard

Length of output: 731


🏁 Script executed:

sed -n '525,565p' src/app/\(dashboard\)/peer/page.tsx

Repository: netbirdio/dashboard

Length of output: 1220


🏁 Script executed:

rg "EditableValue" src/app/\(dashboard\)/peer/page.tsx -B 2 -A 5

Repository: netbirdio/dashboard

Length of output: 674


Add an accessible name for the icon-only edit button.

The edit button in EditableValue is icon-only (displaying only a pencil icon) and lacks an aria-label, making it inaccessible to assistive technology users. Distinguishing between IPv4 and IPv6 edit actions requires context-specific labels.

♿ Proposed fix
 function EditableValue({
   value,
   canEdit,
   onEdit,
+  editAriaLabel,
 }: {
   value: string;
   canEdit: boolean;
   onEdit: () => void;
+  editAriaLabel: string;
 }) {
@@
       {canEdit && (
         <button
+          type="button"
+          aria-label={editAriaLabel}
           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();
           }}
         >
@@
               <EditableValue
                 value={peer.ip}
                 canEdit={permission.peers.update}
                 onEdit={() => setShowEditIPModal(true)}
+                editAriaLabel={"Edit NetBird IP Address"}
               />
@@
                 <EditableValue
                   value={peer.ipv6}
                   canEdit={permission.peers.update}
                   onEdit={() => setShowEditIPv6Modal(true)}
+                  editAriaLabel={"Edit NetBird IPv6 Address"}
                 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(dashboard)/peer/page.tsx around lines 535 - 560, The edit button
rendered by the EditableValue component is icon-only and missing an accessible
name; update the EditableValue usage for the IPv4 and IPv6 entries (where
value={peer.ip} with onEdit={() => setShowEditIPModal(true)} and
value={peer.ipv6} with onEdit={() => setShowEditIPv6Modal(true)}) to provide a
context-specific aria-label (e.g., "Edit IPv4 address" and "Edit IPv6 address")
or add a prop (like editAriaLabel or ariaLabel) to EditableValue and pass the
labels through so the icon-only button is announced correctly by assistive
technologies.

}
/>
)}

<Card.ListItem
copy
copyText={"Public IP Address"}
label={
<>
<NetworkIcon size={16} />
<NetworkIcon size={16} className={"shrink-0"} />
Public IP Address
</>
}
Expand All @@ -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
</>
}
Expand All @@ -560,7 +597,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
copyText={"Hostname"}
label={
<>
<MonitorSmartphoneIcon size={16} />
<MonitorSmartphoneIcon size={16} className={"shrink-0"} />
Hostname
</>
}
Expand All @@ -570,7 +607,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<FlagIcon size={16} />
<FlagIcon size={16} className={"shrink-0"} />
Region
</>
}
Expand Down Expand Up @@ -600,7 +637,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<Cpu size={16} />
<Cpu size={16} className={"shrink-0"} />
Operating System
</>
}
Expand All @@ -611,7 +648,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<Barcode size={16} />
<Barcode size={16} className={"shrink-0"} />
Serial Number
</>
}
Expand All @@ -623,7 +660,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<CalendarDays size={16} />
<CalendarDays size={16} className={"shrink-0"} />
Registered on
</>
}
Expand All @@ -639,7 +676,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<History size={16} />
<History size={16} className={"shrink-0"} />
Last seen
</>
}
Expand All @@ -656,7 +693,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<NetBirdIcon size={16} />
<NetBirdIcon size={16} className={"shrink-0"} />
Agent Version
</>
}
Expand All @@ -667,7 +704,7 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
<Card.ListItem
label={
<>
<NetBirdIcon size={16} />
<NetBirdIcon size={16} className={"shrink-0"} />
UI Version
</>
}
Expand Down Expand Up @@ -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>
);
}
Loading
Loading