feat: info page redesign (#207)
* feat: info page redesign * style: headers and nesting for expandable lists * style: body typography * chore: bee version check * feat: key list item component * style: hover state for the key displaying component * style: left border on expanded items * fix: word break and splitting for hexstrings divisible by 6 * chore: moved the styles to classes * style: tooltips now have arrow * feat: indicate value has been copied * feat: removed the connectivity table in favor of info page * feat: added health tooltips for connectivity * refactor: simplified the topology into single component * fix: spacing between the bee version and the update button/chip * fix: spacing on devices not supporting rowGap
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { ReactElement, ReactNode, useState } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Collapse, ListItem, ListItemText, Typography } from '@material-ui/core'
|
||||
import { ExpandLess, ExpandMore } from '@material-ui/icons'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
marginTop: theme.spacing(4),
|
||||
'&:first-child': {
|
||||
marginTop: 0,
|
||||
},
|
||||
},
|
||||
rootLevel1: { marginTop: theme.spacing(1) },
|
||||
rootLevel2: { marginTop: theme.spacing(0.5) },
|
||||
header: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
content: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
label: ReactNode
|
||||
level?: 0 | 1 | 2
|
||||
defaultOpen?: boolean
|
||||
}
|
||||
|
||||
export default function ExpandableList({ children, label, level, defaultOpen }: Props): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
const [open, setOpen] = useState<boolean>(Boolean(defaultOpen))
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
let rootLevelClass = ''
|
||||
let typographyVariant: 'h1' | 'h2' | 'h3' = 'h1'
|
||||
|
||||
if (level === 1) {
|
||||
rootLevelClass = classes.rootLevel1
|
||||
typographyVariant = 'h2'
|
||||
} else if (level === 2) {
|
||||
rootLevelClass = classes.rootLevel2
|
||||
typographyVariant = 'h3'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${classes.root} ${rootLevelClass}`}>
|
||||
<ListItem button onClick={handleClick} className={classes.header}>
|
||||
<ListItemText primary={<Typography variant={typographyVariant}>{label}</Typography>} />
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<div className={classes.content}>{children}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ReactElement, ReactNode } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import { Typography, Grid, IconButton, Tooltip } from '@material-ui/core'
|
||||
import { Info } from 'react-feather'
|
||||
import ListItem from '@material-ui/core/ListItem'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
marginBottom: theme.spacing(0.25),
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
copyValue: {
|
||||
cursor: 'pointer',
|
||||
padding: theme.spacing(1),
|
||||
borderRadius: 0,
|
||||
'&:hover': {
|
||||
backgroundColor: '#fcf2e8',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
value?: ReactNode
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export default function ExpandableListItem({ label, value, tooltip }: Props): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<ListItem className={classes.header}>
|
||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||
{label && <Typography variant="body1">{label}</Typography>}
|
||||
{value && (
|
||||
<Typography variant="body2">
|
||||
{value}
|
||||
{tooltip && (
|
||||
<Tooltip title={tooltip} placement="top" arrow>
|
||||
<IconButton size="small" className={classes.copyValue}>
|
||||
<Info strokeWidth={1} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
||||
import Collapse from '@material-ui/core/Collapse'
|
||||
import { ListItem, Typography, Grid, IconButton, Tooltip } from '@material-ui/core'
|
||||
import { Eye, Minus } from 'react-feather'
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
header: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
marginBottom: theme.spacing(0.25),
|
||||
borderLeft: `${theme.spacing(0.25)}px solid rgba(0,0,0,0)`,
|
||||
wordBreak: 'break-word',
|
||||
},
|
||||
headerOpen: {
|
||||
borderLeft: `${theme.spacing(0.25)}px solid ${theme.palette.primary.main}`,
|
||||
},
|
||||
copyValue: {
|
||||
cursor: 'pointer',
|
||||
padding: theme.spacing(1),
|
||||
borderRadius: 0,
|
||||
'&:hover': {
|
||||
backgroundColor: '#fcf2e8',
|
||||
color: theme.palette.primary.main,
|
||||
},
|
||||
},
|
||||
content: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
keyMargin: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const lengthWithoutPrefix = (s: string) => s.replace(/^0x/i, '').length
|
||||
|
||||
function isPrefixedHexString(s: unknown): boolean {
|
||||
return typeof s === 'string' && /^0x[0-9a-f]+$/i.test(s)
|
||||
}
|
||||
|
||||
const split = (s: string): string[] => {
|
||||
const nonPrefixLength = lengthWithoutPrefix(s)
|
||||
|
||||
if (nonPrefixLength % 6 === 0) return s.match(/(0x|.{6})/gi) || []
|
||||
|
||||
return s.match(/(0x|.{1,8})/gi) || []
|
||||
}
|
||||
|
||||
export default function ExpandableListItemKey({ label, value }: Props): ReactElement | null {
|
||||
const classes = useStyles()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const toggleOpen = () => setOpen(!open)
|
||||
|
||||
const tooltipClickHandler = () => setCopied(true)
|
||||
const tooltipCloseHandler = () => setCopied(false)
|
||||
|
||||
const splitValues = split(value)
|
||||
const hasPrefix = isPrefixedHexString(value)
|
||||
|
||||
return (
|
||||
<ListItem className={`${classes.header} ${open ? classes.headerOpen : ''}`}>
|
||||
<Grid container direction="column" justifyContent="space-between" alignItems="stretch">
|
||||
<Grid container direction="row" justifyContent="space-between" alignItems="center">
|
||||
{label && <Typography variant="body1">{label}</Typography>}
|
||||
<Typography variant="body2">
|
||||
<div>
|
||||
{!open && (
|
||||
<span className={classes.copyValue}>
|
||||
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
|
||||
<CopyToClipboard text={value}>
|
||||
<span onClick={tooltipClickHandler}>{`${
|
||||
hasPrefix ? `${splitValues[0]} ${splitValues[1]}` : splitValues[0]
|
||||
}[…]${splitValues[splitValues.length - 1]}`}</span>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
<IconButton size="small" className={classes.copyValue}>
|
||||
{open ? <Minus onClick={toggleOpen} strokeWidth={1} /> : <Eye onClick={toggleOpen} strokeWidth={1} />}
|
||||
</IconButton>
|
||||
</div>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<div className={classes.content}>
|
||||
<Tooltip title={copied ? 'Copied' : 'Copy'} placement="top" arrow onClose={tooltipCloseHandler}>
|
||||
<CopyToClipboard text={value}>
|
||||
{/* This has to be wrapped in two spans otherwise either the tooltip or the highlighting does not work*/}
|
||||
<span onClick={tooltipClickHandler}>
|
||||
<span className={classes.copyValue}>
|
||||
{splitValues.map((s, i) => (
|
||||
<Typography variant="body2" key={i} className={classes.keyMargin} component="span">
|
||||
{s}
|
||||
</Typography>
|
||||
))}
|
||||
</span>
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Collapse>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom'
|
||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||
import { OpenInNewSharp } from '@material-ui/icons'
|
||||
import { Divider, List, Drawer, Grid, Link as MUILink } from '@material-ui/core'
|
||||
import { Home, FileText, DollarSign, Share2, Settings, Layers, BookOpen } from 'react-feather'
|
||||
import { Home, FileText, DollarSign, Settings, Layers, BookOpen } from 'react-feather'
|
||||
import { ROUTES } from '../routes'
|
||||
import SideBarItem from './SideBarItem'
|
||||
import SideBarStatus from './SideBarStatus'
|
||||
@@ -32,11 +32,6 @@ const navBarItems = [
|
||||
path: ROUTES.ACCOUNTING,
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
label: 'Peers',
|
||||
path: ROUTES.PEERS,
|
||||
icon: Share2,
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
path: ROUTES.SETTINGS,
|
||||
|
||||
@@ -1,64 +1,44 @@
|
||||
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'
|
||||
import ExpandableList from './ExpandableList'
|
||||
import ExpandableListItem from './ExpandableListItem'
|
||||
|
||||
interface RootProps {
|
||||
interface Props {
|
||||
topology: Topology | null
|
||||
}
|
||||
|
||||
interface Props extends RootProps {
|
||||
thresholds: ThresholdValues
|
||||
}
|
||||
|
||||
const TopologyStats = (props: RootProps): ReactElement => {
|
||||
const TopologyStats = (props: Props): 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 = ({ 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} />
|
||||
</div>
|
||||
<ExpandableList label="Connectivity" defaultOpen>
|
||||
<ExpandableListItem label="Overall Health Indicator" value={percentageText} />
|
||||
<ExpandableListItem
|
||||
label="Connected Peers"
|
||||
value={props.topology?.connected.toString()}
|
||||
tooltip={thresholds.connectedPeers.explanation}
|
||||
/>
|
||||
<ExpandableListItem
|
||||
label="Pupulation"
|
||||
value={props.topology?.population.toString()}
|
||||
tooltip={thresholds.population.explanation}
|
||||
/>
|
||||
<ExpandableListItem
|
||||
label="Depth"
|
||||
value={props.topology?.depth.toString()}
|
||||
tooltip={thresholds.depth.explanation}
|
||||
/>
|
||||
</ExpandableList>
|
||||
)
|
||||
}
|
||||
|
||||
const Metrics = ({ topology, thresholds }: Props): ReactElement => (
|
||||
<Grid container spacing={3} style={{ marginBottom: '20px' }}>
|
||||
<Grid key={1} item xs={12} sm={12} md={6} lg={4} xl={4}>
|
||||
<StatCard
|
||||
label="Connected Peers"
|
||||
statistic={topology?.connected.toString()}
|
||||
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()}
|
||||
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()} tooltip={thresholds.depth.explanation} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
|
||||
export default TopologyStats
|
||||
|
||||
Reference in New Issue
Block a user