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