16 changed files with 507 additions and 77 deletions
@ -1,15 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import styles from './CardChannel.scss' |
|||
|
|||
const CardChannel = ({ channel }) => ( |
|||
<li className={styles.channel}> |
|||
{channel.chan_id} |
|||
</li> |
|||
) |
|||
|
|||
CardChannel.propTypes = { |
|||
channel: PropTypes.object.isRequired |
|||
} |
|||
|
|||
export default CardChannel |
@ -1,6 +0,0 @@ |
|||
.channel { |
|||
width: 40%; |
|||
margin: 0 5% 20px 5%; |
|||
height: 500px; |
|||
background: red; |
|||
} |
@ -0,0 +1,133 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { ForceGraph, ForceGraphNode, ForceGraphLink } from 'react-vis-force' |
|||
import { FaCircle } from 'react-icons/lib/fa' |
|||
import styles from './NetworkChannels.scss' |
|||
|
|||
class NetworkChannels extends Component { |
|||
constructor(props) { |
|||
super(props) |
|||
this.state = { |
|||
ready: false |
|||
} |
|||
} |
|||
|
|||
componentWillMount() { |
|||
setTimeout(() => { |
|||
this.setState({ ready: true }) |
|||
}, 1000) |
|||
|
|||
this.props.setCurrentChannel(this.props.channels[0]) |
|||
} |
|||
|
|||
render() { |
|||
const { ready } = this.state |
|||
const { |
|||
channels, |
|||
network: { nodes, edges, selectedChannel, networkLoading }, |
|||
identity_pubkey, |
|||
setCurrentChannel |
|||
} = this.props |
|||
|
|||
if (!ready || networkLoading) { |
|||
return ( |
|||
<div className={styles.networkLoading}> |
|||
<h1>loading network graph</h1> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.networkchannels}> |
|||
<div className={styles.network}> |
|||
<ForceGraph |
|||
simulationOptions={ |
|||
{ |
|||
width: 1000, |
|||
height: 1000, |
|||
strength: { |
|||
charge: -750 |
|||
} |
|||
} |
|||
} |
|||
labelAttr='label' |
|||
opacityFactor={1} |
|||
zoomOptions={{ minScale: 0.1, maxScale: 5 }} |
|||
zoom |
|||
animate |
|||
highlightDependencies |
|||
> |
|||
{ |
|||
nodes.map(node => ( |
|||
<ForceGraphNode |
|||
r={15} |
|||
label={node.pub_key} |
|||
key={node.pub_key} |
|||
node={{ id: node.pub_key }} |
|||
className={`${styles.node} ${identity_pubkey === node.pub_key && styles.active}`} |
|||
fill={identity_pubkey === node.pub_key ? 'green' : 'silver'} |
|||
/> |
|||
)) |
|||
} |
|||
{ |
|||
edges.map(edge => ( |
|||
<ForceGraphLink |
|||
className={`${styles.line} ${selectedChannel.chan_id === edge.channel_id && styles.active}`} |
|||
key={edge.channel_id} |
|||
link={{ source: edge.node1_pub, target: edge.node2_pub }} |
|||
/> |
|||
)) |
|||
} |
|||
</ForceGraph> |
|||
</div> |
|||
<div className={styles.channels}> |
|||
<ul> |
|||
{ |
|||
channels.map((channel, index) => ( |
|||
<li |
|||
key={index} |
|||
className={`${styles.channel} ${channel.chan_id === selectedChannel.chan_id && styles.active}`} |
|||
onClick={() => setCurrentChannel(channel)} |
|||
> |
|||
<header> |
|||
{ |
|||
channel.active ? |
|||
<span className={styles.active}> |
|||
<FaCircle /> |
|||
<i>active</i> |
|||
</span> |
|||
: |
|||
<span className={styles.notactive}> |
|||
<FaCircle /> |
|||
<i>not active</i> |
|||
</span> |
|||
} |
|||
</header> |
|||
<div className={styles.content}> |
|||
<div> |
|||
<h4>Remote Pubkey</h4> |
|||
<h2>{`${channel.remote_pubkey.substring(30, channel.remote_pubkey.length)}...`}</h2> |
|||
</div> |
|||
<div> |
|||
<h4>Channel Point</h4> |
|||
<h2>{`${channel.channel_point.substring(30, channel.channel_point.length)}...`}</h2> |
|||
</div> |
|||
</div> |
|||
</li> |
|||
)) |
|||
} |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
NetworkChannels.propTypes = { |
|||
channels: PropTypes.array.isRequired, |
|||
network: PropTypes.object.isRequired, |
|||
identity_pubkey: PropTypes.string.isRequired, |
|||
setCurrentChannel: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default NetworkChannels |
@ -0,0 +1,114 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
@keyframes dash { |
|||
to { |
|||
stroke-dashoffset: 1000; |
|||
} |
|||
} |
|||
|
|||
.networkLoading { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100vh; |
|||
background: $black; |
|||
|
|||
h1 { |
|||
font-size: 22px; |
|||
color: $green; |
|||
font-family: 'courier'; |
|||
text-align: center; |
|||
margin-top: 25%; |
|||
} |
|||
} |
|||
|
|||
.networkchannels { |
|||
position: relative; |
|||
} |
|||
|
|||
.network, .channels { |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
} |
|||
|
|||
.network { |
|||
width: 70%; |
|||
} |
|||
|
|||
.channels { |
|||
width: calc(30% - 2px); |
|||
border: 1px solid $darkestgrey; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.node { |
|||
r: 15; |
|||
fill: $darkestgrey; |
|||
|
|||
.active { |
|||
r: 25; |
|||
fill: $green; |
|||
stroke: $green; |
|||
} |
|||
} |
|||
|
|||
.line.active { |
|||
stroke-width: 10; |
|||
opacity: 1; |
|||
stroke: $green; |
|||
stroke-dasharray: 100; |
|||
animation: dash 2.5s infinite linear; |
|||
} |
|||
|
|||
.channel { |
|||
padding: 10px; |
|||
background: rgba(0, 0, 0, 0.7); |
|||
cursor: pointer; |
|||
transition: all 0.25s; |
|||
border-bottom: 1px solid $white; |
|||
|
|||
&.active, &:hover { |
|||
background: rgba(255, 255, 255, 0.4); |
|||
} |
|||
|
|||
header { |
|||
margin-bottom: 10px; |
|||
|
|||
.active { |
|||
color: $green; |
|||
} |
|||
|
|||
.notactive { |
|||
color: $green; |
|||
} |
|||
|
|||
span svg { |
|||
font-size: 10px; |
|||
} |
|||
|
|||
i { |
|||
text-transform: uppercase; |
|||
font-size: 10px; |
|||
margin-left: 5px; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
div { |
|||
margin-bottom: 5px; |
|||
} |
|||
|
|||
h4 { |
|||
text-transform: uppercase; |
|||
margin-bottom: 5px; |
|||
font-size: 10px; |
|||
color: $main; |
|||
} |
|||
|
|||
h2 { |
|||
font-size: 14px; |
|||
color: $white; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,129 @@ |
|||
import { createSelector } from 'reselect' |
|||
import { ipcRenderer } from 'electron' |
|||
|
|||
// ------------------------------------
|
|||
// Constants
|
|||
// ------------------------------------
|
|||
export const GET_DESCRIBE_NETWORK = 'GET_DESCRIBE_NETWORK' |
|||
export const RECEIVE_DESCRIBE_NETWORK = 'RECEIVE_DESCRIBE_NETWORK' |
|||
|
|||
export const GET_QUERY_ROUTES = 'GET_QUERY_ROUTES' |
|||
export const RECEIVE_QUERY_ROUTES = 'RECEIVE_QUERY_ROUTES' |
|||
|
|||
export const SET_CURRENT_ROUTE = 'SET_CURRENT_ROUTE' |
|||
|
|||
export const SET_CURRENT_CHANNEL = 'SET_CURRENT_CHANNEL' |
|||
|
|||
// ------------------------------------
|
|||
// Actions
|
|||
// ------------------------------------
|
|||
export function getDescribeNetwork() { |
|||
return { |
|||
type: GET_DESCRIBE_NETWORK |
|||
} |
|||
} |
|||
|
|||
export function getQueryRoutes(pubkey) { |
|||
return { |
|||
type: GET_QUERY_ROUTES, |
|||
pubkey |
|||
} |
|||
} |
|||
|
|||
export function setCurrentRoute(route) { |
|||
return { |
|||
type: SET_CURRENT_ROUTE, |
|||
route |
|||
} |
|||
} |
|||
|
|||
export function setCurrentChannel(selectedChannel) { |
|||
return { |
|||
type: SET_CURRENT_CHANNEL, |
|||
selectedChannel |
|||
} |
|||
} |
|||
|
|||
// Send IPC event for describeNetwork
|
|||
export const fetchDescribeNetwork = () => (dispatch) => { |
|||
dispatch(getDescribeNetwork()) |
|||
ipcRenderer.send('lnd', { msg: 'describeNetwork' }) |
|||
} |
|||
|
|||
// Receive IPC event for describeNetwork
|
|||
export const receiveDescribeNetwork = (event, { nodes, edges }) => dispatch => dispatch({ type: RECEIVE_DESCRIBE_NETWORK, nodes, edges }) |
|||
|
|||
export const queryRoutes = (pubkey, amount) => (dispatch) => { |
|||
dispatch(getQueryRoutes(pubkey)) |
|||
ipcRenderer.send('lnd', { msg: 'queryRoutes', data: { pubkey, amount } }) |
|||
} |
|||
|
|||
export const receiveQueryRoutes = (event, { routes }) => dispatch => dispatch({ type: RECEIVE_QUERY_ROUTES, routes }) |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[GET_DESCRIBE_NETWORK]: state => ({ ...state, networkLoading: true }), |
|||
[RECEIVE_DESCRIBE_NETWORK]: (state, { nodes, edges }) => ({ ...state, networkLoading: false, nodes, edges }), |
|||
|
|||
[GET_QUERY_ROUTES]: (state, { pubkey }) => ({ ...state, networkLoading: true, selectedNode: { pubkey, routes: [], currentRoute: {} } }), |
|||
[RECEIVE_QUERY_ROUTES]: (state, { routes }) => ( |
|||
{ |
|||
...state, |
|||
networkLoading: false, |
|||
selectedNode: { pubkey: state.selectedNode.pubkey, routes, currentRoute: routes[0] } |
|||
} |
|||
), |
|||
|
|||
[SET_CURRENT_ROUTE]: (state, { route }) => ( |
|||
{ |
|||
...state, |
|||
selectedNode: { pubkey: state.selectedNode.pubkey, routes: state.selectedNode.routes, currentRoute: route } |
|||
} |
|||
), |
|||
|
|||
[SET_CURRENT_CHANNEL]: (state, { selectedChannel }) => ({ ...state, selectedChannel }) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Selectors
|
|||
// ------------------------------------
|
|||
const networkSelectors = {} |
|||
const currentRouteSelector = state => state.network.selectedNode.currentRoute |
|||
|
|||
networkSelectors.currentRouteHopChanIds = createSelector( |
|||
currentRouteSelector, |
|||
(currentRoute) => { |
|||
if (!currentRoute.hops) { return [] } |
|||
|
|||
return currentRoute.hops.map(hop => hop.chan_id) |
|||
} |
|||
) |
|||
|
|||
export { networkSelectors } |
|||
|
|||
// ------------------------------------
|
|||
// Initial State
|
|||
// ------------------------------------
|
|||
const initialState = { |
|||
networkLoading: false, |
|||
nodes: [], |
|||
edges: [], |
|||
selectedNode: { |
|||
pubkey: '', |
|||
routes: [], |
|||
currentRoute: {} |
|||
}, |
|||
selectedChannel: {} |
|||
} |
|||
|
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
export default function activityReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
Loading…
Reference in new issue