Browse Source

feat(clightning): add tabs to the clightning sidebar

master
jamaljsr 5 years ago
parent
commit
19fa2a9104
  1. 12
      src/components/designer/clightning/ActionsTab.tsx
  2. 105
      src/components/designer/clightning/CLightningDetails.tsx
  3. 12
      src/components/designer/clightning/ConnectTab.tsx
  4. 96
      src/components/designer/clightning/InfoTab.tsx
  5. 3
      src/components/designer/clightning/index.ts
  6. 29
      src/i18n/locales/en-US.json
  7. 29
      src/i18n/locales/es.json
  8. 4
      src/lib/clightning/clightningService.ts
  9. 6
      src/lib/clightning/clightningTypes.ts
  10. 14
      src/store/models/clightning.ts
  11. 1
      src/types/index.ts
  12. 1
      src/utils/tests.tsx

12
src/components/designer/clightning/ActionsTab.tsx

@ -0,0 +1,12 @@
import React from 'react';
import { CLightningNode } from 'shared/types';
interface Props {
node: CLightningNode;
}
const ConnectTab: React.FC<Props> = ({ node }) => {
return <div>Actions {node.name}</div>;
};
export default ConnectTab;

105
src/components/designer/clightning/CLightningDetails.tsx

@ -1,13 +1,13 @@
import React from 'react';
import React, { ReactNode, useState } from 'react';
import { useAsync } from 'react-async-hook';
import { Alert, Icon, Tooltip } from 'antd';
import { Alert, Icon } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { CLightningNode, Status } from 'shared/types';
import { useStoreActions, useStoreState } from 'store';
import { ellipseInner } from 'utils/strings';
import { CopyIcon, DetailsList, Loader, StatusBadge } from 'components/common';
import { DetailValues } from 'components/common/DetailsList';
import { abbreviate } from 'utils/numbers';
import { Loader } from 'components/common';
import SidebarCard from '../SidebarCard';
import { ActionsTab, ConnectTab, InfoTab } from './';
interface Props {
node: CLightningNode;
@ -15,99 +15,66 @@ interface Props {
const CLightningDetails: React.FC<Props> = ({ node }) => {
const { l } = usePrefixedTranslation('cmps.designer.clightning.CLightningDetails');
const { getInfo } = useStoreActions(s => s.clightning);
const { nodes } = useStoreState(s => s.clightning);
const [activeTab, setActiveTab] = useState('info');
const { getInfo, getBalance } = useStoreActions(s => s.clightning);
const getInfoAsync = useAsync(
async (node: CLightningNode) => {
if (node.status === Status.Started) {
return await getInfo(node);
await getInfo(node);
await getBalance(node);
}
},
[node],
);
const details: DetailValues = [
{ label: l('nodeType'), value: node.type },
{ label: l('implementation'), value: node.implementation },
{ label: l('version'), value: `v${node.version}` },
{
label: l('status'),
value: (
<StatusBadge
status={node.status}
text={l(`enums.status.${Status[node.status]}`)}
/>
),
},
];
let extra: ReactNode | undefined;
const { nodes } = useStoreState(s => s.clightning);
const nodeState = nodes[node.name];
if (node.status === Status.Started && nodeState) {
if (nodeState.info) {
const {
id,
alias,
warningBitcoindSync,
numPendingChannels,
numActiveChannels,
numInactiveChannels,
} = nodeState.info;
const pubkey = (
<>
{ellipseInner(id)}
<CopyIcon value={id} label="PubKey" />
</>
);
const channels = (
<Tooltip title={l('channelsTooltip')}>
{`${numActiveChannels} / ${numPendingChannels} / ${numInactiveChannels}`}
</Tooltip>
);
details.push(
{ label: l('alias'), value: alias },
{ label: l('pubkey'), value: pubkey },
{ label: l('channels'), value: channels },
);
if (warningBitcoindSync) {
const synced = (
<Tooltip title={warningBitcoindSync}>{warningBitcoindSync}</Tooltip>
);
details.push({ label: l('syncedToChain'), value: synced });
}
if (nodeState.balance) {
const { confBalance } = nodeState.balance;
extra = <strong>{abbreviate(confBalance)} sats</strong>;
}
}
const tabHeaders = [
{ key: 'info', tab: l('info') },
{ key: 'connect', tab: l('connect') },
{ key: 'actions', tab: l('actions') },
];
const tabContents: Record<string, ReactNode> = {
info: <InfoTab node={node} />,
connect: <ConnectTab node={node} />,
actions: <ActionsTab node={node} />,
};
return (
<SidebarCard title={node.name}>
{getInfoAsync.loading && <Loader />}
<SidebarCard
title={node.name}
extra={extra}
tabList={tabHeaders}
activeTabKey={activeTab}
onTabChange={setActiveTab}
>
{node.status === Status.Starting && (
<Alert
type="info"
showIcon
closable={false}
message={l('waitingNotice')}
icon={<Icon type="loading" />}
/>
)}
{node.status === Status.Error && node.errorMsg && (
<Alert
type="error"
message={l('startError')}
description={node.errorMsg}
closable={false}
showIcon
message={l('waitingNotice')}
/>
)}
{getInfoAsync.error && (
{node.status !== Status.Started && !nodeState && getInfoAsync.loading && <Loader />}
{getInfoAsync.error && node.status === Status.Started && (
<Alert
type="error"
closable={false}
message={l('getInfoErr')}
message={l('connectError')}
description={getInfoAsync.error.message}
/>
)}
<DetailsList details={details} />
{tabContents[activeTab]}
</SidebarCard>
);
};

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

@ -0,0 +1,12 @@
import React from 'react';
import { CLightningNode } from 'shared/types';
interface Props {
node: CLightningNode;
}
const ConnectTab: React.FC<Props> = ({ node }) => {
return <div>Connect {node.name}</div>;
};
export default ConnectTab;

96
src/components/designer/clightning/InfoTab.tsx

@ -0,0 +1,96 @@
import React, { ReactNode } from 'react';
import { Alert, Tooltip } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { CLightningNode, Status } from 'shared/types';
import { useStoreState } from 'store';
import { ellipseInner } from 'utils/strings';
import { format } from 'utils/units';
import { StatusBadge } from 'components/common';
import CopyIcon from 'components/common/CopyIcon';
import DetailsList, { DetailValues } from 'components/common/DetailsList';
interface Props {
node: CLightningNode;
}
const InfoTab: React.FC<Props> = ({ node }) => {
const { l } = usePrefixedTranslation('cmps.designer.clightning.InfoTab');
const { nodes } = useStoreState(s => s.clightning);
const details: DetailValues = [
{ label: l('nodeType'), value: node.type },
{ label: l('implementation'), value: node.implementation },
{ label: l('version'), value: `v${node.version}` },
{
label: l('status'),
value: (
<StatusBadge
status={node.status}
text={l(`enums.status.${Status[node.status]}`)}
/>
),
},
];
let warning: ReactNode | undefined;
const nodeState = nodes[node.name];
if (node.status === Status.Started && nodeState) {
if (nodeState.balance) {
const { confBalance, unconfBalance } = nodeState.balance;
details.push({
label: l('confirmedBalance'),
value: `${format(confBalance)} sats`,
});
details.push({
label: l('unconfirmedBalance'),
value: `${format(unconfBalance)} sats`,
});
}
if (nodeState.info) {
const {
id,
alias,
warningBitcoindSync,
numPendingChannels,
numActiveChannels,
numInactiveChannels,
} = nodeState.info;
const pubkey = (
<>
{ellipseInner(id)}
<CopyIcon value={id} label="PubKey" />
</>
);
const channels = (
<Tooltip title={l('channelsTooltip')}>
{`${numActiveChannels} / ${numPendingChannels} / ${numInactiveChannels}`}
</Tooltip>
);
details.push(
{ label: l('alias'), value: alias },
{ label: l('pubkey'), value: pubkey },
{ label: l('channels'), value: channels },
);
if (warningBitcoindSync) {
warning = warningBitcoindSync;
}
}
}
return (
<>
{warning && <Alert type="warning" message={warning} closable={false} showIcon />}
{node.status === Status.Error && node.errorMsg && (
<Alert
type="error"
message={l('startError')}
description={node.errorMsg}
closable={false}
showIcon
/>
)}
<DetailsList details={details} />
</>
);
};
export default InfoTab;

3
src/components/designer/clightning/index.ts

@ -0,0 +1,3 @@
export { default as InfoTab } from './InfoTab';
export { default as ConnectTab } from './ConnectTab';
export { default as ActionsTab } from './ActionsTab';

29
src/i18n/locales/en-US.json

@ -25,18 +25,23 @@
"cmps.designer.bitcoind.MineBlocksInput.label": "Manually Mine Blocks",
"cmps.designer.bitcoind.MineBlocksInput.error": "Unable to mine blocks",
"cmps.designer.bitcoind.MineBlocksInput.btn": "Mine",
"cmps.designer.clightning.CLightningDetails.nodeType": "Node Type",
"cmps.designer.clightning.CLightningDetails.implementation": "Implementation",
"cmps.designer.clightning.CLightningDetails.version": "Version",
"cmps.designer.clightning.CLightningDetails.status": "Status",
"cmps.designer.clightning.CLightningDetails.alias": "Alias",
"cmps.designer.clightning.CLightningDetails.pubkey": "Pubkey",
"cmps.designer.clightning.CLightningDetails.syncedToChain": "Sync Status",
"cmps.designer.clightning.CLightningDetails.channels": "Channels",
"cmps.designer.clightning.CLightningDetails.channelsTooltip": "Active / Pending / Inactive",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Waiting for lightningd to come online",
"cmps.designer.clightning.CLightningDetails.getInfoErr": "Unable to connect to node",
"cmps.designer.clightning.CLightningDetails.startError": "Unable to connect to c-lightning node",
"cmps.designer.clightning.CLightningDetails.info": "Info",
"cmps.designer.clightning.CLightningDetails.connect": "Connect",
"cmps.designer.clightning.CLightningDetails.actions": "Actions",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Waiting for c-lightning to come online",
"cmps.designer.clightning.CLightningDetails.connectError": "Unable to connect to node",
"cmps.designer.clightning.InfoTab.nodeType": "Node Type",
"cmps.designer.clightning.InfoTab.implementation": "Implementation",
"cmps.designer.clightning.InfoTab.version": "Version",
"cmps.designer.clightning.InfoTab.status": "Status",
"cmps.designer.clightning.InfoTab.confirmedBalance": "Confirmed Balance",
"cmps.designer.clightning.InfoTab.unconfirmedBalance": "Unconfirmed Balance",
"cmps.designer.clightning.InfoTab.alias": "Alias",
"cmps.designer.clightning.InfoTab.pubkey": "Pubkey",
"cmps.designer.clightning.InfoTab.syncedToChain": "Sync Status",
"cmps.designer.clightning.InfoTab.channels": "Channels",
"cmps.designer.clightning.InfoTab.channelsTooltip": "Active / Pending / Inactive",
"cmps.designer.clightning.InfoTab.startError": "Unable to connect to the c-lightningdnode",
"cmps.designer.default.DefaultSidebar.title": "Network Designer",
"cmps.designer.default.DefaultSidebar.mainDesc": "Click on an element in the designer to see details",
"cmps.designer.default.DefaultSidebar.addNodesTitle": "Add Nodes",

29
src/i18n/locales/es.json

@ -25,18 +25,23 @@
"cmps.designer.bitcoind.MineBlocksInput.label": "Bloques de mina manual",
"cmps.designer.bitcoind.MineBlocksInput.error": "Incapaz de extraer bloques",
"cmps.designer.bitcoind.MineBlocksInput.btn": "Mina",
"cmps.designer.clightning.CLightningDetails.nodeType": "Tipo de nodo",
"cmps.designer.clightning.CLightningDetails.implementation": "Implementación",
"cmps.designer.clightning.CLightningDetails.version": "Versión",
"cmps.designer.clightning.CLightningDetails.status": "Estado",
"cmps.designer.clightning.CLightningDetails.alias": "Alias",
"cmps.designer.clightning.CLightningDetails.pubkey": "Pubkey",
"cmps.designer.clightning.CLightningDetails.syncedToChain": "Estado de sincronización",
"cmps.designer.clightning.CLightningDetails.channels": "Canales",
"cmps.designer.clightning.CLightningDetails.channelsTooltip": "Activo / Pendiente / Inactivo",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Esperando a que lightningd se conecte",
"cmps.designer.clightning.CLightningDetails.getInfoErr": "No se puede conectar al nodo",
"cmps.designer.clightning.CLightningDetails.startError": "No se puede conectar al nodo c-lightning",
"cmps.designer.clightning.CLightningDetails.info": "Informacion",
"cmps.designer.clightning.CLightningDetails.connect": "Conectar",
"cmps.designer.clightning.CLightningDetails.actions": "Comportamiento",
"cmps.designer.clightning.CLightningDetails.waitingNotice": "Esperando a que LND se conecte",
"cmps.designer.clightning.CLightningDetails.connectError": "No se puede conectar al nodo",
"cmps.designer.clightning.InfoTab.nodeType": "Tipo de nodo",
"cmps.designer.clightning.InfoTab.implementation": "Implementación",
"cmps.designer.clightning.InfoTab.version": "Versión",
"cmps.designer.clightning.InfoTab.status": "Estado",
"cmps.designer.clightning.InfoTab.confirmedBalance": "Saldo confirmado",
"cmps.designer.clightning.InfoTab.unconfirmedBalance": "Saldo no confirmado",
"cmps.designer.clightning.InfoTab.alias": "Alias",
"cmps.designer.clightning.InfoTab.pubkey": "Pubkey",
"cmps.designer.clightning.InfoTab.syncedToChain": "Sincronizado a la cadena",
"cmps.designer.clightning.InfoTab.channels": "Canales",
"cmps.designer.clightning.InfoTab.channelsTooltip": "Activo / Pendiente / Inactivo",
"cmps.designer.clightning.InfoTab.startError": "No se puede conectar al nodo LND",
"cmps.designer.default.DefaultSidebar.title": "Diseñador de red",
"cmps.designer.default.DefaultSidebar.mainDesc": "Haga clic en un elemento en el diseñador para ver detalles",
"cmps.designer.default.DefaultSidebar.addNodesTitle": "Agregar nodos",

4
src/lib/clightning/clightningService.ts

@ -10,6 +10,10 @@ class CLightningService implements CLightningLibrary {
return await this.request(node, 'getinfo');
}
async getBalance(node: CLightningNode): Promise<CLN.GetBalanceResponse> {
return await this.request(node, 'getBalance');
}
/**
* Helper function to continually query the node until a successful
* response is received or it times out

6
src/lib/clightning/clightningTypes.ts

@ -19,3 +19,9 @@ export interface GetInfoResponse {
feesCollectedMsat: string;
warningBitcoindSync: string;
}
export interface GetBalanceResponse {
totalBalance: number;
confBalance: number;
unconfBalance: number;
}

14
src/store/models/clightning.ts

@ -10,6 +10,7 @@ export interface CLightningNodeMapping {
export interface CLightningNodeModel {
info?: CLN.GetInfoResponse;
balance?: CLN.GetBalanceResponse;
}
export interface CLightningModel {
@ -17,6 +18,11 @@ export interface CLightningModel {
removeNode: Action<CLightningModel, string>;
setInfo: Action<CLightningModel, { node: CLightningNode; info: CLN.GetInfoResponse }>;
getInfo: Thunk<CLightningModel, CLightningNode, StoreInjections, RootModel>;
setBalance: Action<
CLightningModel,
{ node: CLightningNode; balance: CLN.GetBalanceResponse }
>;
getBalance: Thunk<CLightningModel, CLightningNode, StoreInjections, RootModel>;
}
const lndModel: CLightningModel = {
@ -36,6 +42,14 @@ const lndModel: CLightningModel = {
const info = await injections.clightningService.getInfo(node);
actions.setInfo({ node, info });
}),
setBalance: action((state, { node, balance }) => {
if (!state.nodes[node.name]) state.nodes[node.name] = {};
state.nodes[node.name].balance = balance;
}),
getBalance: thunk(async (actions, node, { injections }) => {
const balance = await injections.clightningService.getBalance(node);
actions.setBalance({ node, balance });
}),
};
export default lndModel;

1
src/types/index.ts

@ -69,6 +69,7 @@ export interface LndLibrary {
export interface CLightningLibrary {
waitUntilOnline: (node: CLightningNode) => Promise<void>;
getInfo: (node: CLightningNode) => Promise<CLN.GetInfoResponse>;
getBalance: (node: CLightningNode) => Promise<CLN.GetBalanceResponse>;
}
export interface StoreInjections {

1
src/utils/tests.tsx

@ -60,6 +60,7 @@ export const injections: StoreInjections = {
clightningService: {
waitUntilOnline: jest.fn(),
getInfo: jest.fn(),
getBalance: jest.fn(),
},
};

Loading…
Cancel
Save