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 { Card, CardContent, Typography } from '@material-ui/core/'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { Skeleton } from '@material-ui/lab'
|
import { Skeleton } from '@material-ui/lab'
|
||||||
|
import type { ReactElement } from 'react'
|
||||||
|
import { Title } from './Title'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
minWidth: 275,
|
minWidth: 275,
|
||||||
},
|
},
|
||||||
title: {
|
|
||||||
fontSize: 16,
|
|
||||||
},
|
|
||||||
pos: {
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string
|
label: string
|
||||||
statistic?: string
|
statistic?: string
|
||||||
loading?: boolean
|
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()
|
const classes = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,9 +31,7 @@ export default function StatCard({ loading, label, statistic }: Props): ReactEle
|
|||||||
)}
|
)}
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<>
|
<>
|
||||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
<Title label={label} tooltip={tooltip} />
|
||||||
{label}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="h5" component="h2">
|
<Typography variant="h5" component="h2">
|
||||||
{statistic}
|
{statistic}
|
||||||
</Typography>
|
</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 type { Topology } from '@ethersphere/bee-js'
|
||||||
import { Grid } from '@material-ui/core/'
|
import { Grid } from '@material-ui/core/'
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
|
import { pickThreshold, ThresholdValues } from '../utils/threshold'
|
||||||
import StatCard from './StatCard'
|
import StatCard from './StatCard'
|
||||||
|
|
||||||
interface Props {
|
interface RootProps {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
topology: Topology | null
|
topology: Topology | null
|
||||||
error: Error | null // FIXME: should display error
|
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 style={{ marginBottom: '20px', flexGrow: 1 }}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
<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>
|
||||||
<Grid key={2} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
<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>
|
||||||
<Grid key={3} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
<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>
|
</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