Skip to content

Commit

Permalink
feat: info page redesign (#207)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
vojtechsimetka committed Oct 4, 2021
1 parent c4c7d96 commit 57f5a73
Show file tree
Hide file tree
Showing 11 changed files with 306 additions and 302 deletions.
65 changes: 65 additions & 0 deletions src/components/ExpandableList.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
54 changes: 54 additions & 0 deletions src/components/ExpandableListItem.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
114 changes: 114 additions & 0 deletions src/components/ExpandableListItemKey.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
7 changes: 1 addition & 6 deletions src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -32,11 +32,6 @@ const navBarItems = [
path: ROUTES.ACCOUNTING,
icon: DollarSign,
},
{
label: 'Peers',
path: ROUTES.PEERS,
icon: Share2,
},
{
label: 'Settings',
path: ROUTES.SETTINGS,
Expand Down
58 changes: 19 additions & 39 deletions src/components/TopologyStats.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}

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
<ExpandableList label="Connectivity" defaultOpen>
<ExpandableListItem label="Overall Health Indicator" value={percentageText} />
<ExpandableListItem
label="Connected Peers"
statistic={topology?.connected.toString()}
value={props.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()}
<ExpandableListItem
label="Pupulation"
value={props.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>
)
<ExpandableListItem
label="Depth"
value={props.topology?.depth.toString()}
tooltip={thresholds.depth.explanation}
/>
</ExpandableList>
)
}

export default TopologyStats
Loading

0 comments on commit 57f5a73

Please sign in to comment.