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 { createStyles, Theme, makeStyles } from '@material-ui/core/styles'
|
||||||
import { OpenInNewSharp } from '@material-ui/icons'
|
import { OpenInNewSharp } from '@material-ui/icons'
|
||||||
import { Divider, List, Drawer, Grid, Link as MUILink } from '@material-ui/core'
|
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 { ROUTES } from '../routes'
|
||||||
import SideBarItem from './SideBarItem'
|
import SideBarItem from './SideBarItem'
|
||||||
import SideBarStatus from './SideBarStatus'
|
import SideBarStatus from './SideBarStatus'
|
||||||
@@ -32,11 +32,6 @@ const navBarItems = [
|
|||||||
path: ROUTES.ACCOUNTING,
|
path: ROUTES.ACCOUNTING,
|
||||||
icon: DollarSign,
|
icon: DollarSign,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Peers',
|
|
||||||
path: ROUTES.PEERS,
|
|
||||||
icon: Share2,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
path: ROUTES.SETTINGS,
|
path: ROUTES.SETTINGS,
|
||||||
|
|||||||
@@ -1,64 +1,44 @@
|
|||||||
import type { Topology } from '@ethersphere/bee-js'
|
import type { Topology } from '@ethersphere/bee-js'
|
||||||
import { Grid } from '@material-ui/core/'
|
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { pickThreshold, ThresholdValues } from '../utils/threshold'
|
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
|
topology: Topology | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends RootProps {
|
const TopologyStats = (props: Props): ReactElement => {
|
||||||
thresholds: ThresholdValues
|
|
||||||
}
|
|
||||||
|
|
||||||
const TopologyStats = (props: RootProps): ReactElement => {
|
|
||||||
const thresholds: ThresholdValues = {
|
const thresholds: ThresholdValues = {
|
||||||
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
|
connectedPeers: pickThreshold('connectedPeers', props.topology?.connected || 0),
|
||||||
population: pickThreshold('population', props.topology?.population || 0),
|
population: pickThreshold('population', props.topology?.population || 0),
|
||||||
depth: pickThreshold('depth', props.topology?.depth || 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 maximumTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.maximumScore, 0)
|
||||||
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
|
const actualTotalScore = Object.values(thresholds).reduce((sum, item) => sum + item.score, 0)
|
||||||
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
|
const percentageText = Math.round((actualTotalScore / maximumTotalScore) * 100) + '%'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: '20px' }}>
|
<ExpandableList label="Connectivity" defaultOpen>
|
||||||
<StatCard label="Overall Health Indicator" statistic={percentageText} />
|
<ExpandableListItem label="Overall Health Indicator" value={percentageText} />
|
||||||
</div>
|
<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
|
export default TopologyStats
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import { ReactElement, useState } from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
|
||||||
import { Card, CardContent, Typography, Chip, Button } from '@material-ui/core/'
|
|
||||||
import { ArrowRight, ArrowDropUp } from '@material-ui/icons/'
|
|
||||||
import { NodeAddresses, Topology } from '@ethersphere/bee-js'
|
|
||||||
import { ROUTES } from '../../routes'
|
|
||||||
|
|
||||||
const useStyles = makeStyles(() =>
|
|
||||||
createStyles({
|
|
||||||
root: {
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1 1 auto',
|
|
||||||
flexDirection: 'column',
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
color: '#2145a0',
|
|
||||||
backgroundColor: '#e1effe',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
nodeAddresses: NodeAddresses | null
|
|
||||||
nodeTopology: Topology | null
|
|
||||||
userBeeVersion?: string
|
|
||||||
isLatestBeeVersion: boolean
|
|
||||||
isOk: boolean
|
|
||||||
latestUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusCard({
|
|
||||||
userBeeVersion,
|
|
||||||
nodeAddresses,
|
|
||||||
nodeTopology,
|
|
||||||
isLatestBeeVersion,
|
|
||||||
latestUrl,
|
|
||||||
}: Props): ReactElement | null {
|
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const [underlayAddressesVisible, setUnderlayAddresessVisible] = useState<boolean>(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className={classes.root}>
|
|
||||||
<>
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<span style={{ marginRight: '20px' }}>Discovered Nodes: {nodeTopology?.population}</span>
|
|
||||||
<span style={{ marginRight: '20px' }}>
|
|
||||||
<span>Connected Peers: </span>
|
|
||||||
<Link to={ROUTES.PEERS}>{nodeTopology?.connected}</Link>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>AGENT: </span>
|
|
||||||
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
|
|
||||||
Bee
|
|
||||||
</a>{' '}
|
|
||||||
<span>{userBeeVersion || '-'}</span>
|
|
||||||
{isLatestBeeVersion ? (
|
|
||||||
<Chip
|
|
||||||
style={{ marginLeft: '7px', color: '#2145a0' }}
|
|
||||||
size="small"
|
|
||||||
label="latest"
|
|
||||||
className={classes.status}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button size="small" variant="outlined" href={latestUrl}>
|
|
||||||
update
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>PUBLIC KEY: </span>
|
|
||||||
<span>{nodeAddresses?.publicKey ? nodeAddresses.publicKey : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>PSS PUBLIC KEY: </span>
|
|
||||||
<span>{nodeAddresses?.pssPublicKey ? nodeAddresses.pssPublicKey : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<span>OVERLAY ADDRESS (PEER ID): </span>
|
|
||||||
<span>{nodeAddresses?.overlay ? nodeAddresses.overlay : '-'}</span>
|
|
||||||
</Typography>
|
|
||||||
<Typography component="div" variant="subtitle2" gutterBottom>
|
|
||||||
<Typography component="div" onClick={() => setUnderlayAddresessVisible(!underlayAddressesVisible)}>
|
|
||||||
<Button color="primary" style={{ padding: 0, marginTop: '6px' }}>
|
|
||||||
{underlayAddressesVisible ? (
|
|
||||||
<ArrowDropUp style={{ fontSize: '12px' }} />
|
|
||||||
) : (
|
|
||||||
<ArrowRight style={{ fontSize: '12px' }} />
|
|
||||||
)}
|
|
||||||
<span>Underlay Addresses</span>
|
|
||||||
</Button>
|
|
||||||
</Typography>
|
|
||||||
{underlayAddressesVisible && (
|
|
||||||
<div>
|
|
||||||
{nodeAddresses?.underlay.map(item => (
|
|
||||||
<li key={item}>{item}</li>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StatusCard
|
|
||||||
+39
-26
@@ -1,24 +1,14 @@
|
|||||||
import { ReactElement, useContext } from 'react'
|
import { ReactElement, useContext } from 'react'
|
||||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'
|
import { Chip, Button } from '@material-ui/core'
|
||||||
|
|
||||||
import StatusCard from './StatusCard'
|
|
||||||
import EthereumAddressCard from '../../components/EthereumAddressCard'
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
||||||
import { Context as BeeContext } from '../../providers/Bee'
|
import { Context as BeeContext } from '../../providers/Bee'
|
||||||
|
import ExpandableList from '../../components/ExpandableList'
|
||||||
const useStyles = makeStyles((theme: Theme) =>
|
import ExpandableListItem from '../../components/ExpandableListItem'
|
||||||
createStyles({
|
import ExpandableListItemKey from '../../components/ExpandableListItemKey'
|
||||||
root: {
|
import TopologyStats from '../../components/TopologyStats'
|
||||||
width: '100%',
|
|
||||||
display: 'grid',
|
|
||||||
rowGap: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
export default function Status(): ReactElement {
|
export default function Status(): ReactElement {
|
||||||
const classes = useStyles()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
latestUserVersion,
|
latestUserVersion,
|
||||||
@@ -32,18 +22,41 @@ export default function Status(): ReactElement {
|
|||||||
if (!status.all) return <TroubleshootConnectionCard />
|
if (!status.all) return <TroubleshootConnectionCard />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div>
|
||||||
<StatusCard
|
<ExpandableList label="Bee Node" defaultOpen>
|
||||||
userBeeVersion={latestUserVersion}
|
<ExpandableListItem
|
||||||
isLatestBeeVersion={isLatestBeeVersion}
|
label="Agent"
|
||||||
isOk={status.all}
|
value={
|
||||||
nodeTopology={topology}
|
<div>
|
||||||
latestUrl={latestBeeVersionUrl}
|
<a href="https://github.com/ethersphere/bee" rel="noreferrer" target="_blank">
|
||||||
nodeAddresses={nodeAddresses}
|
Bee
|
||||||
/>
|
</a>{' '}
|
||||||
{nodeAddresses && chequebookAddress && (
|
<span>{latestUserVersion || '-'}</span>{' '}
|
||||||
<EthereumAddressCard nodeAddresses={nodeAddresses} chequebookAddress={chequebookAddress} />
|
{isLatestBeeVersion ? (
|
||||||
|
<Chip style={{ marginLeft: '7px', color: '#2145a0' }} size="small" label="latest" />
|
||||||
|
) : (
|
||||||
|
<Button size="small" variant="outlined" href={latestBeeVersionUrl}>
|
||||||
|
update
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ExpandableListItemKey label="Public key" value={nodeAddresses?.publicKey || ''} />
|
||||||
|
<ExpandableListItemKey label="PSS public key" value={nodeAddresses?.pssPublicKey || ''} />
|
||||||
|
<ExpandableListItemKey label="Overlay address (Peer ID)" value={nodeAddresses?.overlay || ''} />
|
||||||
|
|
||||||
|
<ExpandableList level={1} label="Underlay addresses">
|
||||||
|
{nodeAddresses?.underlay.map(addr => (
|
||||||
|
<ExpandableListItem key={addr} value={addr} />
|
||||||
|
))}
|
||||||
|
</ExpandableList>
|
||||||
|
</ExpandableList>
|
||||||
|
<ExpandableList label="Blockchain" defaultOpen>
|
||||||
|
<ExpandableListItemKey label="Ethereum address" value={nodeAddresses?.ethereum || ''} />
|
||||||
|
<ExpandableListItemKey label="Chequebook contract address" value={chequebookAddress?.chequebookAddress || ''} />
|
||||||
|
</ExpandableList>
|
||||||
|
<TopologyStats topology={topology} />
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { ReactElement, useState, useContext } from 'react'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableRow,
|
|
||||||
TableHead,
|
|
||||||
Button,
|
|
||||||
Paper,
|
|
||||||
Tooltip,
|
|
||||||
CircularProgress,
|
|
||||||
} from '@material-ui/core'
|
|
||||||
import { Autorenew } from '@material-ui/icons'
|
|
||||||
|
|
||||||
import { Context as SettingsContext } from '../../providers/Settings'
|
|
||||||
import type { Peer } from '@ethersphere/bee-js'
|
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
|
||||||
table: {
|
|
||||||
minWidth: 650,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
peers: Peer[] | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PeerLatency {
|
|
||||||
rtt: string
|
|
||||||
loading: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPingState(peerLatency: Record<string, PeerLatency>, peer: Peer): ReactElement {
|
|
||||||
if (peerLatency[peer.address]?.loading) return <CircularProgress size={20} />
|
|
||||||
|
|
||||||
if (peerLatency[peer.address]?.rtt) return <span>{peerLatency[peer.address]?.rtt}</span>
|
|
||||||
|
|
||||||
return <Autorenew />
|
|
||||||
}
|
|
||||||
|
|
||||||
function PeerTable(props: Props): ReactElement {
|
|
||||||
const classes = useStyles()
|
|
||||||
const { beeDebugApi } = useContext(SettingsContext)
|
|
||||||
|
|
||||||
const [peerLatency, setPeerLatency] = useState<Record<string, PeerLatency>>({})
|
|
||||||
|
|
||||||
const pingPeer = (peerId: string) => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: '', loading: true } }))
|
|
||||||
beeDebugApi
|
|
||||||
?.pingPeer(peerId)
|
|
||||||
.then(res => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: res.rtt, loading: false } }))
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setPeerLatency(prevPeerLatency => ({ ...prevPeerLatency, [peerId]: { rtt: 'error', loading: false } }))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer component={Paper}>
|
|
||||||
<Table className={classes.table}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>Index</TableCell>
|
|
||||||
<TableCell>Peer Id</TableCell>
|
|
||||||
<TableCell align="right">Actions</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{props.peers?.map((peer: Peer, idx: number) => (
|
|
||||||
<TableRow key={peer.address}>
|
|
||||||
<TableCell component="th" scope="row">
|
|
||||||
{idx + 1}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{peer.address}</TableCell>
|
|
||||||
<TableCell align="right">
|
|
||||||
<Tooltip title="Ping node">
|
|
||||||
<Button color="primary" onClick={() => pingPeer(peer.address)}>
|
|
||||||
{getPingState(peerLatency, peer)}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PeerTable
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import PeerTable from './PeerTable'
|
|
||||||
import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard'
|
|
||||||
|
|
||||||
import { Context } from '../../providers/Bee'
|
|
||||||
import TopologyStats from '../../components/TopologyStats'
|
|
||||||
import { ReactElement, useContext } from 'react'
|
|
||||||
|
|
||||||
export default function Peers(): ReactElement {
|
|
||||||
const { topology, peers, status } = useContext(Context)
|
|
||||||
|
|
||||||
if (!status.all) {
|
|
||||||
return <TroubleshootConnectionCard />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TopologyStats topology={topology} />
|
|
||||||
<PeerTable peers={peers} />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import { Route } from 'react-router-dom'
|
|||||||
import Info from './pages/info'
|
import Info from './pages/info'
|
||||||
import Status from './pages/status'
|
import Status from './pages/status'
|
||||||
import Files from './pages/files'
|
import Files from './pages/files'
|
||||||
import Peers from './pages/peers'
|
|
||||||
import Accounting from './pages/accounting'
|
import Accounting from './pages/accounting'
|
||||||
import Settings from './pages/settings'
|
import Settings from './pages/settings'
|
||||||
import Stamps from './pages/stamps'
|
import Stamps from './pages/stamps'
|
||||||
@@ -14,7 +13,6 @@ import Stamps from './pages/stamps'
|
|||||||
export enum ROUTES {
|
export enum ROUTES {
|
||||||
INFO = '/',
|
INFO = '/',
|
||||||
FILES = '/files',
|
FILES = '/files',
|
||||||
PEERS = '/peers',
|
|
||||||
ACCOUNTING = '/accounting',
|
ACCOUNTING = '/accounting',
|
||||||
SETTINGS = '/settings',
|
SETTINGS = '/settings',
|
||||||
STAMPS = '/stamps',
|
STAMPS = '/stamps',
|
||||||
@@ -25,7 +23,6 @@ const BaseRouter = (): ReactElement => (
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={ROUTES.INFO} component={Info} />
|
<Route exact path={ROUTES.INFO} component={Info} />
|
||||||
<Route exact path={ROUTES.FILES} component={Files} />
|
<Route exact path={ROUTES.FILES} component={Files} />
|
||||||
<Route exact path={ROUTES.PEERS} component={Peers} />
|
|
||||||
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
<Route exact path={ROUTES.ACCOUNTING} component={Accounting} />
|
||||||
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
<Route exact path={ROUTES.SETTINGS} component={Settings} />
|
||||||
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
<Route exact path={ROUTES.STAMPS} component={Stamps} />
|
||||||
|
|||||||
@@ -130,6 +130,19 @@ export const theme = createMuiTheme({
|
|||||||
fontSize: '1.3rem',
|
fontSize: '1.3rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
body2: {
|
||||||
|
fontFamily: '"IBM Plex Mono", monospace',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user