feat: initial Proof of Concept UI (#1)
* initial dashboard layout * add node status card * add accounting section, pull peer data * add file functionality with bee-js, first iteration of accounts page * Add balances and chequebook table * add blockie / identicon for addresses * add basic settlements table * implement theme overrides * cleanup logging * Add troubleshooting block * add initial dark theme support, add copy to clipboard, QR code support * show active element on sidebar * remove duplicate status page and make status page index * Update package.json Co-authored-by: Vojtech Simetka <vojtech@simetka.cz> * Update public/index.html Co-authored-by: Vojtech Simetka <vojtech@simetka.cz> * Update src/pages/accounting/AccountCard.tsx Co-authored-by: Vojtech Simetka <vojtech@simetka.cz> * change bee api client to use beeJS library * add initial setup workflow * breakout ethereum address component, define initial setup workflow * add types to responses, add additional node troubleshooting info to workflow * make setup steps nonlinear and interactive * make host endpoint dynamic on setup * split out api calls into custom hooks, add component loading indicators * add depost / withdrawl functionality, show transactions in BZZ * add multiOS code support troubleshooting, check for balance in chequebook on setup * add ability to change apis in settings page * show file loading status * Style active sidebar item * reload on theme change * modify troubleshooting verbage, add cashout functionality and details, * facilitate file upload with beeJS * update readme to show UI samples * remove nnPeersWatermark from peers page * split node steps into separate components, make status page visible at anytime * minor UI/UX enhancements * format accounting page * remove WIP wallet connection code * Update src/components/CashoutModal.tsx Co-authored-by: Vojtech Simetka <vojtech@simetka.cz> * use bigint for deposits/withdrawls * revise status card * clean up unused imports and variables * add api status to sidebar * obfuscate pages with troubleshooting component when apis not connected * add localhost OS detection for troubleshooting code * cleanup extra logos * monospace BZZ in tables * hide troubleshooting page while loading API status * Remove ability to remove peers * add null types to API responses Co-authored-by: Vojtech Simetka <vojtech@simetka.cz>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
|
||||
import { beeDebugApi } from '../services/bee';
|
||||
|
||||
export default function DepositModal() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [peerId, setPeerId] = React.useState('');
|
||||
|
||||
const [showToast, setToastVisibility] = React.useState(false);
|
||||
const [toastContent, setToastContent] = React.useState('');
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleCashout = () => {
|
||||
if (peerId) {
|
||||
beeDebugApi.chequebook.peerCashout(peerId)
|
||||
.then(res => {
|
||||
setOpen(false);
|
||||
handleToast(`Successfully cashed out cheque. Transaction ${res.data.transactionHash}`)
|
||||
})
|
||||
.catch(error => {
|
||||
handleToast('Error with cashout')
|
||||
})
|
||||
} else {
|
||||
handleToast('Peer Id invalid')
|
||||
}
|
||||
};
|
||||
|
||||
const handleToast = (text: string) => {
|
||||
setToastContent(text)
|
||||
setToastVisibility(true);
|
||||
setTimeout(
|
||||
() => setToastVisibility(false),
|
||||
7000
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="contained" color="primary" onClick={handleClickOpen} style={{marginLeft:'7px'}}>
|
||||
Cashout
|
||||
</Button>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
open={showToast}
|
||||
message={toastContent}
|
||||
/>
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Cashout Cheque</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText style={{marginTop: '20px'}}>
|
||||
Specify the peer Id of the peer you would like to cashout.
|
||||
</DialogContentText>
|
||||
<Input
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="peerId"
|
||||
type="text"
|
||||
placeholder='Peer Id'
|
||||
fullWidth
|
||||
onChange={(e) => setPeerId(e.target.value)}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCashout} color="primary">
|
||||
Cashout
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Snackbar } from '@material-ui/core';
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard';
|
||||
import { Clipboard } from 'react-feather';
|
||||
|
||||
interface IProps {
|
||||
value: string,
|
||||
}
|
||||
|
||||
export default function ClipboardCopy(props: IProps) {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
setCopied(true);
|
||||
setTimeout(
|
||||
() => setCopied(false),
|
||||
3000
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div style={{marginRight:'3px', marginLeft:'3px'}}>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
open={copied}
|
||||
message="Copied"
|
||||
/>
|
||||
<IconButton color="primary" size='small' onClick={handleCopy}>
|
||||
<CopyToClipboard text={props.value}
|
||||
>
|
||||
<Clipboard style={{height:'20px'}}/>
|
||||
</CopyToClipboard>
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter';
|
||||
import { hybrid } from 'react-syntax-highlighter/dist/esm/styles/hljs';
|
||||
|
||||
interface IProps {
|
||||
code: string,
|
||||
language: string,
|
||||
showLineNumbers?: boolean,
|
||||
}
|
||||
|
||||
const CodeBlock = (props: IProps) => {
|
||||
return (
|
||||
<div style={{textAlign:'left'}}>
|
||||
<SyntaxHighlighter
|
||||
language={props.language}
|
||||
style={hybrid}
|
||||
showLineNumbers={props.showLineNumbers}
|
||||
>
|
||||
{props.code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeBlock;
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { withStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import { Tabs, Tab, Box, Typography } from '@material-ui/core';
|
||||
import CodeBlock from './CodeBlock';
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
linux: string;
|
||||
mac: string;
|
||||
showLineNumbers?: boolean
|
||||
}
|
||||
|
||||
function a11yProps(index: any) {
|
||||
return {
|
||||
id: `simple-tab-${index}`,
|
||||
'aria-controls': `simple-tabpanel-${index}`,
|
||||
};
|
||||
}
|
||||
|
||||
function getOS() {
|
||||
var userAgent = window.navigator.userAgent,
|
||||
platform = window.navigator.platform,
|
||||
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
|
||||
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
|
||||
iosPlatforms = ['iPhone', 'iPad', 'iPod'],
|
||||
os = null;
|
||||
|
||||
if (macosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'macOS';
|
||||
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'iOS';
|
||||
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
||||
os = 'windows';
|
||||
} else if (/Android/.test(userAgent)) {
|
||||
os = 'android';
|
||||
} else if (!os && /Linux/.test(platform)) {
|
||||
os = 'linux';
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
export default function CodeBlockTabs(props: IProps) {
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let os = getOS()
|
||||
if(os === 'windows') {
|
||||
setValue(0)
|
||||
} else if (os === 'linux') {
|
||||
setValue(0)
|
||||
} else if (os === 'macOS') {
|
||||
setValue(1)
|
||||
}
|
||||
|
||||
}, [])
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`simple-tabpanel-${index}`}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box style={{ marginTop: '-12px' }}>
|
||||
<Typography>{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const AntTabs = withStyles({
|
||||
root: {
|
||||
borderBottom: '1px solid #e8e8e8',
|
||||
},
|
||||
indicator: {
|
||||
backgroundColor: '#3f51b5',
|
||||
},
|
||||
})(Tabs);
|
||||
|
||||
interface StyledTabProps {
|
||||
label: string;
|
||||
}
|
||||
|
||||
const AntTab = withStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
textTransform: 'none',
|
||||
minWidth: 72,
|
||||
backgroundColor: 'transparent',
|
||||
fontWeight: theme.typography.fontWeightRegular,
|
||||
marginRight: theme.spacing(4),
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
'&:hover': {
|
||||
color: '#3f51b5',
|
||||
opacity: 1,
|
||||
},
|
||||
'&$selected': {
|
||||
color: '#3f51b5',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
},
|
||||
'&:focus': {
|
||||
color: '#3f51b5',
|
||||
},
|
||||
},
|
||||
selected: {},
|
||||
}),
|
||||
)((props: StyledTabProps) => <Tab disableRipple {...props} />);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AntTabs style={{ marginTop: '12px' }} value={value} onChange={handleChange} aria-label="ant example">
|
||||
<AntTab label="Linux" {...a11yProps(0)} />
|
||||
<AntTab label="MacOS" {...a11yProps(1)} />
|
||||
</AntTabs>
|
||||
<TabPanel value={value} index={0}>
|
||||
<CodeBlock
|
||||
showLineNumbers={props.showLineNumbers}
|
||||
language='bash'
|
||||
code={props.linux}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={value} index={1}>
|
||||
<CodeBlock
|
||||
showLineNumbers={props.showLineNumbers}
|
||||
language='bash'
|
||||
code={props.mac}
|
||||
/>
|
||||
</TabPanel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField, Button, CircularProgress, Container } from '@material-ui/core';
|
||||
|
||||
interface IProps {
|
||||
defaultHost?: string,
|
||||
hostName: string,
|
||||
}
|
||||
|
||||
export default function ConnectToHost(props: IProps) {
|
||||
const [hostInputVisible, toggleHostInputVisibility] = useState(false)
|
||||
const [connectingToHost, setConnectingToHost] = useState(false)
|
||||
const [host, setHost] = useState('')
|
||||
|
||||
const handleNewHostConnection = () => {
|
||||
if (host) {
|
||||
setConnectingToHost(true)
|
||||
sessionStorage.setItem(props.hostName, host)
|
||||
toggleHostInputVisibility(!hostInputVisible)
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hostInputVisible ?
|
||||
<div style={{display:'flex'}}>
|
||||
<TextField
|
||||
defaultValue={props.defaultHost}
|
||||
label="Enter host"
|
||||
variant="outlined"
|
||||
size='small'
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
style={{marginRight:'15px', minWidth:'300px'}}
|
||||
/>
|
||||
<Button onClick={() => handleNewHostConnection()} size='small' variant="outlined">Connect</Button>
|
||||
<Button style={{marginLeft:'7px'}} onClick={() => toggleHostInputVisibility(!hostInputVisible)} size='small'>Cancel</Button>
|
||||
</div>
|
||||
:
|
||||
connectingToHost ?
|
||||
<Container style={{textAlign:'center', padding:'0px'}}>
|
||||
<CircularProgress size={20} />
|
||||
</Container>
|
||||
:
|
||||
<Button onClick={() => toggleHostInputVisibility(!hostInputVisible)} size='small' variant="outlined">Change host</Button>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
|
||||
import { beeDebugApi } from '../services/bee';
|
||||
|
||||
export default function DepositModal() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [amount, setAmount] = React.useState(BigInt(0));
|
||||
const [showToast, setToastVisibility] = React.useState(false);
|
||||
const [toastContent, setToastContent] = React.useState('');
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (amount > 0) {
|
||||
beeDebugApi.chequebook.deposit(amount)
|
||||
.then(res => {
|
||||
setOpen(false);
|
||||
handleToast(`Successful Deposit. Transaction ${res.data.transactionHash}`)
|
||||
})
|
||||
.catch(error => {
|
||||
handleToast('Error with Deposit')
|
||||
})
|
||||
} else {
|
||||
handleToast('Must be amount of greater than 0')
|
||||
}
|
||||
};
|
||||
|
||||
const handleToast = (text: string) => {
|
||||
setToastContent(text)
|
||||
setToastVisibility(true);
|
||||
setTimeout(
|
||||
() => setToastVisibility(false),
|
||||
7000
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="outlined" color="primary" onClick={handleClickOpen} style={{marginLeft:'7px'}}>
|
||||
Deposit
|
||||
</Button>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
open={showToast}
|
||||
message={toastContent}
|
||||
/>
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Deposit Funds</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Specify the amount you would like to deposit to your node.
|
||||
</DialogContentText>
|
||||
<Input
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
type="number"
|
||||
placeholder='Amount'
|
||||
fullWidth
|
||||
onChange={(e) => setAmount(BigInt(e.target.value))}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleWithdraw} color="primary">
|
||||
Deposit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Typography } from '@material-ui/core/';
|
||||
import QRCodeModal from './QRCodeModal';
|
||||
import ClipboardCopy from './ClipboardCopy';
|
||||
|
||||
// @ts-ignore
|
||||
import Identicon from 'react-identicons';
|
||||
|
||||
interface IProps {
|
||||
address: string | undefined,
|
||||
network?: string,
|
||||
hideBlockie?: boolean,
|
||||
transaction?: boolean,
|
||||
truncate?: boolean,
|
||||
}
|
||||
|
||||
export default function EthereumAddress(props: IProps) {
|
||||
return (
|
||||
<Typography component="p" variant="subtitle1">
|
||||
{props.address ?
|
||||
<div style={{display:'flex'}}>
|
||||
{props.hideBlockie ?
|
||||
null
|
||||
:
|
||||
<div style={{paddingTop:'5px', marginRight: '10px', }}>
|
||||
<Identicon size='20' string={props.address} />
|
||||
</div>}
|
||||
<div>
|
||||
<a
|
||||
style={props.truncate ?
|
||||
{ marginRight:'7px', maxWidth:'200px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display:'block'}
|
||||
:
|
||||
{ marginRight:'7px'}
|
||||
}
|
||||
href={`https://${props.network}.${process.env.REACT_APP_ETHERSCAN_HOST}/${props.transaction ? 'tx' : 'address' }/${props.address}`}
|
||||
target='_blank'
|
||||
rel="noreferrer"
|
||||
>
|
||||
{ props.address }
|
||||
</a>
|
||||
</div>
|
||||
<QRCodeModal
|
||||
value={ props.address }
|
||||
label={'Ethereum Address'}
|
||||
/>
|
||||
<ClipboardCopy
|
||||
value={ props.address }
|
||||
/>
|
||||
</div>
|
||||
: '-' }
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Theme, createStyles, makeStyles, useTheme } from '@material-ui/core/styles';
|
||||
import { Card, CardContent, Typography } from '@material-ui/core/';
|
||||
|
||||
import EthereumAddress from '../components/EthereumAddress';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
|
||||
import type { ChequebookAddressResponse } from '@ethersphere/bee-js';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
marginTop: '20px',
|
||||
},
|
||||
details: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
content: {
|
||||
flex: '1 0 auto',
|
||||
},
|
||||
status: {
|
||||
color: '#fff',
|
||||
backgroundColor: '#76a9fa',
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
interface NodeAddresses {
|
||||
overlay: string,
|
||||
underlay: string[],
|
||||
ethereum: string,
|
||||
public_key: string,
|
||||
pss_public_key: string
|
||||
}
|
||||
|
||||
interface IProps{
|
||||
nodeAddresses: NodeAddresses | null,
|
||||
isLoadingNodeAddresses: boolean,
|
||||
chequebookAddress: ChequebookAddressResponse | null,
|
||||
isLoadingChequebookAddress: boolean,
|
||||
}
|
||||
|
||||
function EthereumAddressCard(props: IProps) {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card className={classes.root}>
|
||||
{props.isLoadingNodeAddresses ?
|
||||
<div style={{padding: '16px'}}>
|
||||
<Skeleton width={300} height={30} animation="wave" />
|
||||
<Skeleton width={300} height={50} animation="wave" />
|
||||
</div>
|
||||
:
|
||||
<div className={classes.details}>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<span>Ethereum Address</span>
|
||||
</Typography>
|
||||
<EthereumAddress
|
||||
address={props.nodeAddresses?.ethereum}
|
||||
network={'goerli'}
|
||||
/>
|
||||
</CardContent>
|
||||
</div>}
|
||||
{props.isLoadingChequebookAddress ?
|
||||
<div style={{padding: '16px'}}>
|
||||
<Skeleton width={300} height={30} animation="wave" />
|
||||
<Skeleton width={300} height={50} animation="wave" />
|
||||
</div>
|
||||
:
|
||||
<div className={classes.details}>
|
||||
<CardContent className={classes.content}>
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<span>Chequebook Contract Address</span>
|
||||
</Typography>
|
||||
<EthereumAddress
|
||||
address={props.chequebookAddress?.chequebookaddress}
|
||||
network={'goerli'}
|
||||
/>
|
||||
</CardContent>
|
||||
</div>}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EthereumAddressCard
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
|
||||
import { AppBar, Toolbar, Chip, IconButton } from '@material-ui/core/';
|
||||
|
||||
import { Sun, Moon } from 'react-feather';
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
appBar: {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: drawerWidth,
|
||||
background:'linear-gradient(35deg,#fb6340,#fbb140)!important'
|
||||
},
|
||||
network: {
|
||||
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
export default function SideBar(props: any) {
|
||||
const [darkMode, toggleDarkMode] = useState(false);
|
||||
|
||||
const switchTheme = () => {
|
||||
let theme = localStorage.getItem('theme')
|
||||
if (theme) {
|
||||
localStorage.setItem('theme', theme === 'light' ? 'dark' : 'light')
|
||||
} else {
|
||||
localStorage.setItem('theme', darkMode ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
toggleDarkMode(!darkMode)
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AppBar position="fixed" className={classes.appBar}>
|
||||
<Toolbar style={{display: 'flex'}}>
|
||||
<Chip
|
||||
style={{ marginLeft: '7px'}}
|
||||
size="small"
|
||||
label='Goerli'
|
||||
className={classes.network}
|
||||
/>
|
||||
<div style={{width:'100%'}}>
|
||||
<div style={{float:'right'}} >
|
||||
<IconButton style={{marginRight:'10px'}} aria-label="dark-mode" onClick={() => switchTheme()}>
|
||||
{props.themeMode === 'dark' ?
|
||||
<Moon />
|
||||
:
|
||||
<Sun />
|
||||
}
|
||||
</IconButton>
|
||||
{/* <Chip
|
||||
label="Connect Wallet"
|
||||
color="primary"
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React,{ useState } from 'react';
|
||||
import QRCode from 'qrcode.react';
|
||||
import { IconButton, Dialog, DialogTitle } from '@material-ui/core';
|
||||
import { FilterCenterFocusSharp } from '@material-ui/icons';
|
||||
|
||||
interface IProps {
|
||||
value: string,
|
||||
label: string,
|
||||
}
|
||||
|
||||
export default function QRCodeModal(props: IProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton color="primary" size='small' onClick={handleOpen}>
|
||||
<FilterCenterFocusSharp/>
|
||||
</IconButton>
|
||||
<Dialog onClose={handleClose} aria-labelledby="simple-dialog-title" open={open}>
|
||||
<div style={{padding: '30px', textAlign: 'center'}}>
|
||||
<DialogTitle id="simple-dialog-title">{ props.label }</DialogTitle>
|
||||
<QRCode
|
||||
value={props.value}
|
||||
size={150}
|
||||
bgColor={"#ffffff"}
|
||||
fgColor={"#000000"}
|
||||
level={"L"}
|
||||
includeMargin={false}
|
||||
renderAs={"svg"}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import { Paper, InputBase, IconButton } from '@material-ui/core';
|
||||
import { Search } from '@material-ui/icons';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
padding: '2px 4px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: 400,
|
||||
},
|
||||
input: {
|
||||
marginLeft: theme.spacing(1),
|
||||
flex: 1,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 10,
|
||||
},
|
||||
divider: {
|
||||
height: 28,
|
||||
margin: 4,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
interface IProps {
|
||||
|
||||
}
|
||||
|
||||
export default function SearchBar(props: IProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Paper component="form" className={classes.root}>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Enter hash e.g. 0773a91efd6547c754fc1d95fb1c62c7d1b47f959c2caa685dfec8736da95c1c"
|
||||
inputProps={{ 'aria-label': 'search google maps' }}
|
||||
/>
|
||||
<IconButton type="submit" className={classes.iconButton} aria-label="search">
|
||||
<Search />
|
||||
</IconButton>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
|
||||
import { ListItemText, ListItemIcon, ListItem, Divider, List, Drawer, Link as MUILink} from '@material-ui/core';
|
||||
import { OpenInNewSharp } from '@material-ui/icons';
|
||||
import { Activity, FileText, DollarSign, Share2, Settings } from 'react-feather';
|
||||
|
||||
import SwarmLogo from '../assets/swarm-logo-2.svg';
|
||||
import SwarmLogoWhite from '../assets/swarm-logo-2-white.png';
|
||||
|
||||
const drawerWidth = 240;
|
||||
|
||||
const navBarItems = [
|
||||
{
|
||||
'label': 'Status',
|
||||
'id': 'status',
|
||||
'path': '/',
|
||||
'icon': 'activity'
|
||||
},
|
||||
{
|
||||
'label': 'Files',
|
||||
'id': 'files',
|
||||
'path': '/files/',
|
||||
'icon': 'file-text'
|
||||
},
|
||||
{
|
||||
'label': 'Accounting',
|
||||
'id': 'accounting',
|
||||
'path': '/accounting/',
|
||||
'icon': 'dollar-sign'
|
||||
},
|
||||
{
|
||||
'label': 'Peers',
|
||||
'id': 'peers',
|
||||
'path': '/peers/',
|
||||
'icon': 'share-2'
|
||||
},
|
||||
{
|
||||
'label': 'Settings',
|
||||
'id': 'settings',
|
||||
'path': '/settings/',
|
||||
'icon': 'settings'
|
||||
}
|
||||
]
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
},
|
||||
appBar: {
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
marginLeft: drawerWidth,
|
||||
backgroundColor:'#dd7200'
|
||||
},
|
||||
drawer: {
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
},
|
||||
drawerPaper: {
|
||||
width: drawerWidth,
|
||||
},
|
||||
activeSideBar: {
|
||||
color: '#fb6340',
|
||||
},
|
||||
activeSideBarItem: {
|
||||
borderLeft: '4px solid #fb6340',
|
||||
},
|
||||
toolbar: theme.mixins.toolbar,
|
||||
}),
|
||||
);
|
||||
|
||||
const getIcon = (iconPath: string) => {
|
||||
switch (iconPath) {
|
||||
case 'activity':
|
||||
return <Activity style={{height: '20px'}} />
|
||||
case 'file-text':
|
||||
return <FileText style={{height: '20px'}} />
|
||||
case 'dollar-sign':
|
||||
return <DollarSign style={{height: '20px'}} />
|
||||
case 'share-2':
|
||||
return <Share2 style={{height: '20px'}} />
|
||||
case 'settings':
|
||||
return <Settings style={{height: '20px'}} />
|
||||
}
|
||||
}
|
||||
|
||||
export default function SideBar(props: any) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Drawer
|
||||
className={classes.drawer}
|
||||
variant="permanent"
|
||||
classes={{
|
||||
paper: classes.drawerPaper,
|
||||
}}
|
||||
anchor="left"
|
||||
>
|
||||
<div className={classes.toolbar} style={{ textAlign:'center' }}>
|
||||
<Link to='/'>
|
||||
<img alt='swarm' src={props.themeMode === 'light' ? SwarmLogo : SwarmLogoWhite} style={{maxHeight: '60px', alignItems:'center'}} />
|
||||
</Link>
|
||||
</div>
|
||||
<Divider />
|
||||
<List>
|
||||
{navBarItems.map(item => (
|
||||
<Link to={item.path} key={item.id} style={{ color:'inherit', textDecoration:'none'}}>
|
||||
<ListItem button selected={props.location.pathname === item.path} className={props.location.pathname === item.path ? classes.activeSideBarItem : ''}>
|
||||
<ListItemIcon className={props.location.pathname === item.path ? classes.activeSideBar : ''}>
|
||||
{ getIcon(item.icon) }
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={item.label} className={props.location.pathname === item.path ? classes.activeSideBar : ''} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
<MUILink href={process.env.REACT_APP_BEE_DOCS_HOST} target="_blank" style={{textDecoration:'none'}}>
|
||||
<ListItem button>
|
||||
<ListItemText primary={'Docs'} />
|
||||
<OpenInNewSharp fontSize="small" />
|
||||
</ListItem>
|
||||
</MUILink>
|
||||
</List>
|
||||
<div style={{position:'fixed', bottom: 0, width: 'inherit', padding: '10px'}}>
|
||||
<ListItem>
|
||||
<div style={{marginRight:'30px'}}>
|
||||
<div style={{backgroundColor: props.health ? '#32c48d' : '#c9201f', marginRight: '7px', height: '10px',width: '10px', borderRadius: '50%', display: 'inline-block'}} />
|
||||
<span>API</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{backgroundColor: props.nodeHealth?.status === 'ok' ? '#32c48d' : '#c9201f', marginRight: '7px', height: '10px',width: '10px', borderRadius: '50%', display: 'inline-block'}} />
|
||||
<span>Debug API</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react'
|
||||
|
||||
import { makeStyles, } from '@material-ui/core/styles';
|
||||
import { Card, CardContent, Typography } from '@material-ui/core/';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
minWidth: 275,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
label: string,
|
||||
statistic: string,
|
||||
loading?: boolean,
|
||||
}
|
||||
|
||||
export default function StatCard(props: IProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardContent>
|
||||
{props.loading ?
|
||||
<div>
|
||||
<Skeleton width={180} height={25} animation="wave" />
|
||||
<Skeleton width={180} height={35} animation="wave" />
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
<Typography className={classes.title} color="textSecondary" gutterBottom>
|
||||
{props.label}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="h2">
|
||||
{props.statistic}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { makeStyles, } from '@material-ui/core/styles';
|
||||
import { Card, CardContent, Typography } from '@material-ui/core/';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
flexGrow: 1,
|
||||
marginTop: '20px'
|
||||
},
|
||||
title: {
|
||||
textAlign:'center',
|
||||
fontSize: 26,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default function TroubleshootConnectionCard() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Card className={classes.root}>
|
||||
<CardContent>
|
||||
<Typography className={classes.title} gutterBottom>
|
||||
Looks like your node is not connected
|
||||
</Typography>
|
||||
<div style={{marginBottom:'20px', textAlign:'center'}}>
|
||||
<strong><Link to='/' >Click to run status checks</Link> on your nodes connection or check out the <a href={process.env.REACT_APP_BEE_DOCS_HOST} target='_blank' >Swarm Bee Docs</a></strong>
|
||||
</div>
|
||||
|
||||
<div style={{marginBottom:'20px', textAlign:'center'}}>
|
||||
<p style={{marginTop:'50px'}}>Still not working? Drop us a message on the Ethereum Swarm <a href={process.env.REACT_APP_BEE_DISCORD_HOST} target='_blank' >Discord</a></p>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Input from '@material-ui/core/Input';
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
import { Snackbar } from '@material-ui/core';
|
||||
|
||||
import { beeDebugApi } from '../services/bee';
|
||||
|
||||
export default function WithdrawlModal() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [amount, setAmount] = React.useState(BigInt(0));
|
||||
const [showToast, setToastVisibility] = React.useState(false);
|
||||
const [toastContent, setToastContent] = React.useState('');
|
||||
|
||||
const handleClickOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleWithdraw = () => {
|
||||
if (amount > 0) {
|
||||
beeDebugApi.chequebook.withdraw(amount)
|
||||
.then(res => {
|
||||
setOpen(false);
|
||||
handleToast(`Successful withdrawl. Transaction ${res.data.transactionHash}`)
|
||||
})
|
||||
.catch(error => {
|
||||
handleToast('Error with withdrawl')
|
||||
})
|
||||
} else {
|
||||
handleToast('Must be amount of greater than 0')
|
||||
}
|
||||
};
|
||||
|
||||
const handleToast = (text: string) => {
|
||||
setToastContent(text)
|
||||
setToastVisibility(true);
|
||||
setTimeout(
|
||||
() => setToastVisibility(false),
|
||||
7000
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
|
||||
Withdraw
|
||||
</Button>
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
|
||||
open={showToast}
|
||||
message={toastContent}
|
||||
/>
|
||||
<Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Withdraw Funds</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Specify the amount you would like to withdraw from your node.
|
||||
</DialogContentText>
|
||||
<Input
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
type="number"
|
||||
placeholder='Amount'
|
||||
fullWidth
|
||||
onChange={(e) => setAmount(BigInt(e.target.value))}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleWithdraw} color="primary">
|
||||
Withdraw
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user