Peers.tsx raw
1 import {
2 MoreHorizontalIcon,
3 PlugZapIcon,
4 RadioIcon,
5 Trash2Icon,
6 } from "lucide-react";
7 import React from "react";
8 import { toast } from "sonner";
9 import AppHeader from "src/components/AppHeader.tsx";
10 import { DisconnectPeerDialogContent } from "src/components/DisconnectPeerDialogContent";
11 import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
12 import { AlertDialog } from "src/components/ui/alert-dialog.tsx";
13 import { Badge } from "src/components/ui/badge.tsx";
14 import { Button } from "src/components/ui/button.tsx";
15 import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
16 import {
17 DropdownMenu,
18 DropdownMenuContent,
19 DropdownMenuItem,
20 DropdownMenuTrigger,
21 } from "src/components/ui/dropdown-menu.tsx";
22 import {
23 Table,
24 TableBody,
25 TableCell,
26 TableHead,
27 TableHeader,
28 TableRow,
29 } from "src/components/ui/table.tsx";
30 import { useChannels } from "src/hooks/useChannels";
31 import { useNodeDetails } from "src/hooks/useNodeDetails";
32 import { usePeers } from "src/hooks/usePeers.ts";
33 import { useSyncWallet } from "src/hooks/useSyncWallet.ts";
34 import { splitSocketAddress } from "src/lib/utils";
35 import { ConnectPeerRequest, Peer } from "src/types";
36 import { request } from "src/utils/request";
37
38 export default function Peers() {
39 useSyncWallet();
40 const { data: peers } = usePeers();
41 const [peerToDisconnect, setPeerToDisconnect] = React.useState<Peer>();
42
43 return (
44 <>
45 <AppHeader
46 title="Peers"
47 pageTitle="Peers"
48 description="Manage your connections with other lightning nodes"
49 contentRight={
50 <ResponsiveLinkButton
51 text="Connect Peer"
52 icon={RadioIcon}
53 to="/peers/new"
54 />
55 }
56 />
57 <Table>
58 <TableHeader>
59 <TableRow>
60 <TableHead className="w-[80px]">Status</TableHead>
61 <TableHead>Node</TableHead>
62 <TableHead>Pubkey</TableHead>
63 <TableHead>Address</TableHead>
64 <TableHead className="w-[50px]"></TableHead>
65 </TableRow>
66 </TableHeader>
67 <TableBody>
68 {peers?.map((peer) => (
69 <PeerTableRow
70 key={peer.nodeId}
71 peer={peer}
72 setPeerToDisconnect={setPeerToDisconnect}
73 />
74 ))}
75 </TableBody>
76 </Table>
77
78 <AlertDialog
79 open={!!peerToDisconnect}
80 onOpenChange={(open) => {
81 if (!open) {
82 setPeerToDisconnect(undefined);
83 }
84 }}
85 >
86 {peerToDisconnect && (
87 <DisconnectPeerDialogContent peer={peerToDisconnect} />
88 )}
89 </AlertDialog>
90 </>
91 );
92 }
93
94 type PeerTableRowProps = {
95 peer: Peer;
96 setPeerToDisconnect: (peer: Peer) => void;
97 };
98
99 function PeerTableRow(props: PeerTableRowProps) {
100 const { peer } = props;
101 const { data: channels } = useChannels();
102 const { data: peerDetails } = useNodeDetails(peer.nodeId);
103
104 function hasOpenedChannels(peer: Peer) {
105 return channels?.some((channel) => channel.remotePubkey === peer.nodeId);
106 }
107
108 const connectPeer = async (peer: Peer) => {
109 try {
110 const { address, port } = splitSocketAddress(peer.address);
111
112 if (!address || !port) {
113 throw new Error("host not found");
114 }
115 console.info(`🔌 Peering with ${peer.nodeId}`);
116 const connectPeerRequest: ConnectPeerRequest = {
117 pubkey: peer.nodeId,
118 address,
119 port: +port,
120 };
121 await request("/api/peers", {
122 method: "POST",
123 headers: {
124 "Content-Type": "application/json",
125 },
126 body: JSON.stringify(connectPeerRequest),
127 });
128 toast("Peer connected");
129 } catch (error) {
130 console.error(error);
131 toast.error("Something went wrong", {
132 description: "" + error,
133 });
134 }
135 };
136
137 return (
138 <TableRow key={peer.nodeId}>
139 <TableCell>
140 {peer.isConnected ? (
141 <Badge>Online</Badge>
142 ) : (
143 <Badge variant="outline">Offline</Badge>
144 )}{" "}
145 </TableCell>
146 <TableCell className="flex flex-row items-center">
147 <ExternalLinkButton
148 to={`https://amboss.space/node/${peer.nodeId}`}
149 variant="link"
150 className="p-2"
151 >
152 {peerDetails?.alias}
153 </ExternalLinkButton>
154 </TableCell>
155 <TableCell>{peer.nodeId}</TableCell>
156 <TableCell>{peer.address}</TableCell>
157 <TableCell>
158 {(!hasOpenedChannels(peer) || !peer.isConnected) && (
159 <DropdownMenu modal={false}>
160 <Button size="icon" variant="ghost" asChild>
161 <DropdownMenuTrigger>
162 <MoreHorizontalIcon className="h-4 w-4" />
163 </DropdownMenuTrigger>
164 </Button>
165 {channels && (
166 <DropdownMenuContent align="end">
167 {!hasOpenedChannels(peer) && (
168 <DropdownMenuItem
169 onClick={() => props.setPeerToDisconnect(peer)}
170 className="flex flex-row items-center gap-2"
171 >
172 <Trash2Icon className="size-4 text-destructive" />
173 Disconnect Peer
174 </DropdownMenuItem>
175 )}
176 {!peer.isConnected && (
177 <DropdownMenuItem
178 onClick={() => connectPeer(peer)}
179 className="flex flex-row items-center gap-2"
180 >
181 <PlugZapIcon className="size-4" />
182 Try to Reconnect
183 </DropdownMenuItem>
184 )}
185 </DropdownMenuContent>
186 )}
187 </DropdownMenu>
188 )}
189 </TableCell>
190 </TableRow>
191 );
192 }
193