Browse Source

feat(channels): display details of channels in the sidebar

feat/auto-update
jamaljsr 5 years ago
parent
commit
3041347e8d
  1. 1
      TODO.md
  2. 28
      src/components/common/DetailsList.tsx
  3. 6
      src/components/designer/Sidebar.tsx
  4. 45
      src/components/designer/link/Backend.tsx
  5. 45
      src/components/designer/link/Channel.tsx
  6. 39
      src/components/designer/link/LinkDetails.tsx
  7. 2
      src/components/designer/lnd/ActionsTab.tsx
  8. 12
      src/components/designer/lnd/ConnectTab.tsx
  9. 36
      src/utils/chart.ts

1
TODO.md

@ -18,6 +18,7 @@ Bigger things
- add block explorer (https://github.com/janoside/btc-rpc-explorer)
- add grpc API explorer (https://github.com/grpc-ecosystem/awesome-grpc#gui)
- add support for c-lightning, eclair, btcd
- add block explorer (https://github.com/janoside/btc-rpc-explorer)
- live theme changer (https://medium.com/@mzohaib.qc/ant-design-dynamic-runtime-theme-1f9a1a030ba0)
- dark theme UI (https://material.io/design/color/dark-theme.html)
- POC testing-library for testcafe (https://testing-library.com/docs/testcafe-testing-library/intro)

28
src/components/common/DetailsList.tsx

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
const Styled = {
Details: styled.table`
width: 100%;
margin: 0;
margin: 0 0 30px 0;
`,
Row: styled.tr`
border-bottom: 1px solid rgba(#000, 0.05);
@ -40,20 +40,24 @@ export type DetailValues = {
interface Props {
details: DetailValues;
title?: string;
}
const DetailsList: React.SFC<Props> = ({ details }) => {
const DetailsList: React.SFC<Props> = ({ details, title }) => {
return (
<Styled.Details>
<tbody>
{details.map((d, i) => (
<Styled.Row key={i}>
<Styled.LabelCell>{d.label}</Styled.LabelCell>
<Styled.ValueCell>{d.value}</Styled.ValueCell>
</Styled.Row>
))}
</tbody>
</Styled.Details>
<>
{title && <h3>{title}</h3>}
<Styled.Details>
<tbody>
{details.map((d, i) => (
<Styled.Row key={i}>
<Styled.LabelCell>{d.label}</Styled.LabelCell>
<Styled.ValueCell>{d.value}</Styled.ValueCell>
</Styled.Row>
))}
</tbody>
</Styled.Details>
</>
);
};

6
src/components/designer/Sidebar.tsx

@ -5,6 +5,7 @@ import { Button, Tooltip } from 'antd';
import { useStoreActions } from 'store';
import { Network, Status } from 'types';
import BitcoindDetails from './bitcoind/BitcoindDetails';
import LinkDetails from './link/LinkDetails';
import LndDetails from './lnd/LndDetails';
import SidebarCard from './SidebarCard';
@ -20,7 +21,7 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
try {
await syncChart(network);
redrawChart();
notify({ message: 'The network design has been synced with the Lightning nodes' });
notify({ message: 'The designer has been synced with the Lightning nodes' });
} catch (error) {
notify({ message: 'Failed to sync the network', error });
}
@ -37,6 +38,9 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
} else if (node && node.implementation === 'LND') {
return <LndDetails node={node} />;
}
} else if (type === 'link' && id) {
const link = chart.links[id];
return <LinkDetails link={link} network={network} />;
}
return (

45
src/components/designer/link/Backend.tsx

@ -0,0 +1,45 @@
import React from 'react';
import { BitcoinNode, LightningNode, Status } from 'types';
import { StatusBadge } from 'components/common';
import DetailsList, { DetailValues } from 'components/common/DetailsList';
import SidebarCard from '../SidebarCard';
interface Props {
bitcoinNode: BitcoinNode;
lightningNode: LightningNode;
}
const Backend: React.FC<Props> = ({ bitcoinNode, lightningNode }) => {
const backendDetails: DetailValues = [
{ label: 'Name', value: bitcoinNode.name },
{ label: 'Implementation', value: bitcoinNode.implementation },
{ label: 'Version', value: `v${bitcoinNode.version}` },
{
label: 'Status',
value: (
<StatusBadge status={bitcoinNode.status} text={Status[bitcoinNode.status]} />
),
},
];
const lightningDetails: DetailValues = [
{ label: 'Name', value: lightningNode.name },
{ label: 'Implementation', value: lightningNode.implementation },
{ label: 'Version', value: `v${lightningNode.version}` },
{
label: 'Status',
value: (
<StatusBadge status={lightningNode.status} text={Status[lightningNode.status]} />
),
},
];
return (
<SidebarCard title="Blockchain Backend Connection">
<DetailsList title="Lightning Node" details={lightningDetails} />
<DetailsList title="Bitcoin Node" details={backendDetails} />
</SidebarCard>
);
};
export default Backend;

45
src/components/designer/link/Channel.tsx

@ -0,0 +1,45 @@
import React from 'react';
import { ILink } from '@mrblenny/react-flow-chart';
import { LightningNode, Status } from 'types';
import { LinkProperties } from 'utils/chart';
import { format } from 'utils/units';
import { DetailsList, StatusBadge } from 'components/common';
import { DetailValues } from 'components/common/DetailsList';
import SidebarCard from '../SidebarCard';
interface Props {
link: ILink;
from: LightningNode;
to: LightningNode;
}
const Channel: React.FC<Props> = ({ link, from, to }) => {
const { fromBalance, toBalance, capacity, status } = link.properties as LinkProperties;
const channelDetails: DetailValues = [
{ label: 'Status', value: status },
{ label: 'Capacity', value: `${format(capacity)} sats` },
{ label: 'Source Balance', value: `${format(fromBalance)} sats` },
{ label: 'Destination Balance', value: `${format(toBalance)} sats` },
];
const [fromDetails, toDetails] = [from, to].map(node => [
{ label: 'Name', value: node.name },
{ label: 'Implementation', value: node.implementation },
{ label: 'Version', value: `v${node.version}` },
{
label: 'Status',
value: <StatusBadge status={node.status} text={Status[node.status]} />,
},
]);
return (
<SidebarCard title="Channel Details">
<DetailsList details={channelDetails} />
<DetailsList title="Source Node" details={fromDetails} />
<DetailsList title="Destination Node" details={toDetails} />
</SidebarCard>
);
};
export default Channel;

39
src/components/designer/link/LinkDetails.tsx

@ -0,0 +1,39 @@
import React from 'react';
import { ILink } from '@mrblenny/react-flow-chart';
import { Network } from 'types';
import { LinkProperties } from 'utils/chart';
import Backend from './Backend';
import Channel from './Channel';
interface Props {
link: ILink;
network: Network;
}
const LinkDetails: React.FC<Props> = ({ link, network }) => {
let cmp = <div>You&apos;ve somehow managed to select an invalid link</div>;
const { bitcoin, lightning } = network.nodes;
const { type } = link.properties as LinkProperties;
switch (type) {
case 'backend':
const bitcoinNode = bitcoin.find(n => n.name === link.to.nodeId);
const lightningNode = lightning.find(n => n.name === link.from.nodeId);
if (bitcoinNode && lightningNode) {
cmp = <Backend bitcoinNode={bitcoinNode} lightningNode={lightningNode} />;
}
break;
case 'open-channel':
case 'pending-channel':
const from = lightning.find(n => n.name === link.from.nodeId);
const to = lightning.find(n => n.name === link.to.nodeId);
if (from && to) {
cmp = <Channel link={link} from={from} to={to} />;
}
break;
}
return cmp;
};
export default LinkDetails;

2
src/components/designer/lnd/ActionsTab.tsx

@ -12,7 +12,7 @@ const ActionsTab: React.FC<Props> = ({ node }) => {
const { showOpenChannel } = useStoreActions(s => s.modals);
if (node.status !== Status.Started) {
return <>Start the network to interact with this node</>;
return <>Node needs to be started to interact with this node</>;
}
return (

12
src/components/designer/lnd/ConnectTab.tsx

@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useAsync } from 'react-async-hook';
import styled from '@emotion/styled';
import { Alert, Form, Radio } from 'antd';
import { useStoreState } from 'store';
import { LndNode, Status } from 'types';
import { readHex } from 'utils/files';
import CopyableInput from 'components/common/form/CopyableInput';
@ -29,8 +30,14 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
});
}, [node.paths]);
let lnUrl = '';
const nodeState = useStoreState(s => s.lnd.nodes[node.name]);
if (nodeState && nodeState.info) {
lnUrl = nodeState.info.uris[0];
}
if (node.status !== Status.Started) {
return <>Start the network to view connection info</>;
return <>Node needs to be started to view connection info</>;
}
const values = fileType === 'paths' ? node.paths : hexValues;
@ -38,6 +45,9 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
return (
<>
<Form labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<Form.Item label="LN Url">
<CopyableInput value={lnUrl} label="LN Url" />
</Form.Item>
<Form.Item label="GRPC Host">
<CopyableInput value={`127.0.0.1:${node.ports.grpc}`} label="GRPC Host" />
</Form.Item>

36
src/utils/chart.ts

@ -7,10 +7,11 @@ import lndlogo from 'resources/lnd.png';
export interface LinkProperties {
type: 'backend' | 'pending-channel' | 'open-channel';
capacity: 'string';
fromBalance: 'string';
toBalance: 'string';
capacity: string;
fromBalance: string;
toBalance: string;
direction: 'ltr' | 'rtl';
status: string;
}
export const initChartFromNetwork = (network: Network): IChart => {
@ -87,6 +88,7 @@ interface ChannelInfo {
capacity: string;
localBalance: string;
remoteBalance: string;
status: string;
}
const mapOpenChannel = (chan: Channel): ChannelInfo => ({
@ -96,27 +98,29 @@ const mapOpenChannel = (chan: Channel): ChannelInfo => ({
capacity: chan.capacity,
localBalance: chan.localBalance,
remoteBalance: chan.remoteBalance,
status: 'Open',
});
const mapPendingChannel = (chan: PendingChannel): ChannelInfo => ({
const mapPendingChannel = (status: string) => (chan: PendingChannel): ChannelInfo => ({
pending: true,
uniqueId: chan.channelPoint.slice(-12),
pubkey: chan.remoteNodePub,
capacity: chan.capacity,
localBalance: chan.localBalance,
remoteBalance: chan.remoteBalance,
status,
});
const updateLinksAndPorts = (
channel: ChannelInfo,
info: ChannelInfo,
pubkeys: Record<string, string>,
nodes: { [x: string]: INode },
fromNode: INode,
links: { [x: string]: ILink },
) => {
// use the channel point as a unique id since pending channels do not have a channel id yet
const chanId = channel.uniqueId;
const toName = pubkeys[channel.pubkey];
const chanId = info.uniqueId;
const toName = pubkeys[info.pubkey];
const toNode = nodes[toName];
const fromOnLeftSide = fromNode.position.x < toNode.position.x;
@ -141,11 +145,12 @@ const updateLinksAndPorts = (
from: { nodeId: fromNode.id, portId: chanId },
to: { nodeId: toName, portId: chanId },
properties: {
type: channel.pending ? 'pending-channel' : 'open-channel',
capacity: channel.capacity,
fromBalance: channel.localBalance,
toBalance: channel.remoteBalance,
type: info.pending ? 'pending-channel' : 'open-channel',
capacity: info.capacity,
fromBalance: info.localBalance,
toBalance: info.remoteBalance,
direction: fromOnLeftSide ? 'ltr' : 'rtl',
status: info.status,
},
};
};
@ -174,12 +179,13 @@ export const updateChartFromNetwork = (
const { open, opening, closing, forceClosing, waitingClose } = data.channels;
// merge all of the channel types into one array
const mapPending = (c: any) => c.channel as PendingChannel;
const allChannels = [
...open.filter(c => c.initiator).map(mapOpenChannel),
...opening.map(c => c.channel as PendingChannel).map(mapPendingChannel),
...closing.map(c => c.channel as PendingChannel).map(mapPendingChannel),
...forceClosing.map(c => c.channel as PendingChannel).map(mapPendingChannel),
...waitingClose.map(c => c.channel as PendingChannel).map(mapPendingChannel),
...opening.map(mapPending).map(mapPendingChannel('Opening')),
...closing.map(mapPending).map(mapPendingChannel('Closing')),
...forceClosing.map(mapPending).map(mapPendingChannel('Force Closing')),
...waitingClose.map(mapPending).map(mapPendingChannel('Waiting to Close')),
];
allChannels.forEach(channel => {

Loading…
Cancel
Save