Skip to content

Commit

Permalink
Add multiple account balances to dashboard (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
george-dorin authored Jul 26, 2023
1 parent a03e68e commit 9f9c8db
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 79 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-olives-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@smartcontractkit/operator-ui': minor
---

Change the Account Balance section to accommodate multiple accounts on different chains.
2 changes: 1 addition & 1 deletion src/pages/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ interface Props extends OwnProps, StateProps {}
export const Notifications: React.FC<Props> = ({ errors, successes }) => {
return (
<div>
{errors.length > 0 && <Error notifications={errors} />}
{errors?.length > 0 && <Error notifications={errors} />}
{successes.length > 0 && <Success notifications={successes} />}
</div>
)
Expand Down
6 changes: 3 additions & 3 deletions src/reducers/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ const reducer: Reducer<State, Actions> = (state = INITIAL_STATE, action) => {
}
case NotifyActionType.NOTIFY_ERROR: {
const errors = action.error.errors
const notifications = errors.map((e) =>
buildJsonApiErrorNotification(action, e),
)
const notifications = errors?.map(function (e) {
return buildJsonApiErrorNotification(action, e)
})

return {
...state,
Expand Down
4 changes: 2 additions & 2 deletions src/screens/Dashboard/AccountBalance.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function fetchAccountBalancesQuery(
}

describe('Activity', () => {
it('renders the first account balance', async () => {
it('renders the first and second account balance', async () => {
const payload = buildETHKeys()
const mocks: MockedResponse[] = [fetchAccountBalancesQuery(payload)]

Expand All @@ -45,7 +45,7 @@ describe('Activity', () => {
await waitForLoading()

expect(await findByText(payload[0].address)).toBeInTheDocument()
expect(queryByText(payload[1].address)).toBeNull()
expect(queryByText(payload[1].address)).toBeInTheDocument()
})

it('renders GQL query errors', async () => {
Expand Down
7 changes: 0 additions & 7 deletions src/screens/Dashboard/AccountBalanceCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ describe('AccountBalanceCard', () => {
queryByText(fromJuels(ethKey.linkBalance as string)),
).toBeInTheDocument()
expect(queryByText(ethKey.ethBalance as string)).toBeInTheDocument()

// Does not appear if there is only one account
expect(queryByRole('link', { name: /view more accounts/i })).toBeNull()
})

it('renders the empty balances for an account', () => {
Expand Down Expand Up @@ -68,10 +65,6 @@ describe('AccountBalanceCard', () => {
},
},
})

expect(
queryByRole('link', { name: /view more accounts/i }),
).toBeInTheDocument()
})

it('renders no content', () => {
Expand Down
116 changes: 51 additions & 65 deletions src/screens/Dashboard/AccountBalanceCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@ import React from 'react'
import { gql } from '@apollo/client'

import Card from '@material-ui/core/Card'
import CardActions from '@material-ui/core/CardActions'
import CardContent from '@material-ui/core/CardContent'
import CardHeader from '@material-ui/core/CardHeader'
import Grid from '@material-ui/core/Grid'
import { DetailsCardItemValue } from 'src/components/Cards/DetailsCard'
import { ChainAccountBalanceCard } from 'screens/Dashboard/ChainAccountBalanceCard'
import { EthKey } from 'types/generated/graphql'
import CardHeader from '@material-ui/core/CardHeader'
import CircularProgress from '@material-ui/core/CircularProgress'

import { fromJuels } from 'src/utils/tokens/link'
import {
DetailsCardItemTitle,
DetailsCardItemValue,
} from 'src/components/Cards/DetailsCard'
import Link from 'src/components/Link'

export const ACCOUNT_BALANCES_PAYLOAD__RESULTS_FIELDS = gql`
fragment AccountBalancesPayload_ResultsFields on EthKey {
address
Expand All @@ -34,73 +28,65 @@ export interface Props {
errorMsg?: string
}

export type KeysByChainID = {
[chainID: string]: Array<EthKey>
}

export const AccountBalanceCard: React.FC<Props> = ({
data,
errorMsg,
loading,
}) => {
const results = data?.ethKeys.results
const ethKey = results && results.length > 0 ? results[0] : undefined
const keysByChain: KeysByChainID = {}
results?.forEach(function (key) {
if (keysByChain[key.chain.id] === undefined) {
keysByChain[key.chain.id] = []
}
keysByChain[key.chain.id].push(key as EthKey)
})

return (
<Card>
<CardHeader title="Account Balance" />

<CardContent>
<Grid container spacing={16}>
{loading && (
<Grid
item
xs={12}
style={{ display: 'flex', justifyContent: 'center' }}
>
<CircularProgress data-testid="loading-spinner" size={24} />
</Grid>
)}

{errorMsg && (
<Grid item xs={12}>
<DetailsCardItemValue value={errorMsg} />
</Grid>
)}

{ethKey && (
<>
<Grid item xs={12}>
<DetailsCardItemTitle title="Address" />
<DetailsCardItemValue value={ethKey.address} />
</Grid>

<Grid item xs={6}>
<DetailsCardItemTitle title="Native Token Balance" />
<DetailsCardItemValue value={ethKey.ethBalance || '--'} />
</Grid>

<Grid item xs={6}>
<DetailsCardItemTitle title="LINK Balance" />
<DetailsCardItemValue
value={
ethKey.linkBalance ? fromJuels(ethKey.linkBalance) : '--'
}
/>
</Grid>
</>
)}
{errorMsg && (
<>
<CardHeader title="Account Balances" />
<Grid item xs={12}>
<DetailsCardItemValue value={errorMsg} />
</Grid>
</>
)}

{!ethKey && !loading && !errorMsg && (
<Grid item xs={12}>
<DetailsCardItemValue value="No account available" />
</Grid>
)}
{loading && (
<Grid
item
xs={12}
style={{ display: 'flex', justifyContent: 'center' }}
>
<CircularProgress data-testid="loading-spinner" size={24} />
</Grid>
</CardContent>
{results && results.length > 1 && (
<CardActions style={{ marginLeft: 8 }}>
<Link href="/keys" color="primary">
View more accounts
</Link>
</CardActions>
)}

{!results?.length && !loading && !errorMsg && (
<>
<CardHeader title="Account Balances" />
<Grid item xs={12}>
<DetailsCardItemValue value="No account available" />
</Grid>
</>
)}

{results &&
Object.keys(keysByChain).map(function (chainID, index) {
return (
<ChainAccountBalanceCard
key={chainID}
keys={keysByChain[chainID]}
chainID={chainID}
hideHeaderTitle={!!index}
/>
)
})}
</Card>
)
}
69 changes: 69 additions & 0 deletions src/screens/Dashboard/ChainAccountBalanceCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react'
import { renderWithRouter, screen } from 'support/test-utils'
import { ChainAccountBalanceCard } from 'screens/Dashboard/ChainAccountBalanceCard'
import { EthKey } from 'types/generated/graphql'
import { buildETHKey } from 'support/factories/gql/fetchAccountBalances'
import { fromJuels } from 'utils/tokens/link'

const { findByText, queryByText } = screen

describe('ChainAccountBalanceCard', () => {
function renderComponent(
keys: Array<EthKey>,
chainID: string,
hideHeaderTitle: boolean,
) {
renderWithRouter(
<ChainAccountBalanceCard
chainID={chainID}
keys={keys}
hideHeaderTitle={hideHeaderTitle}
/>,
)
}

it('renders the card with one address', async () => {
const key = buildETHKey()

renderComponent([key] as Array<EthKey>, '111', false)

expect(await findByText('Account Balances')).toBeInTheDocument()
expect(await findByText('Chain ID 111')).toBeInTheDocument()
expect(await findByText('Address')).toBeInTheDocument()
expect(await findByText('Native Token Balance')).toBeInTheDocument()
expect(await findByText('LINK Balance')).toBeInTheDocument()

expect(await findByText(key.address)).toBeInTheDocument()
expect(await findByText(key.ethBalance as string)).toBeInTheDocument()
expect(
await findByText(fromJuels(key.linkBalance as string)),
).toBeInTheDocument()
})

it('renders the card with two addresses', async () => {
const keys = [
buildETHKey(),
buildETHKey({
address: '0x0000000000000000000000000000000000000002',
linkBalance: '0.123',
ethBalance: '0.321',
}),
]

renderComponent(keys as Array<EthKey>, '12345321', true)

expect(await queryByText('Account Balances')).toBeNull()
expect(await findByText('Chain ID 12345321')).toBeInTheDocument()

expect(await findByText(keys[0].address)).toBeInTheDocument()
expect(await findByText(keys[1].address)).toBeInTheDocument()
expect(await findByText(keys[0].ethBalance as string)).toBeInTheDocument()
expect(await findByText(keys[1].ethBalance as string)).toBeInTheDocument()
expect(
await findByText(fromJuels(keys[0].linkBalance as string)),
).toBeInTheDocument()
expect(
await findByText(fromJuels(keys[1].linkBalance as string)),
).toBeInTheDocument()
})
})
83 changes: 83 additions & 0 deletions src/screens/Dashboard/ChainAccountBalanceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React from 'react'
import { EthKey } from 'types/generated/graphql'
import Grid from '@material-ui/core/Grid'
import {
DetailsCardItemTitle,
DetailsCardItemValue,
} from 'components/Cards/DetailsCard'
import { fromJuels } from 'utils/tokens/link'
import CardContent from '@material-ui/core/CardContent'
import CardHeader from '@material-ui/core/CardHeader'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemText from '@material-ui/core/ListItemText'
import Divider from '@material-ui/core/Divider'

export interface Props {
keys: Array<EthKey>
chainID: string
hideHeaderTitle: boolean
}
export const ChainAccountBalanceCard: React.FC<Props> = ({
keys,
chainID,
hideHeaderTitle,
}) => {
return (
<>
<CardHeader
title={!hideHeaderTitle && 'Account Balances'}
subheader={'Chain ID ' + chainID}
/>

<CardContent>
<List dense={false} disablePadding={true}>
{keys &&
keys.map((key, i) => {
return (
<>
<ListItem
disableGutters={true}
key={['acc-balance', chainID.toString(), i.toString()].join(
'-',
)}
>
<ListItemText
primary={
<React.Fragment>
<Grid container spacing={16}>
<Grid item xs={12}>
<DetailsCardItemTitle title="Address" />
<DetailsCardItemValue value={key.address} />
</Grid>
<Grid item xs={6}>
<DetailsCardItemTitle title="Native Token Balance" />
<DetailsCardItemValue
value={key.ethBalance || '--'}
/>
</Grid>
<Grid item xs={6}>
<DetailsCardItemTitle title="LINK Balance" />
<DetailsCardItemValue
value={
key.linkBalance
? fromJuels(key.linkBalance)
: '--'
}
/>
</Grid>
</Grid>
</React.Fragment>
}
></ListItemText>
</ListItem>
{/* Don't show divider on the last element */}
{i + 1 < keys.length && <Divider />}
</>
)
})}
</List>
</CardContent>
</>
)
}
2 changes: 1 addition & 1 deletion src/screens/Dashboard/DashboardView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('DashboardView', () => {
)

expect(await findByText('Activity')).toBeInTheDocument()
expect(await findByText('Account Balance')).toBeInTheDocument()
expect(await findByText('Account Balances')).toBeInTheDocument()
expect(await findByText('Recent Jobs')).toBeInTheDocument()
})
})

0 comments on commit 9f9c8db

Please sign in to comment.