feat: add tooltips and health indicator to peers (#169)
* 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
This commit is contained in:
@@ -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 (
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user