mirror of https://github.com/lukechilds/polar.git
jamaljsr
5 years ago
29 changed files with 371 additions and 72 deletions
@ -0,0 +1,115 @@ |
|||
import React from 'react'; |
|||
import { useAsync } from 'react-async-hook'; |
|||
import { Alert, Icon, Tooltip } 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 SidebarCard from '../SidebarCard'; |
|||
|
|||
interface Props { |
|||
node: CLightningNode; |
|||
} |
|||
|
|||
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 getInfoAsync = useAsync( |
|||
async (node: CLightningNode) => { |
|||
if (node.status === Status.Started) { |
|||
return await getInfo(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]}`)} |
|||
/> |
|||
), |
|||
}, |
|||
]; |
|||
|
|||
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 }); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return ( |
|||
<SidebarCard title={node.name}> |
|||
{getInfoAsync.loading && <Loader />} |
|||
{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 |
|||
/> |
|||
)} |
|||
{getInfoAsync.error && ( |
|||
<Alert |
|||
type="error" |
|||
closable={false} |
|||
message={l('getInfoErr')} |
|||
description={getInfoAsync.error.message} |
|||
/> |
|||
)} |
|||
<DetailsList details={details} /> |
|||
</SidebarCard> |
|||
); |
|||
}; |
|||
|
|||
export default CLightningDetails; |
@ -0,0 +1,45 @@ |
|||
import { CLightningNode } from 'shared/types'; |
|||
import { CLightningLibrary } from 'types'; |
|||
import { waitFor } from 'utils/async'; |
|||
import { read } from 'utils/files'; |
|||
import { snakeKeysToCamel } from 'utils/objects'; |
|||
import * as CLN from './clightningTypes'; |
|||
|
|||
class CLightningService implements CLightningLibrary { |
|||
async getInfo(node: CLightningNode): Promise<CLN.GetInfoResponse> { |
|||
return await this.request(node, 'getinfo'); |
|||
} |
|||
|
|||
/** |
|||
* Helper function to continually query the node until a successful |
|||
* response is received or it times out |
|||
*/ |
|||
async waitUntilOnline( |
|||
node: CLightningNode, |
|||
interval = 3 * 1000, // check every 3 seconds
|
|||
timeout = 30 * 1000, // timeout after 30 seconds
|
|||
): Promise<void> { |
|||
return waitFor( |
|||
async () => { |
|||
await this.getInfo(node); |
|||
}, |
|||
interval, |
|||
timeout, |
|||
); |
|||
} |
|||
|
|||
private async request(node: CLightningNode, path: string) { |
|||
const { paths, ports } = node; |
|||
const url = `http://127.0.0.1:${ports.rest}/v1/${path}`; |
|||
const macaroon = await read(paths.macaroon, 'base64'); |
|||
const response = await fetch(url, { |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
macaroon, |
|||
}, |
|||
}); |
|||
return snakeKeysToCamel(await response.json()); |
|||
} |
|||
} |
|||
|
|||
export default new CLightningService(); |
@ -0,0 +1,21 @@ |
|||
export interface GetInfoResponse { |
|||
id: string; |
|||
alias: string; |
|||
color: string; |
|||
numPeers: number; |
|||
numPendingChannels: number; |
|||
numActiveChannels: number; |
|||
numInactiveChannels: number; |
|||
address: string[]; |
|||
binding: { |
|||
type: string; |
|||
address: string; |
|||
port: number; |
|||
}[]; |
|||
version: string; |
|||
blockheight: number; |
|||
network: string; |
|||
msatoshiFeesCollected: number; |
|||
feesCollectedMsat: string; |
|||
warningBitcoindSync: string; |
|||
} |
@ -0,0 +1 @@ |
|||
export { default as clightningService } from './clightningService'; |
@ -1 +0,0 @@ |
|||
export { default as lightningdService } from './lightningdService'; |
@ -1,23 +0,0 @@ |
|||
import { LightningdNode } from 'shared/types'; |
|||
import { read } from 'utils/files'; |
|||
|
|||
class LightningdService { |
|||
async getInfo(node: LightningdNode) { |
|||
return await this.request(node, 'getinfo'); |
|||
} |
|||
|
|||
private async request(node: LightningdNode, path: string) { |
|||
const { paths, ports } = node; |
|||
const url = `http://127.0.0.1:${ports.rest}/v1/${path}`; |
|||
const macaroon = await read(paths.macaroon, 'base64'); |
|||
const response = await fetch(url, { |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
macaroon, |
|||
}, |
|||
}); |
|||
return await response.json(); |
|||
} |
|||
} |
|||
|
|||
export default new LightningdService(); |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,41 @@ |
|||
import { Action, action, Thunk, thunk } from 'easy-peasy'; |
|||
import { CLightningNode } from 'shared/types'; |
|||
import * as CLN from 'lib/clightning/clightningTypes'; |
|||
import { StoreInjections } from 'types'; |
|||
import { RootModel } from './'; |
|||
|
|||
export interface CLightningNodeMapping { |
|||
[key: string]: CLightningNodeModel; |
|||
} |
|||
|
|||
export interface CLightningNodeModel { |
|||
info?: CLN.GetInfoResponse; |
|||
} |
|||
|
|||
export interface CLightningModel { |
|||
nodes: CLightningNodeMapping; |
|||
removeNode: Action<CLightningModel, string>; |
|||
setInfo: Action<CLightningModel, { node: CLightningNode; info: CLN.GetInfoResponse }>; |
|||
getInfo: Thunk<CLightningModel, CLightningNode, StoreInjections, RootModel>; |
|||
} |
|||
|
|||
const lndModel: CLightningModel = { |
|||
// state properties
|
|||
nodes: {}, |
|||
// reducer actions (mutations allowed thx to immer)
|
|||
removeNode: action((state, name) => { |
|||
if (state.nodes[name]) { |
|||
delete state.nodes[name]; |
|||
} |
|||
}), |
|||
setInfo: action((state, { node, info }) => { |
|||
if (!state.nodes[node.name]) state.nodes[node.name] = {}; |
|||
state.nodes[node.name].info = info; |
|||
}), |
|||
getInfo: thunk(async (actions, node, { injections }) => { |
|||
const info = await injections.clightningService.getInfo(node); |
|||
actions.setInfo({ node, info }); |
|||
}), |
|||
}; |
|||
|
|||
export default lndModel; |
@ -0,0 +1,39 @@ |
|||
const isArray = (arg: any) => Array.isArray(arg); |
|||
|
|||
const isObject = (arg: any) => |
|||
arg === Object(arg) && !isArray(arg) && typeof arg !== 'function'; |
|||
|
|||
/** |
|||
* Converts a string from snake case to camel case |
|||
* ex: 'my_fav_text' -> 'myFavText' |
|||
* @param key the string to convert from snake_case to camelCase |
|||
*/ |
|||
const toCamel = (key: string) => { |
|||
// use regex to replace underscore + lowercase char with uppercase char
|
|||
// ex: _a -> A
|
|||
return key.replace(/([_][a-z])/gi, underChar => { |
|||
return underChar.substring(1).toUpperCase(); |
|||
}); |
|||
}; |
|||
|
|||
/** |
|||
* Recursively converts all the keys in the provided object to be camelCase |
|||
* @param arg the object to convert |
|||
*/ |
|||
export const snakeKeysToCamel = (arg: any): any => { |
|||
if (isObject(arg)) { |
|||
const newObj: Record<string, any> = {}; |
|||
// convert each key to camel case
|
|||
Object.keys(arg).forEach(k => { |
|||
newObj[toCamel(k)] = snakeKeysToCamel(arg[k]); |
|||
}); |
|||
return newObj; |
|||
} else if (isArray(arg)) { |
|||
const arr = arg as Array<any>; |
|||
return arr.map(i => { |
|||
return snakeKeysToCamel(i); |
|||
}); |
|||
} |
|||
|
|||
return arg; |
|||
}; |
Loading…
Reference in new issue