Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: node stats page #766

Merged
merged 16 commits into from
Aug 31, 2018
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"cytoscape": "^3.2.16",
"cytoscape-dagre": "^2.2.1",
"d3": "^5.5.0",
"details-polyfill": "^1.1.0",
"enzyme": "^3.4.4",
"enzyme-adapter-react-16": "^1.2.0",
"file-extension": "^4.0.5",
Expand Down
2 changes: 2 additions & 0 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import filesBundle from './files'
import configBundle from './config'
import configSaveBundle from './config-save'
import navbarBundle from './navbar'
import statsBundle from './stats'

export default composeBundles(
appIdle({ idleTimeout: 5000 }),
ipfsBundle(),
statsBundle,
exploreBundle,
nodeBandwidthBundle,
nodeBandwidthChartBundle(),
Expand Down
33 changes: 33 additions & 0 deletions src/bundles/stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'

const bundle = createAsyncResourceBundle({
name: 'stats',
getPromise: async ({getIpfs}) => {
const [bitswap, repo, bw] = await Promise.all([
getIpfs().stats.bitswap(),
getIpfs().stats.repo(),
getIpfs().stats.bw()
])
return { bitswap, repo, bw }
},
staleAfter: 2000,
persist: false,
checkIfOnline: false
})

bundle.selectWantlistLength = (state) => {
return (state.stats.bitswap && state.stats.bitswap.wantlist && state.stats.bitswap.wantlist.length) || 0
}

// Fetch the config if we don't have it or it's more than `staleAfter` ms old
bundle.reactStatsFetch = createSelector(
'selectStatsShouldUpdate',
'selectIpfsReady',
(shouldUpdate, ipfsReady) => {
if (shouldUpdate && ipfsReady) {
return { actionCreator: 'doFetchStats' }
}
}
)

export default bundle
2 changes: 2 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@import "../node_modules/tachyons";
/* They forgot to include word break: https://github.com/tachyons-css/tachyons/issues/563 */
@import "../node_modules/tachyons/src/_word-break.css";
@import "../node_modules/ipfs-css";

body {
Expand Down
5 changes: 5 additions & 0 deletions src/status/Commons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react'

export const Title = ({ children, ...props }) => (
<h2 className='dib tracked ttu f6 fw2 teal-muted hover-aqua link mt0 mb4' {...props}>{ children }</h2>
)
10 changes: 5 additions & 5 deletions src/status/CountryChart.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component } from 'react'
import { Title } from './Commons'
import { Pie } from 'react-chartjs-2'
import { connect } from 'redux-bundler-react'
import PropTypes from 'prop-types'
Expand Down Expand Up @@ -49,7 +50,8 @@ export class CountryChart extends Component {
const options = {
responsive: true,
legend: {
display: false
display: true,
position: 'bottom'
}
}

Expand All @@ -65,10 +67,8 @@ export class CountryChart extends Component {
}

return (
<Box>
<h2 className='dib tracked ttu f6 fw2 teal-muted hover-aqua link mt0 mb4'>
Distribution of peers
</h2>
<Box className={this.props.className}>
<Title>Distribution of peers</Title>
<Pie data={{ datasets, labels }} options={options} />
</Box>
)
Expand Down
5 changes: 3 additions & 2 deletions src/status/NodeBandwidthChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Line } from 'react-chartjs-2'
import { connect } from 'redux-bundler-react'
import PropTypes from 'prop-types'
import filesize from 'filesize'
import { Title } from './Commons'
import Box from '../components/box/Box'

const humansize = filesize.partial({ round: 0 })
Expand Down Expand Up @@ -66,8 +67,8 @@ export class NodeBandwidthChart extends Component {
}

return (
<Box>
<h2 className='dib tracked ttu f6 fw2 teal-muted hover-aqua link mt0 mb4'>Bandwidth over time</h2>
<Box className={`pa4 pr2 ${this.props.className}`}>
<Title>Bandwidth over time</Title>
<Line data={{ datasets }} options={options} />
</Box>
)
Expand Down
141 changes: 141 additions & 0 deletions src/status/NodeInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React from 'react'
import { connect } from 'redux-bundler-react'
import Speedometer from './Speedometer'
import Box from '../components/box/Box'
import { Title } from './Commons'
import 'details-polyfill'

const Block = ({ children }) => (
<div className='dt dt--fixed pt2'>
{ children }
</div>
)

const Label = ({ children }) => (
<label className='dtc silver tracked ttu f7' style={{width: '100px'}}>{children}</label>
)

const Value = ({ children, advanced = false }) => (
<div className={`dtc charcoal monospace ${advanced ? 'word-wrap f7 lh-copy pa2 bg-light-gray' : 'truncate'}`}>{children}</div>
)

const Graph = (props) => (
<div className='mr2 ml2 mt3 mt0-l'>
<Speedometer {...props} />
</div>
)

class NodeInfo extends React.Component {
state = {
downSpeed: {
filled: 0,
total: 125000 // Starts with 1 Mb/s max
},
upSpeed: {
filled: 0,
total: 125000 // Starts with 1 Mb/s max
}
}

componentDidUpdate (_, prevState) {
const { stats } = this.props

const down = stats ? parseInt(stats.bw.rateIn.toFixed(0), 10) : 0
const up = stats ? parseInt(stats.bw.rateOut.toFixed(0), 10) : 0

if (down !== prevState.downSpeed.filled || up !== prevState.upSpeed.filled) {
this.setState({
downSpeed: {
filled: down,
total: Math.max(down, prevState.downSpeed.total)
},
upSpeed: {
filled: up,
total: Math.max(up, prevState.upSpeed.total)
}
})
}
}

getField (obj, field, fn) {
if (obj && obj[field]) {
if (fn) {
return fn(obj[field])
}

return obj[field]
}

return 'Cannot access the API.'
}

render () {
const { ipfsIdentity, stats, peers } = this.props
const { downSpeed, upSpeed } = this.state

let addresses = null

if (ipfsIdentity) {
addresses = [...new Set(ipfsIdentity.addresses)].map(addr => <div key={addr}>{addr}</div>)
}

return (
<Box className='f6 pa4'>
<div className='flex flex-column flex-row-l flex-wrap-l justify-between-l'>
<div className='w-100 w-50-l pr2-l' style={{ maxWidth: '34em' }} >
<Title style={{ marginBottom: '0.5rem' }}>Node Info</Title>
<Block>
<Label>CID</Label>
<Value>{this.getField(ipfsIdentity, 'id')}</Value>
</Block>
<Block>
<Label>Peers</Label>
<Value>{peers ? peers.length : 0}</Value>
</Block>
<Block>
<Label>Version</Label>
<Value>{this.getField(ipfsIdentity, 'agentVersion')}</Value>
</Block>
</div>
<div className='w-100 pl2-l flex-wrap flex-no-wrap-l flex justify-between' style={{ maxWidth: '35rem' }}>
<Graph
title='Up Speed'
color='#69c4cd'
{...upSpeed} />
<Graph
title='Down Speed'
color='#f39021'
{...downSpeed} />
<Graph
title='Space Used'
color='#0b3a53'
noSpeed
filled={stats ? parseInt(stats.repo.repoSize.toFixed(0), 10) : 0}
total={stats ? parseInt(stats.repo.storageMax.toFixed(0), 10) : 0} />
</div>
</div>

<details className='mt3'>
<summary className='pointer blue outline-0'>Advanced</summary>
<div className='mt3'>
<Block>
<Label>Public Key</Label>
<Value advanced>{this.getField(ipfsIdentity, 'publicKey')}</Value>
</Block>
<Block>
<Label>Addresses</Label>
<Value advanced>{addresses}</Value>
</Block>
</div>
</details>
</Box>
)
}
}

export default connect(
'selectIpfsIdentity',
'selectPeers',
'selectStats',
NodeInfo
)
3 changes: 2 additions & 1 deletion src/status/PeerBandwidthTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import filesize from 'filesize'
import CountryFlag from 'react-country-flag'
import Box from '../components/box/Box'
import { Title } from './Commons'
import ComponentLoader from '../loader/ComponentLoader.js'

const humansize = filesize.partial({round: 0})
Expand Down Expand Up @@ -53,7 +54,7 @@ export class PeerBandwidthTable extends Component {
<ComponentLoader pastDelay />
) : (
<Box className={className}>
<h2 className='dib tracked ttu f6 fw2 teal-muted hover-aqua link mt0 mb4'>Bandwidth by peer</h2>
<Title>Bandwidth by peer</Title>
<table className='collapse'>
<tbody>
<tr className='tl'>
Expand Down
50 changes: 50 additions & 0 deletions src/status/Speedometer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react'
import {Doughnut} from 'react-chartjs-2'
import filesize from 'filesize'

const rotation = (n) => (0.5 + (1 - n)) * Math.PI
const circumference = (n) => n * 2 * Math.PI

export default function ({ total = 100, title, filled = 0, noSpeed = false, color = '#FF6384' }) {
const doughnut = {
options: {
legend: {
display: false
},
tooltips: {
enabled: false
},
maintainAspectRatio: false,
cutoutPercentage: 75,
rotation: rotation(0.7),
circumference: circumference(0.7)
},
data: {
labels: ['Speed', 'Nothing'],
datasets: [{
data: [filled, filled > total ? 0 : total - filled],
backgroundColor: [color, '#f5f5f5'],
hoverBackgroundColor: [color, '#f5f5f5']
}]
}
}

const data = filesize(filled, {
output: 'array',
round: 0,
bits: !noSpeed
})

return (
<div className='relative tc center overflow-hidden' style={{ width: '11em', height: '9em' }} >
<div style={{ width: '11em', height: '11em', marginTop: '-1em' }}>
<Doughnut {...doughnut} />
</div>

<div className='absolute' style={{ top: '60%', left: '50%', transform: 'translate(-50%, -50%)' }} >
<span className='f3'>{data[0]}</span><span className='ml1 f7'>{data[1]}{ noSpeed ? '' : '/s' }</span>
<span className='db f7'>{title}</span>
</div>
</div>
)
}
15 changes: 11 additions & 4 deletions src/status/StatusPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import CountryChart from './CountryChart'
import NodeInfo from './NodeInfo'
import NodeBandwidthChart from './NodeBandwidthChart'
import PeerBandwidthTable from './PeerBandwidthTable'

Expand All @@ -9,9 +10,15 @@ export default () => (
<Helmet>
<title>Status - IPFS</title>
</Helmet>
<h1>Status</h1>
<CountryChart />
<NodeBandwidthChart />
<PeerBandwidthTable className='pa4 mt3' />
<NodeInfo />
<div className='flex flex-column-s flex-column-m flex-row'>
<div className='w-100-s w-100-m w-50 mt3 pr0-s pr0-m pr2'>
<NodeBandwidthChart />
</div>
<div className='w-100-s w-100-m w-50 mt3 pl0-s pl0-m pl2'>
<CountryChart />
</div>
</div>
<PeerBandwidthTable className='mt3 pa4 overflow-x-auto' />
</div>
)
4 changes: 3 additions & 1 deletion test/e2e/navigation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ function addMockIpfs (page) {
stat: () => Promise.resolve({})
},
stats: {
bw: () => Promise.resolve(fakeBandwidth())
bw: () => Promise.resolve(fakeBandwidth()),
repo: () => Promise.resolve({}),
bitswap: () => Promise.resolve({})
},
swarm: {
peers: () => Promise.resolve([])
Expand Down