ChannelsTable.tsx raw
1 import { InfoIcon } from "lucide-react";
2 import { ChannelWarning } from "src/components/channels/ChannelWarning";
3 import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
4 import Loading from "src/components/Loading.tsx";
5 import { Badge } from "src/components/ui/badge.tsx";
6 import {
7 Card,
8 CardContent,
9 CardHeader,
10 CardTitle,
11 } from "src/components/ui/card";
12 import { Progress } from "src/components/ui/progress.tsx";
13 import {
14 Table,
15 TableBody,
16 TableCell,
17 TableHead,
18 TableHeader,
19 TableRow,
20 } from "src/components/ui/table.tsx";
21 import {
22 Tooltip,
23 TooltipContent,
24 TooltipProvider,
25 TooltipTrigger,
26 } from "src/components/ui/tooltip.tsx";
27 import { useNodeDetails } from "src/hooks/useNodeDetails";
28 import { Channel, LongUnconfirmedZeroConfChannel } from "src/types";
29 import { ChannelDropdownMenu } from "./ChannelDropdownMenu";
30
31 type ChannelsTableProps = {
32 channels?: Channel[];
33 longUnconfirmedZeroConfChannels: LongUnconfirmedZeroConfChannel[];
34 };
35
36 export function ChannelsTable({
37 channels,
38 longUnconfirmedZeroConfChannels,
39 }: ChannelsTableProps) {
40 if (channels && !channels.length) {
41 return null;
42 }
43
44 return (
45 <Card className="hidden lg:block">
46 <CardHeader>
47 <CardTitle className="text-2xl">Channels</CardTitle>
48 </CardHeader>
49 <CardContent>
50 <Table>
51 <TableHeader>
52 <TableRow>
53 <TableHead className="w-[160px] text-muted-foreground">
54 Peer
55 </TableHead>
56 <TableHead className="w-[80px] text-muted-foreground">
57 <TooltipProvider>
58 <Tooltip>
59 <TooltipTrigger>
60 <div className="flex flex-row gap-1 items-center text-muted-foreground">
61 Type
62 <InfoIcon className="h-3 w-3 shrink-0" />
63 </div>
64 </TooltipTrigger>
65 <TooltipContent>
66 The type of lightning channel, By default private channel
67 is recommended. If you a podcaster or musician and expect
68 to receive keysend or Value4Value payments you will need a
69 public channel.
70 </TooltipContent>
71 </Tooltip>
72 </TooltipProvider>
73 </TableHead>
74 <TableHead className="w-[80px] text-muted-foreground">
75 Status
76 </TableHead>
77 <TableHead className="w-[128px] text-muted-foreground">
78 <TooltipProvider>
79 <Tooltip>
80 <TooltipTrigger>
81 <div className="flex flex-row gap-1 items-center text-muted-foreground">
82 Capacity
83 <InfoIcon className="h-3 w-3 shrink-0" />
84 </div>
85 </TooltipTrigger>
86 <TooltipContent>
87 Total Spending and Receiving capacity of your lightning
88 channel.
89 </TooltipContent>
90 </Tooltip>
91 </TooltipProvider>
92 </TableHead>
93 <TableHead className="w-[128px]">
94 <TooltipProvider>
95 <Tooltip>
96 <TooltipTrigger>
97 <div className="flex flex-row gap-1 items-center text-muted-foreground">
98 Reserve
99 <InfoIcon className="h-3 w-3 shrink-0" />
100 </div>
101 </TooltipTrigger>
102 <TooltipContent>
103 Funds each participant sets aside to discourage cheating
104 by ensuring each party has something at stake. This
105 reserve cannot be spent during the channel's lifetime and
106 typically amounts to 1% of the channel capacity. The
107 reserve will automatically be filled when payments are
108 received on the channel.
109 </TooltipContent>
110 </Tooltip>
111 </TooltipProvider>
112 </TableHead>
113 <TableHead className="w-[350px]">
114 <div className="flex flex-row justify-between items-center gap-2 text-muted-foreground">
115 <div>Spending</div>
116 <div>Receiving</div>
117 </div>
118 </TableHead>
119 <TableHead className="w-px"></TableHead>
120 <TableHead className="w-[50px]"></TableHead>
121 </TableRow>
122 </TableHeader>
123 <TableBody>
124 {channels && channels.length > 0 && (
125 <>
126 {channels
127 .sort((a, b) =>
128 a.localBalanceMsat + a.remoteBalanceMsat >
129 b.localBalanceMsat + b.remoteBalanceMsat
130 ? -1
131 : 1
132 )
133 .map((channel) => {
134 const unconfirmedChannel =
135 longUnconfirmedZeroConfChannels.find(
136 (uc) => uc.id === channel.id
137 );
138 return (
139 <ChannelTableRow
140 key={channel.id}
141 channel={channel}
142 unconfirmedChannel={unconfirmedChannel}
143 hasMultipleChannels={channels.length > 1}
144 />
145 );
146 })}
147 </>
148 )}
149 {!channels && (
150 <TableRow>
151 <TableCell colSpan={6}>
152 <Loading className="m-2" />
153 </TableCell>
154 </TableRow>
155 )}
156 </TableBody>
157 </Table>
158 </CardContent>
159 </Card>
160 );
161 }
162
163 type ChannelTableRowProps = {
164 channel: Channel;
165 unconfirmedChannel: LongUnconfirmedZeroConfChannel | undefined;
166 hasMultipleChannels: boolean;
167 };
168
169 function ChannelTableRow({
170 channel,
171 unconfirmedChannel,
172 hasMultipleChannels,
173 }: ChannelTableRowProps) {
174 const { data: peerDetails } = useNodeDetails(channel.remotePubkey);
175 const capacityMsat = channel.localBalanceMsat + channel.remoteBalanceMsat;
176 const alias = peerDetails?.alias || "Unknown";
177
178 return (
179 <TableRow key={channel.id} className="channel">
180 <TableCell>
181 <span className="font-semibold text-sm mr-2">{alias}</span>
182 </TableCell>
183 <TableCell>{channel.public ? "Public" : "Private"}</TableCell>
184 <TableCell>
185 {channel.status == "online" ? (
186 unconfirmedChannel ? (
187 <Badge variant="outline" title={unconfirmedChannel.message}>
188 Unconfirmed
189 </Badge>
190 ) : (
191 <Badge variant="positive">Online</Badge>
192 )
193 ) : channel.status == "opening" ? (
194 <Badge variant="outline">Opening</Badge>
195 ) : (
196 <Badge variant="warning">Offline</Badge>
197 )}
198 </TableCell>
199 <TableCell>
200 <FormattedBitcoinAmount amountMsat={capacityMsat} />
201 </TableCell>
202 <TableCell title={channel.unspendablePunishmentReserveSat + " sats"}>
203 {channel.localBalanceMsat <
204 channel.unspendablePunishmentReserveSat * 1000 && (
205 <>
206 <FormattedBitcoinAmount
207 amountMsat={Math.min(
208 channel.localBalanceMsat,
209 channel.unspendablePunishmentReserveSat * 1000
210 )}
211 showSymbol={false}
212 />{" "}
213 /{" "}
214 </>
215 )}
216 <FormattedBitcoinAmount
217 amountMsat={channel.unspendablePunishmentReserveSat * 1000}
218 />
219 </TableCell>
220 <TableCell>
221 <div className="relative">
222 <Progress
223 value={(channel.localSpendableBalanceMsat / capacityMsat) * 100}
224 className="h-6 absolute"
225 />
226 <div className="flex flex-row w-full justify-between px-2 text-xs items-center h-6 mix-blend-exclusion text-white">
227 <span>
228 <FormattedBitcoinAmount
229 amountMsat={channel.localSpendableBalanceMsat}
230 />
231 </span>
232 <span>
233 <FormattedBitcoinAmount amountMsat={channel.remoteBalanceMsat} />
234 </span>
235 </div>
236 </div>
237 </TableCell>
238 <TableCell>
239 <ChannelWarning channel={channel} />
240 </TableCell>
241 <TableCell>
242 <ChannelDropdownMenu
243 alias={alias}
244 channel={channel}
245 hasMultipleChannels={hasMultipleChannels}
246 />
247 </TableCell>
248 </TableRow>
249 );
250 }
251