diff --git a/src/App.tsx b/src/App.tsx index 7016cf4..c6b8dce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import BaseRouter from './routes/routes' import { lightTheme, darkTheme } from './theme' import { Provider as StampsProvider } from './providers/Stamps' import { Provider as PlatformProvider } from './providers/Platform' +import { Provider as BeeProvider } from './providers/Bee' const App = (): ReactElement => { const [themeMode, toggleThemeMode] = useState('light') @@ -36,18 +37,20 @@ const App = (): ReactElement => { return (
- - - - <> - - - - - - - - + + + + + <> + + + + + + + + +
) diff --git a/src/components/AlertVersion.tsx b/src/components/AlertVersion.tsx index e6d1bed..44fea3a 100644 --- a/src/components/AlertVersion.tsx +++ b/src/components/AlertVersion.tsx @@ -1,10 +1,10 @@ -import { ReactElement, useState } from 'react' +import { ReactElement, useState, useContext } from 'react' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { Alert, AlertTitle } from '@material-ui/lab' import Collapse from '@material-ui/core/Collapse' import IconButton from '@material-ui/core/IconButton' import CloseIcon from '@material-ui/icons/Close' -import { useStatusNodeVersion } from '../hooks/status' +import { Context } from '../providers/Bee' import { SUPPORTED_BEE_VERSION_EXACT } from '@ethersphere/bee-js' const useStyles = makeStyles((theme: Theme) => @@ -18,12 +18,12 @@ const useStyles = makeStyles((theme: Theme) => export default function VersionAlert(): ReactElement | null { const classes = useStyles() - const { isLoading, userVersion } = useStatusNodeVersion() + const { isLoading, latestUserVersionExact } = useContext(Context) const [open, setOpen] = useState(true) - const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === userVersion + const isExactlySupportedBeeVersion = SUPPORTED_BEE_VERSION_EXACT === latestUserVersionExact - if (isLoading || !userVersion) return null + if (isLoading || !latestUserVersionExact) return null return ( @@ -44,9 +44,9 @@ export default function VersionAlert(): ReactElement | null { } > Warning - Your Bee node version ({userVersion}) does not exactly match the Bee version we tested the Bee - Dashboard against ({SUPPORTED_BEE_VERSION_EXACT}). Please note that some functionality may not - work properly. + Your Bee node version ({latestUserVersionExact}) does not exactly match the Bee version we tested + the Bee Dashboard against ({SUPPORTED_BEE_VERSION_EXACT}). Please note that some functionality + may not work properly. diff --git a/src/components/EthereumAddressCard.tsx b/src/components/EthereumAddressCard.tsx index f46b7a5..1759ca8 100644 --- a/src/components/EthereumAddressCard.tsx +++ b/src/components/EthereumAddressCard.tsx @@ -4,7 +4,6 @@ import { createStyles, makeStyles } 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, NodeAddresses } from '@ethersphere/bee-js' @@ -28,9 +27,7 @@ const useStyles = makeStyles(() => interface Props { nodeAddresses: NodeAddresses | null - isLoadingNodeAddresses: boolean chequebookAddress: ChequebookAddressResponse | null - isLoadingChequebookAddress: boolean } function EthereumAddressCard(props: Props): ReactElement { @@ -38,36 +35,23 @@ function EthereumAddressCard(props: Props): ReactElement { return ( - {props.isLoadingNodeAddresses ? ( -
- - -
- ) : ( -
- - - Ethereum Address - - - -
- )} - {props.isLoadingChequebookAddress ? ( -
- - -
- ) : ( -
- - - Chequebook Contract Address - - - -
- )} +
+ + + Ethereum Address + + + +
+ +
+ + + Chequebook Contract Address + + + +
) } diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 3e4efa4..c7aa2f3 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -9,6 +9,8 @@ import { Activity, FileText, DollarSign, Share2, Settings, Layers } from 'react- import SwarmLogoOrange from '../assets/swarm-logo-orange.svg' import { Health } from '@ethersphere/bee-js' +import LastUpdate from './LastUpdate' + const drawerWidth = 240 const navBarItems = [ @@ -85,6 +87,7 @@ interface Props extends RouteComponentProps { themeMode: string health: boolean nodeHealth: Health | null + lastUpdate: number | null } export default function SideBar(props: Props): ReactElement { @@ -167,6 +170,9 @@ export default function SideBar(props: Props): ReactElement { Debug API +
+ +
diff --git a/src/components/TopologyStats.tsx b/src/components/TopologyStats.tsx index 6eeceb1..609499f 100644 --- a/src/components/TopologyStats.tsx +++ b/src/components/TopologyStats.tsx @@ -5,9 +5,7 @@ import { pickThreshold, ThresholdValues } from '../utils/threshold' import StatCard from './StatCard' interface RootProps { - isLoading: boolean topology: Topology | null - error: Error | null // FIXME: should display error } interface Props extends RootProps { @@ -29,26 +27,25 @@ const TopologyStats = (props: RootProps): ReactElement => { ) } -const Indicator = ({ isLoading, thresholds }: Props): ReactElement => { +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 (
- +
) } -const Metrics = ({ isLoading, topology, thresholds }: Props): ReactElement => ( +const Metrics = ({ topology, thresholds }: Props): ReactElement => ( @@ -56,17 +53,11 @@ const Metrics = ({ isLoading, topology, thresholds }: Props): ReactElement => ( - + diff --git a/src/hooks/apiHooks.tsx b/src/hooks/apiHooks.tsx index 566cc6f..2ac0741 100644 --- a/src/hooks/apiHooks.tsx +++ b/src/hooks/apiHooks.tsx @@ -1,229 +1,9 @@ import { useState, useEffect } from 'react' -import { - NodeAddresses, - ChequebookAddressResponse, - LastChequesResponse, - Health, - Peer, - Topology, - LastChequesForPeerResponse, -} from '@ethersphere/bee-js' - -import { beeDebugApi, beeApi } from '../services/bee' +import { beeDebugApi } from '../services/bee' import axios from 'axios' import { Token } from '../models/Token' -export interface HealthHook { - health: boolean - isLoadingHealth: boolean - error: Error | null -} -export const useApiHealth = (): HealthHook => { - const [health, setHealth] = useState(false) - const [isLoadingHealth, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeApi.status - .health() - .then(res => { - setHealth(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { health, isLoadingHealth, error } -} - -export interface DebugHealthHook { - nodeHealth: Health | null - isLoadingNodeHealth: boolean - error: Error | null -} - -export const useDebugApiHealth = (): DebugHealthHook => { - const [nodeHealth, setNodeHealth] = useState(null) - const [isLoadingNodeHealth, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.status - .nodeHealth() - .then(res => { - setNodeHealth(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { nodeHealth, isLoadingNodeHealth, error } -} - -export interface NodeAddressesHook { - nodeAddresses: NodeAddresses | null - isLoadingNodeAddresses: boolean - error: Error | null -} - -export const useApiNodeAddresses = (): NodeAddressesHook => { - const [nodeAddresses, setNodeAddresses] = useState(null) - const [isLoadingNodeAddresses, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.connectivity - .addresses() - .then(res => { - setNodeAddresses(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { nodeAddresses, isLoadingNodeAddresses, error } -} - -export interface NodeTopologyHook { - topology: Topology | null - isLoading: boolean - error: Error | null -} - -export const useApiNodeTopology = (): NodeTopologyHook => { - const [topology, setNodeTopology] = useState(null) - const [isLoading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.connectivity - .topology() - .then(res => { - setNodeTopology(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { topology, isLoading, error } -} -export interface ChequebookAddressHook { - chequebookAddress: ChequebookAddressResponse | null - isLoadingChequebookAddress: boolean - error: Error | null -} - -export const useApiChequebookAddress = (): ChequebookAddressHook => { - const [chequebookAddress, setChequebookAddress] = useState(null) - const [isLoadingChequebookAddress, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.chequebook - .address() - .then(res => { - setChequebookAddress(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { chequebookAddress, isLoadingChequebookAddress, error } -} - -export interface NodePeersHook { - peers: Peer[] | null - isLoading: boolean - error: Error | null -} - -export const useApiNodePeers = (): NodePeersHook => { - const [peers, setPeers] = useState(null) - const [isLoading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.connectivity - .listPeers() - .then(res => { - setPeers(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { peers, isLoading, error } -} - -export interface ChequebookBalance { - totalBalance: Token - availableBalance: Token -} - -export interface ChequebookBalanceHook { - chequebookBalance: ChequebookBalance | null - isLoadingChequebookBalance: boolean - error: Error | null -} - -export const useApiChequebookBalance = (): ChequebookBalanceHook => { - const [chequebookBalance, setChequebookBalance] = useState(null) - const [isLoadingChequebookBalance, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.chequebook - .balance() - .then(({ totalBalance, availableBalance }) => { - const balance = { - totalBalance: new Token(totalBalance), - availableBalance: new Token(availableBalance), - } - setChequebookBalance(balance) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { chequebookBalance, isLoadingChequebookBalance, error } -} - export interface Balance { peer: string balance: Token @@ -260,64 +40,6 @@ export const useApiPeerBalances = (): PeerBalanceHook => { return { peerBalances, isLoadingPeerBalances, error } } -export interface PeerChequesHook { - peerCheques: LastChequesResponse | null - isLoadingPeerCheques: boolean - error: Error | null -} - -export const useApiPeerCheques = (): PeerChequesHook => { - const [peerCheques, setPeerCheques] = useState(null) - const [isLoadingPeerCheques, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.chequebook - .getLastCheques() - .then(res => { - setPeerCheques(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, []) - - return { peerCheques, isLoadingPeerCheques, error } -} - -export interface PeerLastChequesHook { - peerCheque: LastChequesForPeerResponse | null - isLoadingPeerCheque: boolean - error: Error | null -} - -export const useApiPeerLastCheque = (peerId: string): PeerLastChequesHook => { - const [peerCheque, setPeerCheque] = useState(null) - const [isLoadingPeerCheque, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.chequebook - .getPeerLastCheques(peerId) - .then(res => { - setPeerCheque(res) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, [peerId]) - - return { peerCheque, isLoadingPeerCheque, error } -} - export interface Settlement { peer: string received: Token @@ -368,40 +90,6 @@ export const useApiSettlements = (): SettlementsHook => { return { settlements, isLoadingSettlements, error } } -export interface LastCashout { - peer: string - uncashedAmount: Token -} - -export interface PeerLastCashoutHook { - peerCashout: LastCashout | null - isLoadingPeerCashout: boolean - error: Error | null -} - -export const useApiPeerLastCashout = (peerId: string): PeerLastCashoutHook => { - const [peerCashout, setPeerCashout] = useState(null) - const [isLoadingPeerCashout, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - setLoading(true) - beeDebugApi.chequebook - .getPeerLastCashout(peerId) - .then(({ peer, uncashedAmount }) => { - setPeerCashout({ peer, uncashedAmount: new Token(uncashedAmount.toString()) }) - }) - .catch(error => { - setError(error) - }) - .finally(() => { - setLoading(false) - }) - }, [peerId]) - - return { peerCashout, isLoadingPeerCashout, error } -} - export interface LatestBeeReleaseHook { latestBeeRelease: LatestBeeRelease | null isLoadingLatestBeeRelease: boolean diff --git a/src/hooks/status.ts b/src/hooks/status.ts deleted file mode 100644 index fa0588e..0000000 --- a/src/hooks/status.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ChequebookAddressResponse } from '@ethersphere/bee-js' -import { - ChequebookBalance, - useApiChequebookAddress, - useApiChequebookBalance, - useApiHealth, - useApiNodeAddresses, - useApiNodeTopology, - useDebugApiHealth, - useLatestBeeRelease, -} from './apiHooks' -import semver from 'semver' -import { engines } from '../../package.json' - -export interface StatusChequebookHook extends StatusHookCommon { - chequebookBalance: ChequebookBalance | null - chequebookAddress: ChequebookAddressResponse | null -} - -export const useStatusNodeVersion = (): StatusNodeVersionHook => { - const { latestBeeRelease, isLoadingLatestBeeRelease } = useLatestBeeRelease() - const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() - - const latestVersion = semver.coerce(latestBeeRelease?.name)?.version - const latestUserVersion = semver.coerce(nodeHealth?.version)?.version - - const isLatestBeeVersion = Boolean( - latestVersion && - latestUserVersion && - semver.satisfies(latestVersion, latestUserVersion, { - includePrerelease: true, - }), - ) - - return { - isLoading: isLoadingNodeHealth || isLoadingLatestBeeRelease, - isOk: Boolean( - nodeHealth && - semver.satisfies(nodeHealth.version, engines.bee, { - includePrerelease: true, - }), - ), - userVersion: nodeHealth?.version, - latestVersion, - latestUrl: latestBeeRelease?.html_url || 'https://github.com/ethersphere/bee/releases/latest', - isLatestBeeVersion, - } -} - -export const useStatusEthereumConnection = (): StatusEthereumConnectionHook => { - const { isLoadingNodeAddresses, nodeAddresses } = useApiNodeAddresses() - - return { - isLoading: isLoadingNodeAddresses, - isOk: Boolean(nodeAddresses?.ethereum), - nodeAddresses, - } -} - -export const useStatusDebugConnection = (): StatusHookCommon => { - const { isLoadingNodeHealth, nodeHealth } = useDebugApiHealth() - - return { - isLoading: isLoadingNodeHealth, - isOk: Boolean(nodeHealth?.status === 'ok'), - } -} - -export const useStatusConnection = (): StatusHookCommon => { - const { isLoadingHealth, health } = useApiHealth() - - return { - isLoading: isLoadingHealth, - isOk: health, - } -} - -export const useStatusTopology = (): StatusTopologyHook => { - const { topology, isLoading } = useApiNodeTopology() - - return { - isLoading, - isOk: Boolean(topology?.connected && topology?.connected > 0), - topology, - } -} - -export const useStatusChequebook = (): StatusChequebookHook => { - const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() - const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance() - - return { - isLoading: isLoadingChequebookAddress || isLoadingChequebookBalance, - isOk: - Boolean(chequebookAddress?.chequebookAddress) && - chequebookBalance !== null && - chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0), - chequebookBalance, - chequebookAddress, - } -} diff --git a/src/layout/Dashboard.tsx b/src/layout/Dashboard.tsx index 62d2041..7f2e62c 100644 --- a/src/layout/Dashboard.tsx +++ b/src/layout/Dashboard.tsx @@ -1,13 +1,15 @@ -import { useState, useEffect, ReactElement } from 'react' +import { useState, useEffect, useContext, ReactElement } from 'react' import ErrorBoundary from '../components/ErrorBoundary' import AlertVersion from '../components/AlertVersion' +import { Container, CircularProgress } from '@material-ui/core' import { createStyles, Theme, makeStyles } from '@material-ui/core/styles' import SideBar from '../components/SideBar' import NavBar from '../components/NavBar' -import { useApiHealth, useDebugApiHealth } from '../hooks/apiHooks' +import { Context } from '../providers/Bee' + import { RouteComponentProps } from 'react-router' const useStyles = makeStyles((theme: Theme) => @@ -32,8 +34,7 @@ const Dashboard = (props: Props): ReactElement => { const [themeMode, toggleThemeMode] = useState('light') // FIXME: handle errrors and loading - const { health } = useApiHealth() - const { nodeHealth } = useDebugApiHealth() + const { isLoading, lastUpdate, apiHealth, debugApiHealth } = useContext(Context) useEffect(() => { const theme = localStorage.getItem('theme') @@ -56,12 +57,24 @@ const Dashboard = (props: Props): ReactElement => { return (
- +
- {props.children} + {isLoading ? ( + + + + ) : ( + props.children + )}
diff --git a/src/pages/accounting/AccountCard.tsx b/src/pages/accounting/AccountCard.tsx index 6f71a0a..7762eaf 100644 --- a/src/pages/accounting/AccountCard.tsx +++ b/src/pages/accounting/AccountCard.tsx @@ -2,7 +2,6 @@ import { ReactElement } from 'react' import { createStyles, makeStyles } from '@material-ui/core/styles' import { Card, CardContent, Typography, Theme } from '@material-ui/core/' -import { Skeleton } from '@material-ui/lab' import WithdrawModal from '../../containers/WithdrawModal' import DepositModal from '../../containers/DepositModal' @@ -45,12 +44,11 @@ interface ChequebookBalance { interface Props { chequebookAddress: ChequebookAddressResponse | null chequebookBalance: ChequebookBalance | null - totalsent: Token - totalreceived: Token - isLoading: boolean + totalsent?: Token + totalreceived?: Token } -function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: Props): ReactElement { +function AccountCard({ totalreceived, totalsent, chequebookBalance }: Props): ReactElement { const classes = useStyles() return ( @@ -66,37 +64,28 @@ function AccountCard({ totalreceived, totalsent, chequebookBalance, isLoading }: - {!isLoading && ( - -
- - Total Balance - - {chequebookBalance?.totalBalance.toFixedDecimal()} BZZ -
-
- - Available Uncommitted Balance - - {chequebookBalance?.availableBalance.toFixedDecimal()} BZZ -
-
- - Total Sent / Received - - - {totalsent.toFixedDecimal()} / {totalreceived.toFixedDecimal()} BZZ - -
-
- )} - {isLoading && ( -
- - - + +
+ + Total Balance + + {chequebookBalance?.totalBalance.toFixedDecimal()} BZZ
- )} +
+ + Available Uncommitted Balance + + {chequebookBalance?.availableBalance.toFixedDecimal()} BZZ +
+
+ + Total Sent / Received + + + {totalsent?.toFixedDecimal()} / {totalreceived?.toFixedDecimal()} BZZ + +
+
) diff --git a/src/pages/accounting/index.tsx b/src/pages/accounting/index.tsx index f54dc17..36be448 100644 --- a/src/pages/accounting/index.tsx +++ b/src/pages/accounting/index.tsx @@ -1,19 +1,12 @@ -import type { ReactElement } from 'react' +import { ReactElement, useContext } from 'react' import { Theme, createStyles, makeStyles } from '@material-ui/core/styles' -import { Container, CircularProgress } from '@material-ui/core' +import { Container } from '@material-ui/core' import AccountCard from '../accounting/AccountCard' import BalancesTable from './BalancesTable' import EthereumAddressCard from '../../components/EthereumAddressCard' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' - -import { - useApiNodeAddresses, - useApiChequebookAddress, - useApiChequebookBalance, - useApiHealth, - useDebugApiHealth, -} from '../../hooks/apiHooks' +import { Context } from '../../providers/Bee' import { useAccounting } from '../../hooks/accounting' const useStyles = makeStyles((theme: Theme) => @@ -29,38 +22,21 @@ const useStyles = makeStyles((theme: Theme) => export default function Accounting(): ReactElement { const classes = useStyles() - const { chequebookAddress, isLoadingChequebookAddress } = useApiChequebookAddress() - const { chequebookBalance, isLoadingChequebookBalance } = useApiChequebookBalance() - const { nodeAddresses, isLoadingNodeAddresses } = useApiNodeAddresses() - const { health, isLoadingHealth } = useApiHealth() - const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() - const { isLoading, totalsent, totalreceived, accounting, isLoadingUncashed, error } = useAccounting() + const { status, nodeAddresses, chequebookAddress, chequebookBalance, settlements } = useContext(Context) - if (isLoadingHealth || isLoadingNodeHealth) { - return ( - - - - ) - } + const { accounting, isLoadingUncashed, error } = useAccounting() - if (nodeHealth?.status !== 'ok' || !health) return + if (!status.all) return return (
- + {error && ( Error loading accounting details: {error.message} diff --git a/src/pages/files/index.tsx b/src/pages/files/index.tsx index 1f16a10..ef6a095 100644 --- a/src/pages/files/index.tsx +++ b/src/pages/files/index.tsx @@ -1,26 +1,17 @@ -import { ReactElement } from 'react' +import { ReactElement, useContext } from 'react' -import { Container, CircularProgress } from '@material-ui/core' +import { Container } from '@material-ui/core' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' -import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' +import { Context } from '../../providers/Bee' import Download from './Download' import Upload from './Upload' import TabsContainer from '../../components/TabsContainer' export default function Files(): ReactElement { - const { health, isLoadingHealth } = useApiHealth() - const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() + const { status } = useContext(Context) - if (isLoadingHealth || isLoadingNodeHealth) { - return ( - - - - ) - } - - if (!health || nodeHealth?.status !== 'ok') return + if (!status.all) return return ( diff --git a/src/pages/peers/PeerTable.tsx b/src/pages/peers/PeerTable.tsx index 8cb5e75..585155e 100644 --- a/src/pages/peers/PeerTable.tsx +++ b/src/pages/peers/PeerTable.tsx @@ -10,7 +10,6 @@ import { Button, Paper, Tooltip, - Container, CircularProgress, } from '@material-ui/core' import { Autorenew } from '@material-ui/icons' @@ -26,8 +25,6 @@ const useStyles = makeStyles({ interface Props { peers: Peer[] | null - isLoading: boolean - error: Error | null } function PeerTable(props: Props): ReactElement { @@ -47,22 +44,6 @@ function PeerTable(props: Props): ReactElement { }) } - if (props.isLoading) { - return ( - - - - ) - } - - if (props.error || props.peers === null) { - return ( - -

Failed to load peers

-
- ) - } - return (
@@ -75,7 +56,7 @@ function PeerTable(props: Props): ReactElement { - {props.peers.map((peer: Peer, idx: number) => ( + {props.peers?.map((peer: Peer, idx: number) => ( {idx + 1} diff --git a/src/pages/peers/index.tsx b/src/pages/peers/index.tsx index 000646c..c1e1d51 100644 --- a/src/pages/peers/index.tsx +++ b/src/pages/peers/index.tsx @@ -1,32 +1,21 @@ -import { Container, CircularProgress } from '@material-ui/core/' import PeerTable from './PeerTable' import TroubleshootConnectionCard from '../../components/TroubleshootConnectionCard' -import { useApiNodeTopology, useApiNodePeers, useDebugApiHealth } from '../../hooks/apiHooks' +import { Context } from '../../providers/Bee' import TopologyStats from '../../components/TopologyStats' -import { ReactElement } from 'react' +import { ReactElement, useContext } from 'react' export default function Peers(): ReactElement { - const topology = useApiNodeTopology() - const debugHealth = useDebugApiHealth() - const peers = useApiNodePeers() + const { topology, peers, status } = useContext(Context) - if (debugHealth.isLoadingNodeHealth) { - return ( - - - - ) - } - - if (debugHealth.error) { + if (!status.all) { return } return ( <> - - + + ) } diff --git a/src/pages/stamps/index.tsx b/src/pages/stamps/index.tsx index 8f8fe0d..567d487 100644 --- a/src/pages/stamps/index.tsx +++ b/src/pages/stamps/index.tsx @@ -7,8 +7,8 @@ import TroubleshootConnectionCard from '../../components/TroubleshootConnectionC import CreatePostageStampModal from './CreatePostageStampModal' import LastUpdate from '../../components/LastUpdate' -import { useApiHealth, useDebugApiHealth } from '../../hooks/apiHooks' import { Context } from '../../providers/Stamps' +import { Context as BeeContext } from '../../providers/Bee' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -32,8 +32,7 @@ const useStyles = makeStyles((theme: Theme) => export default function Accounting(): ReactElement { const classes = useStyles() - const { health, isLoadingHealth } = useApiHealth() - const { nodeHealth, isLoadingNodeHealth } = useDebugApiHealth() + const beeContext = useContext(BeeContext) const { stamps, isLoading, error, lastUpdate, start, stop } = useContext(Context) useEffect(() => { start() @@ -41,7 +40,7 @@ export default function Accounting(): ReactElement { return () => stop() }, []) - if (isLoadingHealth || isLoadingNodeHealth) { + if (beeContext.isLoading) { return ( @@ -49,7 +48,7 @@ export default function Accounting(): ReactElement { ) } - if (nodeHealth?.status !== 'ok' || !health) return + if (!beeContext.status.all) return return (
diff --git a/src/pages/status/NodeSetupWorkflow.tsx b/src/pages/status/NodeSetupWorkflow.tsx index c8420b9..5673969 100644 --- a/src/pages/status/NodeSetupWorkflow.tsx +++ b/src/pages/status/NodeSetupWorkflow.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect, useState } from 'react' +import { ReactElement, useEffect, useState, useContext } from 'react' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import { Typography, Paper, Button, Step, StepLabel, StepContent, Stepper, StepButton } from '@material-ui/core/' import { CheckCircle, Error, Sync, ExpandLessSharp, ExpandMoreSharp, Autorenew } from '@material-ui/icons/' @@ -9,7 +9,7 @@ import VersionCheck from './SetupSteps/VersionCheck' import EthereumConnectionCheck from './SetupSteps/EthereumConnectionCheck' import ChequebookDeployFund from './SetupSteps/ChequebookDeployFund' import PeerConnection from './SetupSteps/PeerConnection' -import { StatusChequebookHook } from '../../hooks/status' +import { Context } from '../../providers/Bee' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -30,66 +30,65 @@ const useStyles = makeStyles((theme: Theme) => interface Step { label: string isOk: boolean - isLoading: boolean component: ReactElement } -interface Props { - nodeVersion: StatusNodeVersionHook - ethereumConnection: StatusEthereumConnectionHook - debugApiConnection: StatusHookCommon - apiConnection: StatusHookCommon - topology: StatusTopologyHook - chequebook: StatusChequebookHook -} - -export default function NodeSetupWorkflow({ - nodeVersion, - ethereumConnection, - debugApiConnection, - apiConnection, - topology, - chequebook, -}: Props): ReactElement { +export default function NodeSetupWorkflow(): ReactElement { const classes = useStyles() const [activeStep, setActiveStep] = useState(-1) + const { + status, + isLoading, + latestUserVersion, + latestPublishedVersion, + isLatestBeeVersion, + latestBeeVersionUrl, + topology, + nodeAddresses, + chequebookAddress, + } = useContext(Context) + const steps: Step[] = [ { label: 'Connected to Node DebugAPI', - isOk: debugApiConnection.isOk, - isLoading: debugApiConnection.isLoading, - component: , + isOk: status.debugApiConnection, + component: , }, { label: 'Running latest Bee version', - isOk: nodeVersion.isOk, - isLoading: nodeVersion.isLoading, - component: , + isOk: status.version, + component: ( + + ), }, { - label: 'Connected to Ethereum Blockchain', - isOk: ethereumConnection.isOk, - isLoading: ethereumConnection.isLoading, - component: , + label: 'Connected to xDai Blockchain', + isOk: status.blockchainConnection, + component: , }, { label: 'Deployed and Funded Chequebook', - isOk: chequebook.isOk, - isLoading: chequebook.isLoading, - component: , + isOk: status.chequebook, + component: ( + + ), }, { label: 'Connected to Node API', - isOk: apiConnection.isOk, - isLoading: apiConnection.isLoading, - component: , + isOk: status.apiConnection, + component: , }, { label: 'Connected to Peers', - isOk: topology.isOk, - isLoading: topology.isLoading, - component: , + isOk: status.topology, + component: , }, ] @@ -98,7 +97,7 @@ export default function NodeSetupWorkflow({ if (activeStep >= 0 && activeStep < steps.length) return // If any step is not fully loaded yet return - if (!steps.every(step => !step.isLoading)) return + if (!isLoading) return // Select first step that is not OK // This is deliberately a for loop (and not forEach) so that we can terminate the useEffect from within the cycle @@ -131,10 +130,9 @@ export default function NodeSetupWorkflow({ - {steps.map(({ label, isOk, component, isLoading }, index) => ( + {steps.map(({ label, isOk, component }, index) => ( setActiveStep(index === activeStep ? steps.length : index)} StepIconComponent={() => { if (isLoading) return diff --git a/src/pages/status/SetupSteps/ChequebookDeployFund.tsx b/src/pages/status/SetupSteps/ChequebookDeployFund.tsx index 456fa1e..5197f4f 100644 --- a/src/pages/status/SetupSteps/ChequebookDeployFund.tsx +++ b/src/pages/status/SetupSteps/ChequebookDeployFund.tsx @@ -2,22 +2,17 @@ import { Typography } from '@material-ui/core/' import EthereumAddress from '../../../components/EthereumAddress' import DepositModal from '../../../containers/DepositModal' import type { ReactElement } from 'react' -import type { StatusChequebookHook } from '../../../hooks/status' -interface Props extends StatusChequebookHook { - ethereumAddress?: string +interface Props extends StatusHookCommon { + chequebookAddress?: string } -const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance }: Props): ReactElement | null => { - if (isLoading) return null - +const ChequebookDeployFund = ({ chequebookAddress, isOk }: Props): ReactElement | null => { return (
-

- {chequebookAddress?.chequebookAddress && } -

+

{chequebookAddress && }

- {!(chequebookAddress?.chequebookAddress && chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0)) && ( + {!isOk && (
Your chequebook is either not deployed or funded. To run the node you will need xDAI and xBZZ on the xDai @@ -33,7 +28,7 @@ const ChequebookDeployFund = ({ isLoading, chequebookAddress, chequebookBalance Chequebook Address - +
) } diff --git a/src/pages/status/SetupSteps/DebugConnectionCheck.tsx b/src/pages/status/SetupSteps/DebugConnectionCheck.tsx index d61d646..94a512b 100644 --- a/src/pages/status/SetupSteps/DebugConnectionCheck.tsx +++ b/src/pages/status/SetupSteps/DebugConnectionCheck.tsx @@ -9,9 +9,7 @@ import { debugApiHost } from '../../../constants' type Props = StatusHookCommon -export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { - if (isLoading) return null - +export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null { const changeDebugApiUrl = (
diff --git a/src/pages/status/SetupSteps/EthereumConnectionCheck.tsx b/src/pages/status/SetupSteps/EthereumConnectionCheck.tsx index dce48f1..2c72ca2 100644 --- a/src/pages/status/SetupSteps/EthereumConnectionCheck.tsx +++ b/src/pages/status/SetupSteps/EthereumConnectionCheck.tsx @@ -4,9 +4,7 @@ import EthereumAddress from '../../../components/EthereumAddress' type Props = StatusEthereumConnectionHook -export default function EthereumConnectionCheck({ isLoading, isOk, nodeAddresses }: Props): ReactElement | null { - if (isLoading) return null - +export default function EthereumConnectionCheck({ isOk, nodeAddresses }: Props): ReactElement | null { if (isOk) { return (
diff --git a/src/pages/status/SetupSteps/NodeConnectionCheck.tsx b/src/pages/status/SetupSteps/NodeConnectionCheck.tsx index f571451..6078937 100644 --- a/src/pages/status/SetupSteps/NodeConnectionCheck.tsx +++ b/src/pages/status/SetupSteps/NodeConnectionCheck.tsx @@ -8,9 +8,7 @@ import { apiHost } from '../../../constants' type Props = StatusHookCommon -export default function NodeConnectionCheck({ isLoading, isOk }: Props): ReactElement | null { - if (isLoading) return null - +export default function NodeConnectionCheck({ isOk }: Props): ReactElement | null { return (
diff --git a/src/pages/status/SetupSteps/PeerConnection.tsx b/src/pages/status/SetupSteps/PeerConnection.tsx index bb4cd6b..72c21a2 100644 --- a/src/pages/status/SetupSteps/PeerConnection.tsx +++ b/src/pages/status/SetupSteps/PeerConnection.tsx @@ -3,9 +3,7 @@ import { Typography } from '@material-ui/core/' type Props = StatusTopologyHook -export default function PeerConnection({ isLoading, isOk, topology }: Props): ReactElement | null { - if (isLoading) return null - +export default function PeerConnection({ isOk, topology }: Props): ReactElement | null { const peers = (
diff --git a/src/pages/status/SetupSteps/VersionCheck.tsx b/src/pages/status/SetupSteps/VersionCheck.tsx index 89ac0fd..01bbd62 100644 --- a/src/pages/status/SetupSteps/VersionCheck.tsx +++ b/src/pages/status/SetupSteps/VersionCheck.tsx @@ -4,15 +4,7 @@ import CodeBlockTabs from '../../../components/CodeBlockTabs' type Props = StatusNodeVersionHook -export default function VersionCheck({ - isLoading, - isOk, - userVersion, - latestVersion, - latestUrl, -}: Props): ReactElement | null { - if (isLoading) return null - +export default function VersionCheck({ isOk, userVersion, latestVersion, latestUrl }: Props): ReactElement | null { const version = (
diff --git a/src/pages/status/index.tsx b/src/pages/status/index.tsx index 8a21eaf..5d4ed16 100644 --- a/src/pages/status/index.tsx +++ b/src/pages/status/index.tsx @@ -1,18 +1,10 @@ -import { ReactElement } from 'react' -import { Container, CircularProgress } from '@material-ui/core' +import { ReactElement, useContext } from 'react' import { makeStyles, Theme, createStyles } from '@material-ui/core/styles' import NodeSetupWorkflow from './NodeSetupWorkflow' import StatusCard from './StatusCard' import EthereumAddressCard from '../../components/EthereumAddressCard' -import { - useStatusEthereumConnection, - useStatusNodeVersion, - useStatusDebugConnection, - useStatusConnection, - useStatusTopology, - useStatusChequebook, -} from '../../hooks/status' +import { Context } from '../../providers/Bee' const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -27,50 +19,30 @@ const useStyles = makeStyles((theme: Theme) => export default function Status(): ReactElement { const classes = useStyles() - const nodeVersion = useStatusNodeVersion() - const ethereumConnection = useStatusEthereumConnection() - const debugApiConnection = useStatusDebugConnection() - const apiConnection = useStatusConnection() - const topology = useStatusTopology() - const chequebook = useStatusChequebook() - - const checks = [nodeVersion, ethereumConnection, debugApiConnection, apiConnection, topology, chequebook] - - // If any check data are still loading - if (!checks.every(c => !c.isLoading)) { - return ( - - - - ) - } + const { + status, + latestUserVersion, + isLatestBeeVersion, + latestBeeVersionUrl, + topology, + nodeAddresses, + chequebookAddress, + } = useContext(Context) return (
c.isOk)} - nodeTopology={topology.topology} - latestUrl={nodeVersion.latestUrl} - nodeAddresses={ethereumConnection.nodeAddresses} + userBeeVersion={latestUserVersion} + isLatestBeeVersion={isLatestBeeVersion} + isOk={status.all} + nodeTopology={topology} + latestUrl={latestBeeVersionUrl} + nodeAddresses={nodeAddresses} /> - {ethereumConnection.nodeAddresses && chequebook.chequebookAddress && ( - + {nodeAddresses && chequebookAddress && ( + )} - +
) } diff --git a/src/providers/Bee.tsx b/src/providers/Bee.tsx new file mode 100644 index 0000000..89ff3a5 --- /dev/null +++ b/src/providers/Bee.tsx @@ -0,0 +1,256 @@ +import type { ChequebookBalance, Balance, Settlements } from '../types' +import { createContext, ReactChild, ReactElement, useEffect, useState } from 'react' +import { beeApi, beeDebugApi } from '../services/bee' +import { Token } from '../models/Token' +import semver from 'semver' +import { engines } from '../../package.json' + +import type { + NodeAddresses, + ChequebookAddressResponse, + LastChequesResponse, + Health, + Peer, + Topology, +} from '@ethersphere/bee-js' +import { useLatestBeeRelease } from '../hooks/apiHooks' + +interface Status { + all: boolean + version: boolean + blockchainConnection: boolean + debugApiConnection: boolean + apiConnection: boolean + topology: boolean + chequebook: boolean +} + +interface ContextInterface { + status: Status + latestPublishedVersion?: string + latestUserVersion?: string + latestUserVersionExact?: string + isLatestBeeVersion: boolean + latestBeeVersionUrl: string + error: Error | null + apiHealth: boolean + debugApiHealth: Health | null + nodeAddresses: NodeAddresses | null + topology: Topology | null + chequebookAddress: ChequebookAddressResponse | null + peers: Peer[] | null + chequebookBalance: ChequebookBalance | null + peerBalances: Balance[] | null + peerCheques: LastChequesResponse | null + settlements: Settlements | null + latestBeeRelease: LatestBeeRelease | null + isLoading: boolean + isRefreshing: boolean + lastUpdate: number | null + start: (frequency?: number) => void + stop: () => void + refresh: () => Promise +} + +const initialValues: ContextInterface = { + status: { + all: false, + version: false, + blockchainConnection: false, + debugApiConnection: false, + apiConnection: false, + topology: false, + chequebook: false, + }, + latestPublishedVersion: undefined, + latestUserVersion: undefined, + latestUserVersionExact: undefined, + isLatestBeeVersion: false, + latestBeeVersionUrl: 'https://github.com/ethersphere/bee/releases/latest', + error: null, + apiHealth: false, + debugApiHealth: null, + nodeAddresses: null, + topology: null, + chequebookAddress: null, + peers: null, + chequebookBalance: null, + peerBalances: null, + peerCheques: null, + settlements: null, + latestBeeRelease: null, + isLoading: true, + isRefreshing: false, + lastUpdate: null, + start: () => {}, // eslint-disable-line + stop: () => {}, // eslint-disable-line + refresh: () => Promise.reject(), +} + +export const Context = createContext(initialValues) +export const Consumer = Context.Consumer + +interface Props { + children: ReactChild +} + +function getStatus( + latestBeeRelease: LatestBeeRelease | null, + debugApiHealth: Health | null, + nodeAddresses: NodeAddresses | null, + apiHealth: boolean, + topology: Topology | null, + chequebookAddress: ChequebookAddressResponse | null, + chequebookBalance: ChequebookBalance | null, + error: Error | null, +): Status { + const status = { + version: Boolean( + debugApiHealth && + semver.satisfies(debugApiHealth.version, engines.bee, { + includePrerelease: true, + }), + ), + blockchainConnection: Boolean(nodeAddresses?.ethereum), + debugApiConnection: Boolean(debugApiHealth?.status === 'ok'), + apiConnection: apiHealth, + topology: Boolean(topology?.connected && topology?.connected > 0), + chequebook: + Boolean(chequebookAddress?.chequebookAddress) && + chequebookBalance !== null && + chequebookBalance?.totalBalance.toBigNumber.isGreaterThan(0), + } + + return { ...status, all: !error && Object.values(status).every(v => v) } +} + +export function Provider({ children }: Props): ReactElement { + const [apiHealth, setApiHealth] = useState(false) + const [debugApiHealth, setDebugApiHealth] = useState(null) + const [nodeAddresses, setNodeAddresses] = useState(null) + const [topology, setNodeTopology] = useState(null) + const [chequebookAddress, setChequebookAddress] = useState(null) + const [peers, setPeers] = useState(null) + const [chequebookBalance, setChequebookBalance] = useState(null) + const [peerBalances, setPeerBalances] = useState(null) + const [peerCheques, setPeerCheques] = useState(null) + const [settlements, setSettlements] = useState(null) + const { latestBeeRelease } = useLatestBeeRelease() + + const [error, setError] = useState(initialValues.error) + const [isLoading, setIsLoading] = useState(initialValues.isLoading) + const [isRefreshing, setIsRefreshing] = useState(initialValues.isRefreshing) + const [lastUpdate, setLastUpdate] = useState(initialValues.lastUpdate) + const [frequency, setFrequency] = useState(30000) + + const latestPublishedVersion = semver.coerce(latestBeeRelease?.name)?.version + const latestUserVersion = semver.coerce(debugApiHealth?.version)?.version + const latestUserVersionExact = debugApiHealth?.version + + const refresh = async () => { + // Don't want to refresh when already refreshing + if (isRefreshing) return + + try { + setIsRefreshing(true) + + setApiHealth(await beeApi.status.health()) + setDebugApiHealth(await beeDebugApi.status.nodeHealth()) + setNodeAddresses(await beeDebugApi.connectivity.addresses()) + setNodeTopology(await beeDebugApi.connectivity.topology()) + setChequebookAddress(await beeDebugApi.chequebook.address()) + setPeers(await beeDebugApi.connectivity.listPeers()) + + const { totalBalance, availableBalance } = await beeDebugApi.chequebook.balance() + setChequebookBalance({ + totalBalance: new Token(totalBalance), + availableBalance: new Token(availableBalance), + }) + + const { balances } = await beeDebugApi.balance.balances() + setPeerBalances(balances.map(({ peer, balance }) => ({ peer, balance: new Token(balance) }))) + + setPeerCheques(await beeDebugApi.chequebook.getLastCheques()) + const { totalReceived, settlements, totalSent } = await beeDebugApi.settlements.getSettlements() + setSettlements({ + totalReceived: new Token(totalReceived), + totalSent: new Token(totalSent), + settlements: settlements.map(({ peer, received, sent }) => ({ + peer, + received: new Token(received), + sent: new Token(sent), + })), + }) + + setLastUpdate(Date.now()) + } catch (e) { + setError(e) + } finally { + setIsLoading(false) + setIsRefreshing(false) + } + } + + const start = (freq = 30000) => setFrequency(freq) + const stop = () => setFrequency(null) + + // Start the update loop + useEffect(() => { + refresh() + + // Start autorefresh only if the frequency is set + if (frequency) { + const interval = setInterval(refresh, frequency) + + return () => clearInterval(interval) + } + }, [frequency]) + + return ( + + {children} + + ) +} diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index acedcb3..246a950 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -6,7 +6,6 @@ interface LatestBeeRelease { } interface StatusHookCommon { - isLoading: boolean isOk: boolean } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..8fd9a7f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,23 @@ +import type { Token } from './models/Token' + +export interface ChequebookBalance { + totalBalance: Token + availableBalance: Token +} + +export interface Balance { + peer: string + balance: Token +} + +export interface Settlement { + peer: string + received: Token + sent: Token +} + +export interface Settlements { + totalReceived: Token + totalSent: Token + settlements: Settlement[] +}