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
20 changes: 19 additions & 1 deletion src/app/(dashboard)/peer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
import RoutesProvider from "@/contexts/RoutesProvider";
import { useHasChanges } from "@/hooks/useHasChanges";
import type { Group } from "@/interfaces/Group";
import type { Peer } from "@/interfaces/Peer";
import { type Peer, peerMacAddresses } from "@/interfaces/Peer";
import type { User } from "@/interfaces/User";
import PageContainer from "@/layouts/PageContainer";
import useGroupHelper from "@/modules/groups/useGroupHelper";
Expand Down Expand Up @@ -678,6 +678,24 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) {
/>
)}

{peerMacAddresses(peer).length > 0 && (
<Card.ListItem
copy
copyText={"MAC Address"}
label={
<>
<NetworkIcon size={16} className={"shrink-0"} />
MAC Address
</>
}
className={
peerMacAddresses(peer).length > 1 ? "items-start" : ""
}
value={peerMacAddresses(peer)[0]}
extraText={peerMacAddresses(peer).slice(1)}
Comment on lines +681 to +695

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Copying this row drops all but the first MAC address.

Card.ListItem only copies valueToCopy (or value as a fallback), so this new row copies peerMacAddresses(peer)[0] and ignores extraText. For peers with multiple NICs, the clipboard content won't match what the UI shows.

Proposed fix
           {peerMacAddresses(peer).length > 0 && (
             <Card.ListItem
               copy
               copyText={"MAC Address"}
+              valueToCopy={peerMacAddresses(peer).join(", ")}
               label={
                 <>
                   <NetworkIcon size={16} className={"shrink-0"} />
                   MAC Address
                 </>

Based on learnings, the copied content should be passed separately from the notification text, and both should match the displayed value set.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{peerMacAddresses(peer).length > 0 && (
<Card.ListItem
copy
copyText={"MAC Address"}
label={
<>
<NetworkIcon size={16} className={"shrink-0"} />
MAC Address
</>
}
className={
peerMacAddresses(peer).length > 1 ? "items-start" : ""
}
value={peerMacAddresses(peer)[0]}
extraText={peerMacAddresses(peer).slice(1)}
{peerMacAddresses(peer).length > 0 && (
<Card.ListItem
copy
copyText={"MAC Address"}
valueToCopy={peerMacAddresses(peer).join(", ")}
label={
<>
<NetworkIcon size={16} className={"shrink-0"} />
MAC Address
</>
}
className={
peerMacAddresses(peer).length > 1 ? "items-start" : ""
}
value={peerMacAddresses(peer)[0]}
extraText={peerMacAddresses(peer).slice(1)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(dashboard)/peer/page.tsx around lines 681 - 695, The MAC Address
row in Card.ListItem is only copying the first entry because it relies on value
as the fallback copy source while extraText is display-only. Update the
peerMacAddresses(peer) rendering so the clipboard content is built explicitly
from the full set of MAC addresses, and pass that separate copied value through
the copy/copyText behavior in this Card.ListItem usage. Keep the displayed value
and notification text in sync with the complete MAC address list, not just
peerMacAddresses(peer)[0].

Source: Learnings

/>
)}

{peer.created_at && (
<Card.ListItem
label={
Expand Down
2 changes: 1 addition & 1 deletion src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const CardTextItem = ({
return (
<div
className={cn(
"text-right text-nb-gray-400 text-[0.84rem] flex items-center gap-2",
"text-right text-nb-gray-400 text-[0.84rem] flex items-center justify-end gap-2",
copy && "cursor-pointer hover:text-nb-gray-300 transition-all",
)}
onClick={() =>
Expand Down
20 changes: 20 additions & 0 deletions src/interfaces/Peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ export interface Peer {
serial_number: string;
ephemeral: boolean;
local_flags?: PeerLocalFlags;
network_addresses?: NetworkAddress[];
}

export interface NetworkAddress {
net_ip: string;
mac: string;
}

// peerMacAddresses returns the unique, non-empty MAC addresses reported by a
// peer's network interfaces. A peer reports the same MAC for several IPs
// (e.g. IPv4 + IPv6 of one NIC), so we de-duplicate.
export function peerMacAddresses(peer: Peer): string[] {
if (!peer.network_addresses) return [];
return Array.from(
new Set(
peer.network_addresses
.map((address) => address.mac)
.filter((mac) => !!mac),
),
);
}

export interface PeerLocalFlags {
Expand Down
9 changes: 7 additions & 2 deletions src/modules/peers/PeersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { useLocalStorage } from "@/hooks/useLocalStorage";
import { getOperatingSystem } from "@/hooks/useOperatingSystem";
import { Group } from "@/interfaces/Group";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
import { Peer } from "@/interfaces/Peer";
import { Peer, peerMacAddresses } from "@/interfaces/Peer";
import PeerActionCell from "@/modules/peers/PeerActionCell";
import PeerAddressCell from "@/modules/peers/PeerAddressCell";
import PeerGroupCell from "@/modules/peers/PeerGroupCell";
Expand Down Expand Up @@ -262,6 +262,10 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
id: "ipv6",
accessorFn: (row) => row.ipv6,
},
{
id: "mac",
accessorFn: (peer) => peerMacAddresses(peer).join(", "),
},
];

export type PeersTableKind = "users" | "servers";
Expand Down Expand Up @@ -480,7 +484,7 @@ export default function PeersTable({
showResetFilterButton={false}
columns={PeersTableColumns}
data={showBrowserPeers ? browserPeers : regularPeers}
searchPlaceholder={"Search by name, IP, owner or group..."}
searchPlaceholder={"Search by name, IP, MAC, owner or group..."}
columnVisibility={{
select: permission.groups.read,
connected: false,
Expand All @@ -497,6 +501,7 @@ export default function PeersTable({
os: false,
os_kind: false,
ipv6: false,
mac: false,
}}
isLoading={isLoading}
getStartedCard={
Expand Down