diff --git a/src/components/StatCard.tsx b/src/components/StatCard.tsx index 13c1c52..e581c97 100644 --- a/src/components/StatCard.tsx +++ b/src/components/StatCard.tsx @@ -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 && ( <> - - {label} - + <Typography variant="h5" component="h2"> {statistic} </Typography> diff --git a/src/components/Title.tsx b/src/components/Title.tsx new file mode 100644 index 0000000..9819d70 --- /dev/null +++ b/src/components/Title.tsx @@ -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> + ) +} diff --git a/src/components/TopologyStats.tsx b/src/components/TopologyStats.tsx index 023ecfc..6eeceb1 100644 --- a/src/components/TopologyStats.tsx +++ b/src/components/TopologyStats.tsx @@ -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> diff --git a/src/utils/threshold.ts b/src/utils/threshold.ts new file mode 100644 index 0000000..d7a1baf --- /dev/null +++ b/src/utils/threshold.ts @@ -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, + } +}