Skip to content

Commit

Permalink
refactor: update homepage to typescript and hooks (#921)
Browse files Browse the repository at this point in the history
## High Level Overview of Change

Update homepage to typescript and hooks and break out aspects of the
homepage's scrolling ledgers into smaller components. This also adds a
new hook `useTooltip` that cleans up tooltip logic when many places are
updating tooltips.

A small change to move the transaction iteration into its own component
reduced render times of the homepage due by between 30ms to 60ms each
re-render.

### Type of Change

- [x] Refactor (non-breaking change that only restructures code)

### TypeScript/Hooks Update

- [x] Updated files to React Hooks
- [x] Updated files to TypeScript

## Future Work

Optimize the <Streams> component into a hook that causes way fewer
re-renders.
  • Loading branch information
ckniffen authored Feb 1, 2024
1 parent bd138fe commit 77c8722
Show file tree
Hide file tree
Showing 23 changed files with 698 additions and 516 deletions.
2 changes: 1 addition & 1 deletion src/containers/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
AMENDMENTS_ROUTE,
AMENDMENT_ROUTE,
} from './routes'
import Ledgers from '../Ledgers'
import { LedgersPage as Ledgers } from '../Ledgers'
import { Ledger } from '../Ledger'
import { AccountsRouter } from '../Accounts/AccountsRouter'
import { Transaction } from '../Transactions'
Expand Down
2 changes: 1 addition & 1 deletion src/containers/App/test/App.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Error } from '../../../rippled/lib/utils'

jest.mock('../../Ledgers/LedgerMetrics', () => ({
__esModule: true,
default: () => null,
LedgerMetrics: () => null,
}))

jest.mock('xrpl-client', () => ({
Expand Down
53 changes: 53 additions & 0 deletions src/containers/Ledgers/LedgerEntryHash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useTranslation } from 'react-i18next'
import SuccessIcon from '../shared/images/success.svg'
import { LedgerEntryValidator } from './LedgerEntryValidator'
import { LedgerEntryHashTrustedCount } from './LedgerEntryHashTrustedCount'
import { ValidatorResponse } from './types'

export const LedgerEntryHash = ({
hash,
unlCount,
validators,
}: {
hash: any
unlCount?: number
validators: { [pubkey: string]: ValidatorResponse }
}) => {
const { t } = useTranslation()
const shortHash = hash.hash.substr(0, 6)
const barStyle = { background: `#${shortHash}` }
const validated = hash.validated && <SuccessIcon className="validated" />
return (
<div
className={`hash ${hash.unselected ? 'unselected' : ''}`}
key={hash.hash}
>
<div className="bar" style={barStyle} />
<div className="ledger-hash">
<div className="hash-concat">{hash.hash.substr(0, 6)}</div>
{validated}
</div>
<div className="subtitle">
<div className="validation-total">
<div>{t('total')}:</div>
<b>{hash.validations.length}</b>
</div>
<LedgerEntryHashTrustedCount
hash={hash}
unlCount={unlCount}
validators={validators}
/>
</div>
<div className="validations">
{hash.validations.map((validation, i) => (
<LedgerEntryValidator
validators={validators}
validator={validation}
index={i}
key={validation.cookie}
/>
))}
</div>
</div>
)
}
57 changes: 57 additions & 0 deletions src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useTranslation } from 'react-i18next'
import { useTooltip } from '../shared/components/Tooltip'
import { Hash, ValidatorResponse } from './types'

export const LedgerEntryHashTrustedCount = ({
hash,
unlCount,
validators,
}: {
hash: Hash
unlCount?: number
validators: { [pubkey: string]: ValidatorResponse }
}) => {
const { t } = useTranslation()
const { hideTooltip, showTooltip } = useTooltip()
const className = hash.trusted_count < (unlCount || 0) ? 'missed' : ''

const getMissingValidators = () => {
const unl = {}

Object.keys(validators).forEach((pubkey) => {
if (validators[pubkey].unl) {
unl[pubkey] = false
}
})

hash.validations.forEach((v) => {
if (unl[v.pubkey] !== undefined) {
delete unl[v.pubkey]
}
})

return Object.keys(unl).map((pubkey) => validators[pubkey])
}

const missing =
hash.trusted_count && className === 'missed' ? getMissingValidators() : null

return hash.trusted_count ? (
<span
tabIndex={0}
role="button"
className={className}
onMouseMove={(e) =>
missing && missing.length && showTooltip('missing', e, { missing })
}
onFocus={() => {}}
onKeyUp={() => {}}
onMouseLeave={() => hideTooltip()}
>
<div>{t('unl')}:</div>
<b>
{hash.trusted_count}/{unlCount}
</b>
</span>
) : null
}
35 changes: 35 additions & 0 deletions src/containers/Ledgers/LedgerEntryTransaction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import classNames from 'classnames'
import { getAction, getCategory } from '../shared/components/Transaction'
import { TRANSACTION_ROUTE } from '../App/routes'
import { TransactionActionIcon } from '../shared/components/TransactionActionIcon/TransactionActionIcon'
import { RouteLink } from '../shared/routing'
import { useTooltip } from '../shared/components/Tooltip'
import { TransactionSummary } from '../shared/types'

export const LedgerEntryTransaction = ({
transaction,
}: {
transaction: TransactionSummary
}) => {
const { hideTooltip, showTooltip } = useTooltip()

return (
<RouteLink
key={transaction.hash}
className={classNames(
`txn transaction-type transaction-dot bg`,
`tx-category-${getCategory(transaction.type)}`,
`transaction-action-${getAction(transaction.type)}`,
`${transaction.result}`,
)}
onMouseOver={(e) => showTooltip('tx', e, transaction)}
onFocus={() => {}}
onMouseLeave={() => hideTooltip()}
to={TRANSACTION_ROUTE}
params={{ identifier: transaction.hash }}
>
<TransactionActionIcon type={transaction.type} />
<span>{transaction.hash}</span>
</RouteLink>
)
}
28 changes: 28 additions & 0 deletions src/containers/Ledgers/LedgerEntryTransactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { memo } from 'react'
import { Loader } from '../shared/components/Loader'
import { LedgerEntryTransaction } from './LedgerEntryTransaction'
import { TransactionSummary } from '../shared/types'

/**
* A separate component to handle iterating over the transactions for a ledger on the homepage.
* It is a separate component so that it can benefit from React's memoization the array only changes once
* when the ledger closes and the call returns will all its transactions
* @param transactions
* @constructor
*/
export const LedgerEntryTransactions = memo(
({ transactions }: { transactions: TransactionSummary[] }) => (
<>
{transactions == null && <Loader />}
<div className="transactions">
{transactions?.map((tx) => (
<LedgerEntryTransaction transaction={tx} key={tx.hash} />
))}
</div>
</>
),
(prevProps, nextProps) =>
prevProps.transactions &&
nextProps.transactions &&
prevProps.transactions.length === nextProps.transactions.length,
)
47 changes: 47 additions & 0 deletions src/containers/Ledgers/LedgerEntryValidator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useSelectedValidator } from './useSelectedValidator'
import { useTooltip } from '../shared/components/Tooltip'
import { ValidatorResponse } from './types'

export const LedgerEntryValidator = ({
validator,
validators,
index,
}: {
validator: any
validators: { [pubkey: string]: ValidatorResponse }
index: number
}) => {
const { showTooltip, hideTooltip } = useTooltip()
const { selectedValidator, setSelectedValidator } = useSelectedValidator()

const trusted = validator.unl ? 'trusted' : ''
const unselected = selectedValidator ? 'unselected' : ''
const selected = selectedValidator === validator.pubkey ? 'selected' : ''
const className = `validation ${trusted} ${unselected} ${selected} ${validator.pubkey}`
const partial = validator.partial ? <div className="partial" /> : null

return (
<div
key={`${validator.pubkey}_${validator.cookie}`}
role="button"
tabIndex={index}
className={className}
onMouseOver={(e) =>
showTooltip('validator', e, {
...validator,
v: validators[validator.pubkey],
})
}
onFocus={() => {}}
onKeyUp={() => {}}
onMouseLeave={() => hideTooltip()}
onClick={() =>
setSelectedValidator(
selectedValidator === validator.pubkey ? undefined : validator.pubkey,
)
}
>
{partial}
</div>
)
}
99 changes: 99 additions & 0 deletions src/containers/Ledgers/LedgerListEntry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useTranslation } from 'react-i18next'
import { Ledger, ValidatorResponse } from './types'
import { RouteLink } from '../shared/routing'
import { LEDGER_ROUTE } from '../App/routes'
import { Amount } from '../shared/components/Amount'
import { LedgerEntryHash } from './LedgerEntryHash'
import { LedgerEntryTransactions } from './LedgerEntryTransactions'
import {
Tooltip,
TooltipProvider,
useTooltip,
} from '../shared/components/Tooltip'

const SIGMA = '\u03A3'

const LedgerIndex = ({ ledgerIndex }: { ledgerIndex: number }) => {
const { t } = useTranslation()
const flagLedger = ledgerIndex % 256 === 0
return (
<div
className={`ledger-index ${flagLedger ? 'flag-ledger' : ''}`}
title={flagLedger ? t('flag_ledger') : ''}
>
<RouteLink to={LEDGER_ROUTE} params={{ identifier: ledgerIndex }}>
{ledgerIndex}
</RouteLink>
</div>
)
}

export const LedgerListEntryInner = ({
ledger,
unlCount,
validators,
}: {
ledger: Ledger
unlCount?: number
validators: { [pubkey: string]: ValidatorResponse }
}) => {
const { tooltip } = useTooltip()
const { t } = useTranslation()
const time = ledger.close_time
? new Date(ledger.close_time).toLocaleTimeString()
: null

return (
<div className="ledger" key={ledger.ledger_index}>
<div className="ledger-head">
<LedgerIndex ledgerIndex={ledger.ledger_index} />
<div className="close-time">{time}</div>
{/* Render Transaction Count (can be 0) */}
{ledger.txn_count !== undefined && (
<div className="txn-count">
{t('txn_count')}:<b>{ledger.txn_count.toLocaleString()}</b>
</div>
)}
{/* Render Total Fees (can be 0) */}
{ledger.total_fees !== undefined && (
<div className="fees">
{SIGMA} {t('fees')}:
<b>
<Amount value={{ currency: 'XRP', amount: ledger.total_fees }} />
</b>
</div>
)}
<LedgerEntryTransactions transactions={ledger.transactions} />
</div>
<div className="hashes">
{ledger.hashes.map((hash) => (
<LedgerEntryHash
hash={hash}
key={hash.hash}
unlCount={unlCount}
validators={validators}
/>
))}
</div>
<Tooltip tooltip={tooltip} />
</div>
)
}

export const LedgerListEntry = ({
ledger,
unlCount,
validators,
}: {
ledger: Ledger
unlCount?: number
validators: { [pubkey: string]: ValidatorResponse }
}) => (
<TooltipProvider>
<LedgerListEntryInner
ledger={ledger}
validators={validators}
unlCount={unlCount}
/>
</TooltipProvider>
)
Loading

0 comments on commit 77c8722

Please sign in to comment.