mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-28 11:24:28 +01:00
peers and channels details / actions
This commit is contained in:
@@ -1,15 +1,50 @@
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Card, Hr, SmallHeader, Button, InnerCard } from "~/components/layout";
|
||||
import { Card, Hr, SmallHeader, Button, InnerCard, VStack } from "~/components/layout";
|
||||
import PeerConnectModal from "~/components/PeerConnectModal";
|
||||
import { For, Show, Suspense, createResource, createSignal } from "solid-js";
|
||||
import { For, Show, Suspense, createEffect, createResource, createSignal, onCleanup } from "solid-js";
|
||||
import { MutinyChannel, MutinyPeer } from "@mutinywallet/mutiny-wasm";
|
||||
import { TextField } from "@kobalte/core";
|
||||
import { Collapsible, TextField, toaster } from "@kobalte/core";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import eify from "~/utils/eify";
|
||||
import { ConfirmDialog } from "./Dialog";
|
||||
import { ToastItem, showToast } from "./Toaster";
|
||||
|
||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||
type RefetchPeersType = (info?: unknown) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined
|
||||
|
||||
function PeerItem(props: { peer: MutinyPeer }) {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
const handleDisconnectPeer = async () => {
|
||||
const nodes = await state.node_manager?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
|
||||
if (props.peer.is_connected) {
|
||||
await state.node_manager?.disconnect_peer(firstNode, props.peer.pubkey);
|
||||
} else {
|
||||
await state.node_manager?.delete_peer(firstNode, props.peer.pubkey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.peer.alias ? props.peer.alias : props.peer.pubkey}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.peer, null, 2)}
|
||||
</pre>
|
||||
<Button intent="glowy" layout="xs" onClick={handleDisconnectPeer}>Disconnect</Button>
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function PeersList() {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
@@ -19,6 +54,16 @@ function PeersList() {
|
||||
|
||||
const [peers, { refetch }] = createResource(getPeers);
|
||||
|
||||
createEffect(() => {
|
||||
// refetch peers every 5 seconds
|
||||
const interval = setTimeout(() => {
|
||||
refetch();
|
||||
}, 5000);
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<SmallHeader>
|
||||
@@ -26,13 +71,13 @@ function PeersList() {
|
||||
</SmallHeader>
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<For each={peers()} fallback={<code>No peers</code>}>
|
||||
{(peer) => (
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(peer, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</For>
|
||||
<VStack>
|
||||
<For each={peers()} fallback={<code>No peers</code>}>
|
||||
{(peer) => (
|
||||
<PeerItem peer={peer} />
|
||||
)}
|
||||
</For>
|
||||
</VStack>
|
||||
</Suspense>
|
||||
<Button layout="small" onClick={refetch}>Refresh Peers</Button>
|
||||
<ConnectPeer refetchPeers={refetch} />
|
||||
@@ -81,6 +126,54 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
|
||||
type RefetchChannelsListType = (info?: unknown) => MutinyChannel[] | Promise<MutinyChannel[] | undefined> | null | undefined
|
||||
|
||||
function ChannelItem(props: { channel: MutinyChannel, network?: string }) {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
const [confirmOpen, setConfirmOpen] = createSignal(false);
|
||||
const [confirmLoading, setConfirmLoading] = createSignal(false);
|
||||
|
||||
function handleCloseChannel() {
|
||||
setConfirmOpen(true);
|
||||
}
|
||||
|
||||
async function confirmCloseChannel() {
|
||||
setConfirmLoading(true);
|
||||
try {
|
||||
await state.node_manager?.close_channel(props.channel.outpoint as string)
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
setConfirmLoading(false);
|
||||
setConfirmOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapsible.Root>
|
||||
<Collapsible.Trigger class="w-full">
|
||||
<h2 class="truncate text-start text-lg font-mono bg-neutral-200 text-black rounded px-4 py-2">
|
||||
{">"} {props.channel.peer}
|
||||
</h2>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<VStack>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(props.channel, null, 2)}
|
||||
</pre>
|
||||
<a class="" href={mempoolTxUrl(props.channel.outpoint?.split(":")[0], props.network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
<Button intent="glowy" layout="xs" onClick={handleCloseChannel}>Close Channel</Button>
|
||||
|
||||
</VStack>
|
||||
<ConfirmDialog isOpen={confirmOpen()} onConfirm={confirmCloseChannel} onCancel={() => setConfirmOpen(false)} loading={confirmLoading()}>
|
||||
<p>Are you sure you want to close this channel?</p>
|
||||
</ConfirmDialog>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ChannelsList() {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
@@ -90,6 +183,16 @@ function ChannelsList() {
|
||||
|
||||
const [channels, { refetch }] = createResource(getChannels);
|
||||
|
||||
createEffect(() => {
|
||||
// refetch channels every 5 seconds
|
||||
const interval = setTimeout(() => {
|
||||
refetch();
|
||||
}, 5000);
|
||||
onCleanup(() => {
|
||||
clearInterval(interval);
|
||||
});
|
||||
})
|
||||
|
||||
const network = state.node_manager?.get_network();
|
||||
|
||||
return (
|
||||
@@ -101,14 +204,7 @@ function ChannelsList() {
|
||||
<Suspense>
|
||||
<For each={channels()} fallback={<code>No channels</code>}>
|
||||
{(channel) => (
|
||||
<>
|
||||
<pre class="overflow-x-auto whitespace-pre-wrap break-all">
|
||||
{JSON.stringify(channel, null, 2)}
|
||||
</pre>
|
||||
<a class="text-sm font-light opacity-50 mt-2" href={mempoolTxUrl(channel.outpoint?.split(":")[0], network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
</>
|
||||
<ChannelItem channel={channel} network={network} />
|
||||
)}
|
||||
|
||||
</For>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { ButtonLink, SmallHeader } from "~/components/layout";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
|
||||
@@ -12,14 +13,14 @@ export function SentModal(props: { details?: { nice: string } }) {
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<div class="flex justify-between mb-2 items-center">
|
||||
<Dialog.Title>
|
||||
<SmallHeader>
|
||||
Sent!
|
||||
</SmallHeader>
|
||||
</Dialog.Title>
|
||||
<Dialog.CloseButton class="dialog__close-button">
|
||||
<code>X</code>
|
||||
<Dialog.CloseButton class="p-2 hover:bg-white/10 rounded-lg active:bg-m-blue">
|
||||
<img src={close} alt="Close" />
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
|
||||
@@ -10,7 +10,7 @@ const SmallHeader: ParentComponent<{ class?: string }> = (props) => {
|
||||
|
||||
const Card: ParentComponent<{ title?: string }> = (props) => {
|
||||
return (
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950'>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
|
||||
{props.title && <SmallHeader>{props.title}</SmallHeader>}
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
@@ -229,7 +229,7 @@ export default function Receive() {
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<ReceiveSuccessModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto" />
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
|
||||
</div>
|
||||
</ReceiveSuccessModal>
|
||||
@@ -237,7 +237,7 @@ export default function Receive() {
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<ReceiveSuccessModal title="Payment Received!" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto" />
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat />
|
||||
<a href={mempoolTxUrl(paymentTx()?.txid, "signet")} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
|
||||
Reference in New Issue
Block a user