Skip to content

Commit

Permalink
feat: add tooltips and health indicator to peers (#169)
Browse files Browse the repository at this point in the history
* feat: add value thresholds and explanations to topology stats

* feat: extract title and row, refactor threshold, add tooltip, add overall health

* refactor: clean up code

* refactor: reword Node to Bee node
  • Loading branch information
Cafe137 committed Aug 16, 2021
1 parent a62243f commit 480f6dc
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 18 deletions.
19 changes: 6 additions & 13 deletions src/components/StatCard.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,23 @@
import type { ReactElement } from 'react'

import { makeStyles } from '@material-ui/core/styles'
import { Card, CardContent, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Skeleton } from '@material-ui/lab'
import type { ReactElement } from 'react'
import { Title } from './Title'

const useStyles = makeStyles({
root: {
minWidth: 275,
},
title: {
fontSize: 16,
},
pos: {
marginBottom: 12,
},
})

interface Props {
label: string
statistic?: string
loading?: boolean
tooltip?: string
}

export default function StatCard({ loading, label, statistic }: Props): ReactElement {
export default function StatCard({ loading, label, statistic, tooltip }: Props): ReactElement {
const classes = useStyles()

return (
Expand All @@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
)}
{!loading && (
<>
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Title label={label} tooltip={tooltip} />
<Typography variant="h5" component="h2">
{statistic}
</Typography>
Expand Down
41 changes: 41 additions & 0 deletions src/components/Title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Grid, Tooltip, Typography } from '@material-ui/core/'
import { makeStyles } from '@material-ui/core/styles'
import { Info } from '@material-ui/icons'
import type { ReactElement } from 'react'

interface TitleProps {
label: string
tooltip?: string
}

const useStyles = makeStyles({
title: {
fontSize: 16,
},
})

export function Title({ label, tooltip }: TitleProps): ReactElement {
const classes = useStyles()

if (!tooltip) {
return (
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
)
}

// span is needed as Tooltip expects a non-functional element!
return (
<Tooltip title={tooltip}>
<span>
<Grid container direction="row" justify="space-between">
<Typography className={classes.title} color="textSecondary" gutterBottom>
{label}
</Typography>
<Info />
</Grid>
</span>
</Tooltip>
)
}
57 changes: 52 additions & 5 deletions src/components/TopologyStats.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,72 @@
import type { Topology } from '@ethersphere/bee-js'
import { Grid } from '@material-ui/core/'
import type { ReactElement } from 'react'
import { pickThreshold, ThresholdValues } from '../utils/threshold'
import StatCard from './StatCard'

interface Props {
interface RootProps {
isLoading: boolean
topology: Topology | null
error: Error | null // FIXME: should display error
}

const TopologyStats = ({ isLoading, topology }: Props): ReactElement => (
interface Props extends RootProps {
thresholds: ThresholdValues
}

const TopologyStats = (props: RootProps): ReactElement => {
const thresholds: ThresholdValues = {
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
population: pickThreshold('population', props.topology?.population || 0),
depth: pickThreshold('depth', props.topology?.depth || 0),
}

return (
<>
<Indicator {...props} thresholds={thresholds} />
<Metrics {...props} thresholds={thresholds} />
</>
)
}

const Indicator = ({ isLoading, thresholds }: Props): ReactElement => {
const maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'

return (
<div style={{ marginBottom: '20px' }}>
<StatCard label="Overall Health Indicator" statistic={percentageText} loading={isLoading} />
</div>
)
}

const Metrics = ({ isLoading, topology, thresholds }: Props): ReactElement => (
<Grid style={{ marginBottom: '20px', flexGrow: 1 }}>
<Grid container spacing={3}>
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Connected Peers" statistic={topology?.connected.toString()} loading={isLoading} />
<StatCard
label="Connected Peers"
statistic={topology?.connected.toString()}
loading={isLoading}
tooltip={thresholds.connectedPeers.explanation}
/>
</Grid>
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Population" statistic={topology?.population.toString()} loading={isLoading} />
<StatCard
label="Population"
statistic={topology?.population.toString()}
loading={isLoading}
tooltip={thresholds.population.explanation}
/>
</Grid>
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
<StatCard label="Depth" statistic={topology?.depth.toString()} loading={isLoading} />
<StatCard
label="Depth"
statistic={topology?.depth.toString()}
loading={isLoading}
tooltip={thresholds.depth.explanation}
/>
</Grid>
</Grid>
</Grid>
Expand Down
105 changes: 105 additions & 0 deletions src/utils/threshold.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const OPTIMAL_CONNECTED_PEERS = 200
const OPTIMAL_POPULATION = 100_000
const OPTIMAL_DEPTH = 12

interface Threshold {
minimumValue: number
explanation: string
score: number
}

type Thresholds = {
connectedPeers: Threshold[]
population: Threshold[]
depth: Threshold[]
}

type ThresholdValue = {
score: number
maximumScore: number
explanation: string
}

export type ThresholdValues = {
connectedPeers: ThresholdValue
population: ThresholdValue
depth: ThresholdValue
}

const GENERIC_ERROR = 'There may be issues with your Bee node or connection.'

const THRESHOLDS: Thresholds = {
connectedPeers: [
{
minimumValue: OPTIMAL_CONNECTED_PEERS,
explanation: `Perfect! ${OPTIMAL_CONNECTED_PEERS} or more connected peers indicate a healthy topology.`,
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is connected to peers, but this number should ideally be above ${OPTIMAL_CONNECTED_PEERS}. If you have only started your Bee node, this number may increase quickly.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not connected to any peers. ' + GENERIC_ERROR,
score: 0,
},
],
population: [
{
minimumValue: OPTIMAL_POPULATION,
explanation:
'Perfect! Your Bee node seems to have a realistic value for the network size, which means everything is working well on your end.',
score: 2,
},
{
minimumValue: 1,
explanation: `Population is usually above ${OPTIMAL_POPULATION.toLocaleString()}. If the number does not increase within a few hours, there may be issues with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has no information on the network population. ' + GENERIC_ERROR,
score: 0,
},
],
depth: [
{
minimumValue: OPTIMAL_DEPTH,
explanation: 'Perfect! Your Bee node has the highest available depth.',
score: 2,
},
{
minimumValue: 1,
explanation: `Your Bee node is supposed to reach a depth of ${OPTIMAL_DEPTH} eventually. Stagnation or decrease in this number may indicate problems with your Bee node.`,
score: 1,
},
{
minimumValue: 0,
explanation: 'Your Bee node has not started building its topology yet. ' + GENERIC_ERROR,
score: 0,
},
],
}

export function pickThreshold(key: keyof Thresholds, value: number): ThresholdValue {
const thresholds = THRESHOLDS[key]
const maximumScore = thresholds[0].score
for (const item of thresholds) {
if (value >= item.minimumValue) {
return {
score: item.score,
maximumScore,
explanation: item.explanation,
}
}
}
const last = thresholds[thresholds.length - 1]

return {
score: last.score,
maximumScore,
explanation: last.explanation,
}
}

0 comments on commit 480f6dc

Please sign in to comment.