From 256731a4b184a6a766f5ed2592f1ad670761b6ed Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Fri, 12 Jan 2024 18:06:00 -0600 Subject: [PATCH 01/18] Structurally update the homepage to be tsx and hooks --- src/containers/Ledgers/LedgerEntryHash.tsx | 39 ++++ .../Ledgers/LedgerEntryHashTrustedCount.tsx | 44 ++++ .../Ledgers/LedgerEntryTransaction.tsx | 43 ++++ .../Ledgers/LedgerEntryValidator.tsx | 45 +++++ src/containers/Ledgers/LedgerListEntry.tsx | 69 +++++++ src/containers/Ledgers/Ledgers.jsx | 189 +----------------- src/containers/Ledgers/index.tsx | 41 ++-- .../Ledgers/useSelectedValidator.tsx | 35 ++++ src/containers/Ledgers/useTooltip.tsx | 30 +++ 9 files changed, 330 insertions(+), 205 deletions(-) create mode 100644 src/containers/Ledgers/LedgerEntryHash.tsx create mode 100644 src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx create mode 100644 src/containers/Ledgers/LedgerEntryTransaction.tsx create mode 100644 src/containers/Ledgers/LedgerEntryValidator.tsx create mode 100644 src/containers/Ledgers/LedgerListEntry.tsx create mode 100644 src/containers/Ledgers/useSelectedValidator.tsx create mode 100644 src/containers/Ledgers/useTooltip.tsx diff --git a/src/containers/Ledgers/LedgerEntryHash.tsx b/src/containers/Ledgers/LedgerEntryHash.tsx new file mode 100644 index 000000000..8a45c92d8 --- /dev/null +++ b/src/containers/Ledgers/LedgerEntryHash.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next' +import SuccessIcon from '../shared/images/success.svg' +import { LedgerEntryValidator } from './LedgerEntryValidator' +import { LedgerEntryHashTrustedCount } from './LedgerEntryHashTrustedCount' + +export const LedgerEntryHash = ({ hash }: { hash: any }) => { + const { t } = useTranslation() + const shortHash = hash.hash.substr(0, 6) + const barStyle = { background: `#${shortHash}` } + const validated = hash.validated && + return ( +
+
+
+
{hash.hash.substr(0, 6)}
+ {validated} +
+
+
+
{t('total')}:
+ {hash.validations.length} +
+ +
+
+ {hash.validations.map((validation, i) => ( + + ))} +
+
+ ) +} diff --git a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx new file mode 100644 index 000000000..0df6f01db --- /dev/null +++ b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from 'react-i18next' +import { useTooltip } from './useTooltip' + +export const LedgerEntryHashTrustedCount = ({ hash }: any) => { + const { t } = useTranslation() + const { setTooltip } = useTooltip() + // TODO: Fix UNL Count + const unlCount = 0 + const className = hash.trusted_count < unlCount ? 'missed' : '' + const missing = + hash.trusted_count && className === 'missed' + ? this.getMissingValidators(hash) + : null + + const showTooltip = (mode, event, data) => { + setTooltip({ + ...data, + mode, + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) + } + + return hash.trusted_count ? ( + + missing && missing.length && showTooltip('missing', e, { missing }) + } + onFocus={(e) => {}} + onKeyUp={(e) => {}} + onMouseLeave={() => { + setTooltip(undefined) + }} + > +
{t('unl')}:
+ + {hash.trusted_count}/{unlCount} + +
+ ) : null +} diff --git a/src/containers/Ledgers/LedgerEntryTransaction.tsx b/src/containers/Ledgers/LedgerEntryTransaction.tsx new file mode 100644 index 000000000..781ee5054 --- /dev/null +++ b/src/containers/Ledgers/LedgerEntryTransaction.tsx @@ -0,0 +1,43 @@ +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 './useTooltip' + +export const LedgerEntryTransaction = ({ + transaction, +}: { + transaction: any +}) => { + const { setTooltip } = useTooltip() + const showTooltip = (mode, event, data) => { + setTooltip({ + ...data, + mode, + v: mode === 'validator' && data, + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) + } + + return ( + showTooltip('transaction', e, transaction)} + onFocus={() => {}} + onMouseLeave={() => setTooltip(undefined)} + to={TRANSACTION_ROUTE} + params={{ identifier: transaction.hash }} + > + + {transaction.hash} + + ) +} diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx new file mode 100644 index 000000000..1b9ad994b --- /dev/null +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -0,0 +1,45 @@ +import { useSelectedValidator } from './useSelectedValidator' +import { useTooltip } from './useTooltip' + +export const LedgerEntryValidator = ({ + validator, + index, +}: { + validator: any + index: number +}) => { + const { setTooltip } = 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 ?
: null + + const showTooltip = (mode, event, data) => { + setTooltip({ + ...data, + mode, + v: mode === 'validator' && data, + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) + } + + return ( +
showTooltip('validator', e, validator)} + onFocus={(e) => {}} + onKeyUp={(e) => {}} + onMouseLeave={() => setTooltip(undefined)} + onClick={() => setSelectedValidator(validator.pubkey)} + > + {partial} +
+ ) +} diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx new file mode 100644 index 000000000..9e0d54cfc --- /dev/null +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from 'react-i18next' +import { Loader } from '../shared/components/Loader' +import { Ledger } from './types' +import { RouteLink } from '../shared/routing' +import { LEDGER_ROUTE } from '../App/routes' +import { Amount } from '../shared/components/Amount' +import { LedgerEntryTransaction } from './LedgerEntryTransaction' +import { LedgerEntryHash } from './LedgerEntryHash' + +const SIGMA = '\u03A3' + +const LedgerIndex = ({ ledgerIndex }: { ledgerIndex: number }) => { + const { t } = useTranslation() + const flagLedger = ledgerIndex % 256 === 0 + return ( +
+ + {ledgerIndex} + +
+ ) +} + +export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => { + const { t } = useTranslation() + const time = ledger.close_time + ? new Date(ledger.close_time).toLocaleTimeString() + : null + const transactions = ledger.transactions || [] + + return ( +
+
+ +
{time}
+ {/* Render Transaction Count (can be 0) */} + {ledger.txn_count !== undefined && ( +
+ {t('txn_count')}:{ledger.txn_count.toLocaleString()} +
+ )} + {/* Render Total Fees (can be 0) */} + {ledger.total_fees !== undefined && ( +
+ {SIGMA} {t('fees')}: + + + +
+ )} + + {ledger.transactions == null && } +
+ {transactions.map((tx) => ( + + ))} +
+
+
+ {ledger.hashes.map((hash) => ( + + ))} +
+
+ ) +} diff --git a/src/containers/Ledgers/Ledgers.jsx b/src/containers/Ledgers/Ledgers.jsx index f0110e5c5..ef1b805f2 100644 --- a/src/containers/Ledgers/Ledgers.jsx +++ b/src/containers/Ledgers/Ledgers.jsx @@ -1,21 +1,15 @@ import { Component } from 'react' import { withTranslation } from 'react-i18next' import PropTypes from 'prop-types' -import { CURRENCY_OPTIONS } from '../shared/transactionUtils' -import { localizeNumber } from '../shared/utils' import Tooltip from '../shared/components/Tooltip' import './css/ledgers.scss' -import SuccessIcon from '../shared/images/success.svg' import DomainLink from '../shared/components/DomainLink' import { Loader } from '../shared/components/Loader' import SocketContext from '../shared/SocketContext' -import { getAction, getCategory } from '../shared/components/Transaction' -import { TransactionActionIcon } from '../shared/components/TransactionActionIcon/TransactionActionIcon' import { Legend } from './Legend' import { RouteLink } from '../shared/routing' -import { LEDGER_ROUTE, TRANSACTION_ROUTE, VALIDATOR_ROUTE } from '../App/routes' - -const SIGMA = '\u03A3' +import { VALIDATOR_ROUTE } from '../App/routes' +import { LedgerListEntry } from './LedgerListEntry' class Ledgers extends Component { constructor(props) { @@ -55,21 +49,6 @@ class Ledgers extends Component { return Object.keys(unl).map((pubkey) => validators[pubkey]) } - showTooltip = (mode, event, data) => { - const { validators } = this.state - this.setState({ - tooltip: { - ...data, - mode, - v: mode === 'validator' && validators[data.pubkey], - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, - }, - }) - } - - hideTooltip = () => this.setState({ tooltip: null }) - renderSelected = () => { const { validators, selected } = this.state const v = validators[selected] || {} @@ -87,166 +66,6 @@ class Ledgers extends Component { ) } - renderLedger = (ledger) => { - const time = ledger.close_time - ? new Date(ledger.close_time).toLocaleTimeString() - : null - const transactions = ledger.transactions || [] - - return ( -
-
- {this.renderLedgerIndex(ledger.ledger_index)} -
{time}
- {this.renderTxnCount(ledger.txn_count)} - {this.renderFees(ledger.total_fees)} - {ledger.transactions == null && } -
- {transactions.map(this.renderTransaction)} -
-
-
{ledger.hashes.map(this.renderHash)}
-
- ) - } - - renderLedgerIndex = (ledgerIndex) => { - const { t } = this.props - const flagLedger = ledgerIndex % 256 === 0 - return ( -
- - {ledgerIndex} - -
- ) - } - - renderTxnCount = (count) => { - const { t } = this.props - return count !== undefined ? ( -
- {t('txn_count')}:{count.toLocaleString()} -
- ) : null - } - - renderFees = (d) => { - const { t, language } = this.props - const options = { ...CURRENCY_OPTIONS, currency: 'XRP' } - const amount = localizeNumber(d, language, options) - return d !== undefined ? ( -
- {SIGMA} {t('fees')}:{amount} -
- ) : null - } - - renderTransaction = (tx) => ( - this.showTooltip('tx', e, tx)} - onFocus={(e) => {}} - onMouseLeave={this.hideTooltip} - to={TRANSACTION_ROUTE} - params={{ identifier: tx.hash }} - > - - {tx.hash} - - ) - - renderHash = (hash) => { - const { t } = this.props - const shortHash = hash.hash.substr(0, 6) - const barStyle = { background: `#${shortHash}` } - const validated = hash.validated && - return ( -
-
-
-
{hash.hash.substr(0, 6)}
- {validated} -
-
-
-
{t('total')}:
- {hash.validations.length} -
- {this.renderTrustedCount(hash)} -
-
- {hash.validations.map(this.renderValidator)} -
-
- ) - } - - renderTrustedCount = (hash) => { - const { t } = this.props - const { unlCount } = this.state - const className = hash.trusted_count < unlCount ? 'missed' : null - const missing = - hash.trusted_count && className === 'missed' - ? this.getMissingValidators(hash) - : null - - return hash.trusted_count ? ( - - missing && - missing.length && - this.showTooltip('missing', e, { missing }) - } - onFocus={(e) => {}} - onKeyUp={(e) => {}} - onMouseLeave={this.hideTooltip} - > -
{t('unl')}:
- - {hash.trusted_count}/{unlCount} - -
- ) : null - } - - renderValidator = (v, i) => { - const { setSelected } = this.props - const { selected: selectedState } = this.state - const trusted = v.unl ? 'trusted' : '' - const unselected = selectedState ? 'unselected' : '' - const selected = selectedState === v.pubkey ? 'selected' : '' - const className = `validation ${trusted} ${unselected} ${selected} ${v.pubkey}` - const partial = v.partial ?
: null - return ( -
this.showTooltip('validator', e, v)} - onFocus={(e) => {}} - onKeyUp={(e) => {}} - onMouseLeave={this.hideTooltip} - onClick={() => setSelected(v.pubkey)} - > - {partial} -
- ) - } - render() { const { ledgers, selected, tooltip } = this.state const { t, language, isOnline } = this.props @@ -257,7 +76,9 @@ class Ledgers extends Component {
{selected && this.renderSelected()}
- {ledgers.map(this.renderLedger)}{' '} + {ledgers.map((ledger) => ( + + ))}{' '}
{' '} diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index f1b432890..33350ca7d 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -13,6 +13,8 @@ import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' import { useIsOnline } from '../shared/SocketContext' import { useLanguage } from '../shared/hooks' +import { TooltipProvider } from './useTooltip' +import { SelectedValidatorProvider } from './useSelectedValidator' const FETCH_INTERVAL_MILLIS = 5 * 60 * 1000 @@ -23,7 +25,6 @@ const LedgersPage = () => { >({}) const [ledgers, setLedgers] = useState([]) const [paused, setPaused] = useState(false) - const [selected, setSelected] = useState(null) const [metrics, setMetrics] = useState(undefined) const [unlCount, setUnlCount] = useState(undefined) const { isOnline } = useIsOnline() @@ -71,10 +72,6 @@ const LedgersPage = () => { enabled: !!network, }) - const updateSelected = (pubkey: string) => { - setSelected(selected === pubkey ? null : pubkey) - } - const pause = () => setPaused(!paused) return ( @@ -87,22 +84,24 @@ const LedgersPage = () => { updateMetrics={setMetrics} /> )} - pause()} - paused={paused} - /> - + + + pause()} + paused={paused} + /> + + +
) } diff --git a/src/containers/Ledgers/useSelectedValidator.tsx b/src/containers/Ledgers/useSelectedValidator.tsx new file mode 100644 index 000000000..ef8042ee4 --- /dev/null +++ b/src/containers/Ledgers/useSelectedValidator.tsx @@ -0,0 +1,35 @@ +import { + createContext, + Dispatch, + FC, + SetStateAction, + useContext, + useState, +} from 'react' + +export interface SelectedValidatorContextType { + selectedValidator?: string + setSelectedValidator: Dispatch> +} + +export const SelectedValidatorContext = + createContext({ + selectedValidator: undefined, + setSelectedValidator: (validator: SetStateAction) => + validator, + }) + +export const SelectedValidatorProvider: FC = ({ children }) => { + const [selectedValidator, setSelectedValidator] = useState() + + return ( + + {children} + + ) +} + +export const useSelectedValidator = (): SelectedValidatorContextType => + useContext(SelectedValidatorContext) diff --git a/src/containers/Ledgers/useTooltip.tsx b/src/containers/Ledgers/useTooltip.tsx new file mode 100644 index 000000000..83cfade19 --- /dev/null +++ b/src/containers/Ledgers/useTooltip.tsx @@ -0,0 +1,30 @@ +import { + createContext, + Dispatch, + FC, + SetStateAction, + useContext, + useState, +} from 'react' + +export interface TooltipContextType { + tooltip?: string + setTooltip: Dispatch> +} + +export const TooltipContext = createContext({ + tooltip: undefined, + setTooltip: (validator: SetStateAction) => validator, +}) + +export const TooltipProvider: FC = ({ children }) => { + const [tooltip, setTooltip] = useState() + + return ( + + {children} + + ) +} + +export const useTooltip = (): TooltipContextType => useContext(TooltipContext) From 36992936dcd7828bbdf698eec7dd26087b579766 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Fri, 12 Jan 2024 18:29:56 -0600 Subject: [PATCH 02/18] switched Ledgers.jsx to tsx and hooks --- .../Ledgers/LedgerEntryHashTrustedCount.tsx | 19 +++ .../Ledgers/LedgerEntryTransaction.tsx | 2 +- src/containers/Ledgers/LedgerListEntry.tsx | 3 +- src/containers/Ledgers/Ledgers.jsx | 116 ------------------ src/containers/Ledgers/Ledgers.tsx | 61 +++++++++ src/containers/Ledgers/index.tsx | 2 +- 6 files changed, 83 insertions(+), 120 deletions(-) delete mode 100644 src/containers/Ledgers/Ledgers.jsx create mode 100644 src/containers/Ledgers/Ledgers.tsx diff --git a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx index 0df6f01db..02a6b5cc4 100644 --- a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx +++ b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx @@ -21,6 +21,25 @@ export const LedgerEntryHashTrustedCount = ({ hash }: any) => { }) } + // getMissingValidators = (hash) => { + // const { validators } = this.props + // 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]) + // } + return hash.trusted_count ? ( showTooltip('transaction', e, transaction)} + onMouseOver={(e) => showTooltip('tx', e, transaction)} onFocus={() => {}} onMouseLeave={() => setTooltip(undefined)} to={TRANSACTION_ROUTE} diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index 9e0d54cfc..2be7b3be9 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -51,11 +51,10 @@ export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => {
)} - {ledger.transactions == null && }
{transactions.map((tx) => ( - + ))}
diff --git a/src/containers/Ledgers/Ledgers.jsx b/src/containers/Ledgers/Ledgers.jsx deleted file mode 100644 index ef1b805f2..000000000 --- a/src/containers/Ledgers/Ledgers.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import { Component } from 'react' -import { withTranslation } from 'react-i18next' -import PropTypes from 'prop-types' -import Tooltip from '../shared/components/Tooltip' -import './css/ledgers.scss' -import DomainLink from '../shared/components/DomainLink' -import { Loader } from '../shared/components/Loader' -import SocketContext from '../shared/SocketContext' -import { Legend } from './Legend' -import { RouteLink } from '../shared/routing' -import { VALIDATOR_ROUTE } from '../App/routes' -import { LedgerListEntry } from './LedgerListEntry' - -class Ledgers extends Component { - constructor(props) { - super(props) - this.state = { - ledgers: [], - validators: {}, - tooltip: null, - } - } - - static getDerivedStateFromProps(nextProps, prevState) { - return { - selected: nextProps.selected, - ledgers: nextProps.paused ? prevState.ledgers : nextProps.ledgers, - validators: nextProps.validators, - unlCount: nextProps.unlCount, - } - } - - getMissingValidators = (hash) => { - const { validators } = this.props - 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]) - } - - renderSelected = () => { - const { validators, selected } = this.state - const v = validators[selected] || {} - return ( -
- {v.domain && } - - {selected} - -
- ) - } - - render() { - const { ledgers, selected, tooltip } = this.state - const { t, language, isOnline } = this.props - return ( -
- {isOnline && ledgers.length > 0 ? ( - <> - -
{selected && this.renderSelected()}
-
- {ledgers.map((ledger) => ( - - ))}{' '} - -
{' '} - - ) : ( - - )} -
- ) - } -} - -Ledgers.contextType = SocketContext - -Ledgers.propTypes = { - ledgers: PropTypes.arrayOf(PropTypes.shape({})), // eslint-disable-line - validators: PropTypes.shape({}), // eslint-disable-line - unlCount: PropTypes.number, // eslint-disable-line - selected: PropTypes.string, // eslint-disable-line - setSelected: PropTypes.func, - language: PropTypes.string.isRequired, - t: PropTypes.func.isRequired, - paused: PropTypes.bool, - isOnline: PropTypes.bool.isRequired, -} - -Ledgers.defaultProps = { - ledgers: [], - validators: {}, - unlCount: 0, - selected: null, - setSelected: () => {}, - paused: false, -} - -export default withTranslation()(Ledgers) diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx new file mode 100644 index 000000000..4e4484ce8 --- /dev/null +++ b/src/containers/Ledgers/Ledgers.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from 'react-i18next' +import Tooltip from '../shared/components/Tooltip' +import './css/ledgers.scss' +import DomainLink from '../shared/components/DomainLink' +import { Loader } from '../shared/components/Loader' +import { useIsOnline } from '../shared/SocketContext' +import { Legend } from './Legend' +import { RouteLink } from '../shared/routing' +import { VALIDATOR_ROUTE } from '../App/routes' +import { LedgerListEntry } from './LedgerListEntry' +import { useSelectedValidator } from './useSelectedValidator' +import { useLanguage } from '../shared/hooks' +import { useTooltip } from './useTooltip' + +export const Ledgers = ({ + ledgers = [], + validators = {}, +}: { + ledgers: any[] + validators: any +}) => { + const { selectedValidator } = useSelectedValidator() + const isOnline = useIsOnline() + const language = useLanguage() + const { tooltip } = useTooltip() + const { t } = useTranslation() + + return ( +
+ {isOnline && ledgers.length > 0 ? ( + <> + +
+ {selectedValidator && ( +
+ {validators[selectedValidator].domain && ( + + )} + + {selectedValidator} + +
+ )} +
+
+ {ledgers.map((ledger) => ( + + ))}{' '} + +
{' '} + + ) : ( + + )} +
+ ) +} diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index 33350ca7d..965fb45cf 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -7,7 +7,7 @@ import Log from '../shared/log' import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils' import Streams from '../shared/components/Streams' import LedgerMetrics from './LedgerMetrics' -import Ledgers from './Ledgers' +import { Ledgers } from './Ledgers' import { Ledger, ValidatorResponse } from './types' import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' From 085d05e0857c7aa759962cbfa4d4dfede3b41abb Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Fri, 12 Jan 2024 19:13:19 -0600 Subject: [PATCH 03/18] LedgerMetrics using hooks and typescript --- .../Ledgers/LedgerEntryValidator.tsx | 4 +- src/containers/Ledgers/LedgerListEntry.tsx | 2 +- src/containers/Ledgers/LedgerMetrics.jsx | 161 ------------------ src/containers/Ledgers/LedgerMetrics.tsx | 130 ++++++++++++++ src/containers/Ledgers/index.tsx | 16 +- src/containers/Ledgers/useTooltip.tsx | 8 +- 6 files changed, 144 insertions(+), 177 deletions(-) delete mode 100644 src/containers/Ledgers/LedgerMetrics.jsx create mode 100644 src/containers/Ledgers/LedgerMetrics.tsx diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index 1b9ad994b..c7c9e49a9 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -34,8 +34,8 @@ export const LedgerEntryValidator = ({ tabIndex={index} className={className} onMouseOver={(e) => showTooltip('validator', e, validator)} - onFocus={(e) => {}} - onKeyUp={(e) => {}} + onFocus={() => {}} + onKeyUp={() => {}} onMouseLeave={() => setTooltip(undefined)} onClick={() => setSelectedValidator(validator.pubkey)} > diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index 2be7b3be9..b24f1a069 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -60,7 +60,7 @@ export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => {
{ledger.hashes.map((hash) => ( - + ))}
diff --git a/src/containers/Ledgers/LedgerMetrics.jsx b/src/containers/Ledgers/LedgerMetrics.jsx deleted file mode 100644 index 8ee430b8a..000000000 --- a/src/containers/Ledgers/LedgerMetrics.jsx +++ /dev/null @@ -1,161 +0,0 @@ -import { withTranslation } from 'react-i18next' -import { Component } from 'react' -import PropTypes from 'prop-types' -import Tooltip from '../shared/components/Tooltip' -import { renderXRP } from '../shared/utils' -import PauseIcon from '../shared/images/ic_pause.svg' -import ResumeIcon from '../shared/images/ic_play.svg' -import './css/ledgerMetrics.scss' -import SocketContext from '../shared/SocketContext' - -const DEFAULTS = { - load_fee: '--', - txn_sec: '--', - txn_ledger: '--', - ledger_interval: '--', - avg_fee: '--', - quorum: '--', - nUnl: [], -} - -class LedgerMetrics extends Component { - constructor(props) { - super(props) - this.state = { - tooltip: null, - } - } - - static getDerivedStateFromProps(nextProps, prevState) { - return { ...prevState, ...nextProps } - } - - showTooltip = (event) => { - const { data, nUnl } = this.state - this.setState({ - tooltip: { - nUnl: data.nUnl, - mode: 'nUnl', - v: nUnl, - x: event.pageX, - y: event.pageY, - }, - }) - } - - hideTooltip = () => this.setState({ tooltip: null }) - - renderPause() { - const { t, onPause, paused } = this.props - const Icon = paused ? ResumeIcon : PauseIcon - const text = paused ? 'resume' : 'pause' - - return ( -
- - {t(text)} -
- ) - } - - render() { - const { language, t } = this.props - const { data: stateData } = this.state - const data = { ...DEFAULTS, ...stateData } - - if (data.load_fee === '--') { - data.load_fee = data.base_fee || '--' - } - delete data.base_fee - const items = Object.keys(data) - .map((key) => { - let content = null - - let className = 'label' - if (data[key] === undefined && key !== 'nUnl') { - content = '--' - } else if (key.includes('fee') && !isNaN(data[key])) { - content = renderXRP(data[key], language) - } else if (key === 'ledger_interval' && data[key] !== '--') { - content = `${data[key]} ${t('seconds_short')}` - } else if (key === 'nUnl' && data[key]?.length === 0) { - return null - } else if (key === 'nUnl') { - content = data[key].length - className = 'label n-unl-metric' - return ( -
{}} - onBlur={() => {}} - onMouseOver={(e) => this.showTooltip(e)} - onMouseOut={this.hideTooltip} - tabIndex={0} - key={key} - > - - {t(key)} - - {content} -
- ) - } else { - content = data[key] - } - - return ( -
-
{t(key)}
- {content} -
- ) - }) - .reverse() - - const { tooltip } = this.state - - // eslint-disable-next-line react/destructuring-assignment - const isOnline = this.context.getState().online - - return ( -
- {isOnline && ( - <> -
{this.renderPause()}
-
{items}
- - - )} -
- ) - } -} - -LedgerMetrics.contextType = SocketContext - -LedgerMetrics.propTypes = { - data: PropTypes.shape({}), - language: PropTypes.string.isRequired, - t: PropTypes.func.isRequired, - onPause: PropTypes.func.isRequired, - paused: PropTypes.bool.isRequired, -} - -LedgerMetrics.defaultProps = { - data: {}, -} - -export default withTranslation()(LedgerMetrics) diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx new file mode 100644 index 000000000..6de7ebe04 --- /dev/null +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -0,0 +1,130 @@ +import { useTranslation } from 'react-i18next' +import Tooltip from '../shared/components/Tooltip' +import { renderXRP } from '../shared/utils' +import PauseIcon from '../shared/images/ic_pause.svg' +import ResumeIcon from '../shared/images/ic_play.svg' +import './css/ledgerMetrics.scss' +import { useIsOnline } from '../shared/SocketContext' +import { useTooltip } from './useTooltip' +import { useLanguage } from '../shared/hooks' + +const DEFAULTS = { + load_fee: '--', + txn_sec: '--', + txn_ledger: '--', + ledger_interval: '--', + avg_fee: '--', + quorum: '--', + nUnl: [], +} + +export const LedgerMetrics = ({ + data, + onPause, + paused, +}: { + data: any + onPause: any + paused: boolean +}) => { + data = { ...DEFAULTS, ...data } + const { tooltip, setTooltip } = useTooltip() + const { t } = useTranslation() + const isOnline = useIsOnline() + const language = useLanguage() + + const showTooltip = (event) => { + setTooltip({ + nUnl: data.nUnl, + mode: 'nUnl', + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) + } + + const renderPause = () => { + const Icon = paused ? ResumeIcon : PauseIcon + const text = paused ? 'resume' : 'pause' + + return ( +
+ + {t(text)} +
+ ) + } + + if (data.load_fee === '--') { + data.load_fee = data.base_fee || '--' + } + delete data.base_fee + const items = Object.keys(data) + .map((key) => { + let content: any = null + + let className = 'label' + if (data[key] === undefined && key !== 'nUnl') { + content = '--' + } else if (key.includes('fee') && !isNaN(data[key])) { + content = renderXRP(data[key], language) + } else if (key === 'ledger_interval' && data[key] !== '--') { + content = `${data[key]} ${t('seconds_short')}` + } else if (key === 'nUnl' && data[key]?.length === 0) { + return null + } else if (key === 'nUnl') { + content = data[key].length + className = 'label n-unl-metric' + return ( +
{}} + onBlur={() => {}} + onMouseOver={(e) => showTooltip(e)} + onMouseOut={() => setTooltip(undefined)} + tabIndex={0} + key={key} + > + + {t(key)} + + {content} +
+ ) + } else { + content = data[key] + } + + return ( +
+
{t(key)}
+ {content} +
+ ) + }) + .reverse() + + return ( +
+ {isOnline && ( + <> +
{renderPause()}
+
{items}
+ + + )} +
+ ) +} diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index 965fb45cf..e6e7f860f 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -6,13 +6,12 @@ import axios from 'axios' import Log from '../shared/log' import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils' import Streams from '../shared/components/Streams' -import LedgerMetrics from './LedgerMetrics' +import { LedgerMetrics } from './LedgerMetrics' import { Ledgers } from './Ledgers' import { Ledger, ValidatorResponse } from './types' import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' import { useIsOnline } from '../shared/SocketContext' -import { useLanguage } from '../shared/hooks' import { TooltipProvider } from './useTooltip' import { SelectedValidatorProvider } from './useSelectedValidator' @@ -30,7 +29,6 @@ const LedgersPage = () => { const { isOnline } = useIsOnline() const { t } = useTranslation() const network = useContext(NetworkContext) - const language = useLanguage() useEffect(() => { trackScreenLoaded() @@ -84,24 +82,24 @@ const LedgersPage = () => { updateMetrics={setMetrics} /> )} - - + + pause()} paused={paused} /> + + - - + + ) } diff --git a/src/containers/Ledgers/useTooltip.tsx b/src/containers/Ledgers/useTooltip.tsx index 83cfade19..d46be2911 100644 --- a/src/containers/Ledgers/useTooltip.tsx +++ b/src/containers/Ledgers/useTooltip.tsx @@ -8,17 +8,17 @@ import { } from 'react' export interface TooltipContextType { - tooltip?: string - setTooltip: Dispatch> + tooltip?: any + setTooltip: Dispatch> } export const TooltipContext = createContext({ tooltip: undefined, - setTooltip: (validator: SetStateAction) => validator, + setTooltip: (validator: SetStateAction) => validator, }) export const TooltipProvider: FC = ({ children }) => { - const [tooltip, setTooltip] = useState() + const [tooltip, setTooltip] = useState() return ( From 8e2011e4d2b7d0498e8f35b84e90de84ffe6834d Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Tue, 16 Jan 2024 17:50:02 -0600 Subject: [PATCH 04/18] Fix tests and ledger hash output --- src/containers/App/test/App.test.jsx | 2 +- src/containers/Ledgers/LedgerEntryHash.tsx | 17 ++++- .../Ledgers/LedgerEntryHashTrustedCount.tsx | 70 +++++++++---------- .../Ledgers/LedgerEntryTransaction.tsx | 13 +--- .../Ledgers/LedgerEntryValidator.tsx | 24 +++---- src/containers/Ledgers/LedgerListEntry.tsx | 19 ++++- src/containers/Ledgers/LedgerMetrics.tsx | 15 +--- src/containers/Ledgers/Ledgers.tsx | 11 ++- src/containers/Ledgers/index.tsx | 1 - .../Ledgers/test/LedgersPage.test.js | 17 +++-- src/containers/Ledgers/useTooltip.tsx | 24 ++++++- 11 files changed, 121 insertions(+), 92 deletions(-) diff --git a/src/containers/App/test/App.test.jsx b/src/containers/App/test/App.test.jsx index 98b7baf12..15674340b 100644 --- a/src/containers/App/test/App.test.jsx +++ b/src/containers/App/test/App.test.jsx @@ -17,7 +17,7 @@ import { Error } from '../../../rippled/lib/utils' jest.mock('../../Ledgers/LedgerMetrics', () => ({ __esModule: true, - default: () => null, + LedgerMetrics: () => null, })) jest.mock('xrpl-client', () => ({ diff --git a/src/containers/Ledgers/LedgerEntryHash.tsx b/src/containers/Ledgers/LedgerEntryHash.tsx index 8a45c92d8..d9be2a969 100644 --- a/src/containers/Ledgers/LedgerEntryHash.tsx +++ b/src/containers/Ledgers/LedgerEntryHash.tsx @@ -2,8 +2,17 @@ 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 }: { hash: any }) => { +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}` } @@ -23,7 +32,11 @@ export const LedgerEntryHash = ({ hash }: { hash: any }) => {
{t('total')}:
{hash.validations.length} - +
{hash.validations.map((validation, i) => ( diff --git a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx index 02a6b5cc4..aad4fbaab 100644 --- a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx +++ b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx @@ -1,44 +1,40 @@ import { useTranslation } from 'react-i18next' import { useTooltip } from './useTooltip' +import { Hash, ValidatorResponse } from './types' -export const LedgerEntryHashTrustedCount = ({ hash }: any) => { +export const LedgerEntryHashTrustedCount = ({ + hash, + unlCount, + validators, +}: { + hash: Hash + unlCount?: number + validators: { [pubkey: string]: ValidatorResponse } +}) => { const { t } = useTranslation() - const { setTooltip } = useTooltip() - // TODO: Fix UNL Count - const unlCount = 0 - const className = hash.trusted_count < unlCount ? 'missed' : '' - const missing = - hash.trusted_count && className === 'missed' - ? this.getMissingValidators(hash) - : null + 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 + } + }) - const showTooltip = (mode, event, data) => { - setTooltip({ - ...data, - mode, - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, + hash.validations.forEach((v) => { + if (unl[v.pubkey] !== undefined) { + delete unl[v.pubkey] + } }) + + return Object.keys(unl).map((pubkey) => validators[pubkey]) } - // getMissingValidators = (hash) => { - // const { validators } = this.props - // 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 ? ( { onMouseMove={(e) => missing && missing.length && showTooltip('missing', e, { missing }) } - onFocus={(e) => {}} - onKeyUp={(e) => {}} - onMouseLeave={() => { - setTooltip(undefined) - }} + onFocus={() => {}} + onKeyUp={() => {}} + onMouseLeave={() => hideTooltip()} >
{t('unl')}:
diff --git a/src/containers/Ledgers/LedgerEntryTransaction.tsx b/src/containers/Ledgers/LedgerEntryTransaction.tsx index a7ddf8ebe..ee9f20f06 100644 --- a/src/containers/Ledgers/LedgerEntryTransaction.tsx +++ b/src/containers/Ledgers/LedgerEntryTransaction.tsx @@ -10,16 +10,7 @@ export const LedgerEntryTransaction = ({ }: { transaction: any }) => { - const { setTooltip } = useTooltip() - const showTooltip = (mode, event, data) => { - setTooltip({ - ...data, - mode, - v: mode === 'validator' && data, - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, - }) - } + const { hideTooltip, showTooltip } = useTooltip() return ( showTooltip('tx', e, transaction)} onFocus={() => {}} - onMouseLeave={() => setTooltip(undefined)} + onMouseLeave={() => hideTooltip()} to={TRANSACTION_ROUTE} params={{ identifier: transaction.hash }} > diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index c7c9e49a9..4c38a85ad 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -8,7 +8,7 @@ export const LedgerEntryValidator = ({ validator: any index: number }) => { - const { setTooltip } = useTooltip() + const { showTooltip, hideTooltip } = useTooltip() const { selectedValidator, setSelectedValidator } = useSelectedValidator() const trusted = validator.unl ? 'trusted' : '' @@ -17,27 +17,23 @@ export const LedgerEntryValidator = ({ const className = `validation ${trusted} ${unselected} ${selected} ${validator.pubkey}` const partial = validator.partial ?
: null - const showTooltip = (mode, event, data) => { - setTooltip({ - ...data, - mode, - v: mode === 'validator' && data, - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, - }) - } - return (
showTooltip('validator', e, validator)} + onMouseOver={(e) => + showTooltip('validator', e, { validator, v: validator }) + } onFocus={() => {}} onKeyUp={() => {}} - onMouseLeave={() => setTooltip(undefined)} - onClick={() => setSelectedValidator(validator.pubkey)} + onMouseLeave={() => hideTooltip()} + onClick={() => + setSelectedValidator( + selectedValidator === validator.pubkey ? undefined : validator.pubkey, + ) + } > {partial}
diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index b24f1a069..72100af1e 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' import { Loader } from '../shared/components/Loader' -import { Ledger } from './types' +import { Ledger, ValidatorResponse } from './types' import { RouteLink } from '../shared/routing' import { LEDGER_ROUTE } from '../App/routes' import { Amount } from '../shared/components/Amount' @@ -24,7 +24,15 @@ const LedgerIndex = ({ ledgerIndex }: { ledgerIndex: number }) => { ) } -export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => { +export const LedgerListEntry = ({ + ledger, + unlCount, + validators, +}: { + ledger: Ledger + unlCount?: number + validators: { [pubkey: string]: ValidatorResponse } +}) => { const { t } = useTranslation() const time = ledger.close_time ? new Date(ledger.close_time).toLocaleTimeString() @@ -60,7 +68,12 @@ export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => {
{ledger.hashes.map((hash) => ( - + ))}
diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx index 6de7ebe04..5cf171029 100644 --- a/src/containers/Ledgers/LedgerMetrics.tsx +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -28,20 +28,11 @@ export const LedgerMetrics = ({ paused: boolean }) => { data = { ...DEFAULTS, ...data } - const { tooltip, setTooltip } = useTooltip() + const { tooltip, showTooltip, hideTooltip } = useTooltip() const { t } = useTranslation() const isOnline = useIsOnline() const language = useLanguage() - const showTooltip = (event) => { - setTooltip({ - nUnl: data.nUnl, - mode: 'nUnl', - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, - }) - } - const renderPause = () => { const Icon = paused ? ResumeIcon : PauseIcon const text = paused ? 'resume' : 'pause' @@ -86,8 +77,8 @@ export const LedgerMetrics = ({ className="cell" onFocus={() => {}} onBlur={() => {}} - onMouseOver={(e) => showTooltip(e)} - onMouseOut={() => setTooltip(undefined)} + onMouseOver={(e) => showTooltip('nUnl', e, { nUnl: data.nUnl })} + onMouseOut={() => hideTooltip()} tabIndex={0} key={key} > diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index 4e4484ce8..041d01f97 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -13,10 +13,14 @@ import { useLanguage } from '../shared/hooks' import { useTooltip } from './useTooltip' export const Ledgers = ({ + // paused, ledgers = [], + unlCount, validators = {}, }: { + // paused: boolean ledgers: any[] + unlCount?: number validators: any }) => { const { selectedValidator } = useSelectedValidator() @@ -48,7 +52,12 @@ export const Ledgers = ({
{ledgers.map((ledger) => ( - + ))}{' '}
{' '} diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index e6e7f860f..3343a8ad4 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -96,7 +96,6 @@ const LedgersPage = () => { validators={validators} unlCount={unlCount} paused={paused} - isOnline={isOnline} /> diff --git a/src/containers/Ledgers/test/LedgersPage.test.js b/src/containers/Ledgers/test/LedgersPage.test.js index 2b5f474f4..e5440ac46 100644 --- a/src/containers/Ledgers/test/LedgersPage.test.js +++ b/src/containers/Ledgers/test/LedgersPage.test.js @@ -15,6 +15,7 @@ import ledgerMessage from './mock/ledger.json' import validationMessage from './mock/validation.json' import rippledResponses from './mock/rippled.json' import { QuickHarness } from '../../test/utils' +import { SelectedValidatorProvider } from '../useSelectedValidator' function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)) @@ -86,13 +87,15 @@ describe('Ledgers Page container', () => { return mount( - - - - - - - + + + + + + + + + , ) } diff --git a/src/containers/Ledgers/useTooltip.tsx b/src/containers/Ledgers/useTooltip.tsx index d46be2911..3a0a32396 100644 --- a/src/containers/Ledgers/useTooltip.tsx +++ b/src/containers/Ledgers/useTooltip.tsx @@ -2,6 +2,7 @@ import { createContext, Dispatch, FC, + MouseEvent, SetStateAction, useContext, useState, @@ -10,18 +11,37 @@ import { export interface TooltipContextType { tooltip?: any setTooltip: Dispatch> + hideTooltip: () => void + showTooltip: (mode: string, event: MouseEvent, data: any) => void } export const TooltipContext = createContext({ tooltip: undefined, - setTooltip: (validator: SetStateAction) => validator, + setTooltip: (tt: SetStateAction) => tt, + hideTooltip: () => {}, + showTooltip: () => {}, }) export const TooltipProvider: FC = ({ children }) => { const [tooltip, setTooltip] = useState() + const hideTooltip = () => setTooltip(undefined) + const showTooltip = ( + mode: string, + event: MouseEvent, + data: any, + ) => { + setTooltip({ + ...data, + mode, + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) + } return ( - + {children} ) From 80e0cdd1698f3ca99e4b478b1f78d1aa1231de6d Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Tue, 16 Jan 2024 19:02:30 -0600 Subject: [PATCH 05/18] refactor: Tooltip to hooks and Typescript --- src/containers/Ledgers/LedgerMetrics.jsx | 12 +- src/containers/Ledgers/Ledgers.jsx | 12 +- src/containers/NFT/NFTHeader/NFTHeader.tsx | 15 +- src/containers/Network/Hexagons.jsx | 6 +- .../PayStrings/PayStringHeader/index.tsx | 10 +- src/containers/shared/components/Tooltip.jsx | 151 ------------------ src/containers/shared/components/Tooltip.tsx | 126 +++++++++++++++ src/containers/shared/css/tooltip.scss | 10 +- 8 files changed, 159 insertions(+), 183 deletions(-) delete mode 100644 src/containers/shared/components/Tooltip.jsx create mode 100644 src/containers/shared/components/Tooltip.tsx diff --git a/src/containers/Ledgers/LedgerMetrics.jsx b/src/containers/Ledgers/LedgerMetrics.jsx index 8ee430b8a..af665879c 100644 --- a/src/containers/Ledgers/LedgerMetrics.jsx +++ b/src/containers/Ledgers/LedgerMetrics.jsx @@ -1,7 +1,7 @@ import { withTranslation } from 'react-i18next' import { Component } from 'react' import PropTypes from 'prop-types' -import Tooltip from '../shared/components/Tooltip' +import { Tooltip } from '../shared/components/Tooltip' import { renderXRP } from '../shared/utils' import PauseIcon from '../shared/images/ic_pause.svg' import ResumeIcon from '../shared/images/ic_play.svg' @@ -31,14 +31,14 @@ class LedgerMetrics extends Component { } showTooltip = (event) => { - const { data, nUnl } = this.state + const { nUnl } = this.state.data this.setState({ tooltip: { - nUnl: data.nUnl, + data: { nUnl }, mode: 'nUnl', v: nUnl, - x: event.pageX, - y: event.pageY, + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, }, }) } @@ -136,7 +136,7 @@ class LedgerMetrics extends Component { <>
{this.renderPause()}
{items}
- + )} diff --git a/src/containers/Ledgers/Ledgers.jsx b/src/containers/Ledgers/Ledgers.jsx index f0110e5c5..19324ee08 100644 --- a/src/containers/Ledgers/Ledgers.jsx +++ b/src/containers/Ledgers/Ledgers.jsx @@ -3,8 +3,7 @@ import { withTranslation } from 'react-i18next' import PropTypes from 'prop-types' import { CURRENCY_OPTIONS } from '../shared/transactionUtils' import { localizeNumber } from '../shared/utils' -import Tooltip from '../shared/components/Tooltip' -import './css/ledgers.scss' +import { Tooltip } from '../shared/components/Tooltip' import SuccessIcon from '../shared/images/success.svg' import DomainLink from '../shared/components/DomainLink' import { Loader } from '../shared/components/Loader' @@ -14,6 +13,7 @@ import { TransactionActionIcon } from '../shared/components/TransactionActionIco import { Legend } from './Legend' import { RouteLink } from '../shared/routing' import { LEDGER_ROUTE, TRANSACTION_ROUTE, VALIDATOR_ROUTE } from '../App/routes' +import './css/ledgers.scss' const SIGMA = '\u03A3' @@ -59,16 +59,15 @@ class Ledgers extends Component { const { validators } = this.state this.setState({ tooltip: { - ...data, + data: { ...data, v: mode === 'validator' && validators[data.pubkey] }, mode, - v: mode === 'validator' && validators[data.pubkey], x: event.currentTarget.offsetLeft, y: event.currentTarget.offsetTop, }, }) } - hideTooltip = () => this.setState({ tooltip: null }) + hideTooltip = () => this.setState({ tooltip: undefined }) renderSelected = () => { const { validators, selected } = this.state @@ -257,8 +256,7 @@ class Ledgers extends Component {
{selected && this.renderSelected()}
- {ledgers.map(this.renderLedger)}{' '} - + {ledgers.map(this.renderLedger)}
{' '} ) : ( diff --git a/src/containers/NFT/NFTHeader/NFTHeader.tsx b/src/containers/NFT/NFTHeader/NFTHeader.tsx index 5d59d5549..1a46f7ac6 100644 --- a/src/containers/NFT/NFTHeader/NFTHeader.tsx +++ b/src/containers/NFT/NFTHeader/NFTHeader.tsx @@ -4,7 +4,7 @@ import { useQuery } from 'react-query' import { Loader } from '../../shared/components/Loader' import './styles.scss' import SocketContext from '../../shared/SocketContext' -import Tooltip from '../../shared/components/Tooltip' +import { Tooltip, TooltipInstance } from '../../shared/components/Tooltip' import { getNFTInfo, getAccountInfo } from '../../../rippled/lib/rippled' import { formatNFTInfo, formatAccountInfo } from '../../../rippled/lib/utils' import { localizeDate, BAD_REQUEST, HASH_REGEX } from '../../shared/utils' @@ -39,7 +39,7 @@ export const NFTHeader = (props: Props) => { const { tokenId, setError } = props const rippledSocket = useContext(SocketContext) const { trackException } = useAnalytics() - const [tooltip, setTooltip] = useState(null) + const [tooltip, setTooltip] = useState(undefined) const { data, isFetching: loading } = useQuery( ['getNFTInfo', tokenId], @@ -90,11 +90,16 @@ export const NFTHeader = (props: Props) => { : undefined const showTooltip = (event: any, d: any) => { - setTooltip({ ...d, mode: 'nftId', x: event.pageX, y: event.pageY }) + setTooltip({ + data: d, + mode: 'nftId', + x: event.currentTarget.offsetLeft, + y: event.currentTarget.offsetTop, + }) } const hideTooltip = () => { - setTooltip(null) + setTooltip(undefined) } const renderHeaderContent = () => { @@ -154,7 +159,7 @@ export const NFTHeader = (props: Props) => {
{loading ? : renderHeaderContent()}
- + ) } diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index a55c0f5de..4a84e46e2 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -4,9 +4,9 @@ import { useWindowSize } from 'usehooks-ts' import { hexbin } from 'd3-hexbin' import { Loader } from '../shared/components/Loader' -import Tooltip from '../shared/components/Tooltip' -import './css/hexagons.scss' +import { Tooltip } from '../shared/components/Tooltip' import { useLanguage } from '../shared/hooks' +import './css/hexagons.scss' const MAX_WIDTH = 1200 const getDimensions = (width) => ({ @@ -126,7 +126,7 @@ export const Hexagons = ({ list, data }) => { {hexagons?.length === 0 && } - + ) } diff --git a/src/containers/PayStrings/PayStringHeader/index.tsx b/src/containers/PayStrings/PayStringHeader/index.tsx index 9c85c55ed..5a8e18c75 100644 --- a/src/containers/PayStrings/PayStringHeader/index.tsx +++ b/src/containers/PayStrings/PayStringHeader/index.tsx @@ -1,10 +1,8 @@ import { useState, useRef } from 'react' -import { useTranslation } from 'react-i18next' import PayStringLogomark from '../../shared/images/PayString_Logomark.png' import QuestIcon from '../../shared/images/hover_question.svg' -import Tooltip from '../../shared/components/Tooltip' +import { Tooltip } from '../../shared/components/Tooltip' import './styles.scss' -import { useLanguage } from '../../shared/hooks' export interface PayStringHeaderProps { accountId: string @@ -12,8 +10,6 @@ export interface PayStringHeaderProps { export const PayStringHeader = ({ accountId }: PayStringHeaderProps) => { const [showToolTip, setShowToolTip] = useState(false) - const { t } = useTranslation() - const language = useLanguage() const questionRef = useRef(null) return (
@@ -36,9 +32,7 @@ export const PayStringHeader = ({ accountId }: PayStringHeaderProps) => {
{showToolTip && questionRef.current && ( { - const short = key.substr(0, 8) - return
{`${short}...`}
- }) - - return list - } - - renderValidatorTooltip() { - const { language } = this.props - const { v = {}, pubkey, time } = this.state - const key = v.master_key || pubkey - - return ( - <> -
{v.domain}
-
{key}
-
{localizeDate(time, language, DATE_OPTIONS)}
- {v.unl && ( -
- {v.unl} - {v.unl} -
- )} - - ) - } - - renderTxTooltip() { - const { type, result, account } = this.state - return ( - <> -
- -
-
{account}
- - ) - } - - renderMissingValidators() { - const { missing } = this.state - const { t } = this.props - const list = missing.map((d) => ( -
- {d.domain || d.master_key} -
- )) - - return ( - <> -
{t('missing')}:
- {list} - - ) - } - - renderNFTId() { - const { tokenId } = this.state - return
{tokenId}
- } - - renderPayStringToolTip() { - const { t } = this.props - - return ( - <> - - {t('paystring_explainer_blurb')} - - ) - } - - render() { - const { mode, x, y } = this.state - const style = { top: y + PADDING_Y, left: x } - let content = null - let className = 'tooltip' - if (mode === 'validator') { - content = this.renderValidatorTooltip() - } else if (mode === 'tx') { - content = this.renderTxTooltip() - } else if (mode === 'nUnl') { - content = this.renderNegativeUnlTooltip() - } else if (mode === 'missing') { - style.background = 'rgba(120,0,0,.9)' - content = this.renderMissingValidators() - } else if (mode === 'paystring') { - className += ' paystring' - content = this.renderPayStringToolTip() - } else if (mode === 'nftId') { - content = this.renderNFTId() - } - - return content ? ( -
this.setState({ mode: null })} - onKeyUp={() => this.setState({ mode: null })} - > - {content} -
- ) : null - } -} - -Tooltip.propTypes = { - t: PropTypes.func, - language: PropTypes.string, - data: PropTypes.shape({}), -} - -Tooltip.defaultProps = { - t: (d) => d, - language: undefined, - data: null, -} - -export default Tooltip diff --git a/src/containers/shared/components/Tooltip.tsx b/src/containers/shared/components/Tooltip.tsx new file mode 100644 index 000000000..6449b7e46 --- /dev/null +++ b/src/containers/shared/components/Tooltip.tsx @@ -0,0 +1,126 @@ +import { CSSProperties } from 'react' +import { useTranslation } from 'react-i18next' +import successIcon from '../images/success.png' +import { localizeDate } from '../utils' +import '../css/tooltip.scss' +import PayStringToolTip from '../images/paystring_tooltip.svg' +import { TxStatus } from './TxStatus' +import { TxLabel } from './TxLabel' +import { useLanguage } from '../hooks' + +const PADDING_Y = 20 +const DATE_OPTIONS = { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true, +} + +export interface TooltipInstance { + data?: any + mode: string + x: number + y: number +} + +export const Tooltip = ({ tooltip }: { tooltip?: TooltipInstance }) => { + const { t } = useTranslation() + const language = useLanguage() + + if (!tooltip) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <> + } + + const { data } = tooltip + + const renderNegativeUnlTooltip = () => { + const list = data.nUnl.map((key) => { + const short = key.substr(0, 8) + return
{`${short}...`}
+ }) + + return list + } + + const renderValidatorTooltip = () => { + const { v = {}, pubkey, time } = data + const key = v.master_key || pubkey + + return ( + <> +
{v.domain}
+
{key}
+
{localizeDate(time, language, DATE_OPTIONS)}
+ {v.unl && ( +
+ {v.unl} + {v.unl} +
+ )} + + ) + } + + const renderTxTooltip = () => { + const { type, result, account } = data + return ( + <> +
+ +
+
{account}
+ + ) + } + + const renderMissingValidators = () => { + const { missing } = data + const list = missing.map((d) => ( +
+ {d.domain || d.master_key} +
+ )) + + return ( + <> +
{t('missing')}:
+ {list} + + ) + } + + const renderNFTId = () => { + const { tokenId } = data + return
{tokenId}
+ } + + const renderPayStringToolTip = () => ( + <> + + {t('paystring_explainer_blurb')} + + ) + + const { x, y, mode } = tooltip + const style: CSSProperties = { top: y + PADDING_Y, left: x } + const modeMap = { + validator: renderValidatorTooltip, + tx: renderTxTooltip, + nUnl: renderNegativeUnlTooltip, + missing: renderMissingValidators, + paystring: renderPayStringToolTip, + nftId: renderNFTId, + } + + return modeMap[mode] ? ( +
+ {modeMap[mode]()} +
+ ) : null +} diff --git a/src/containers/shared/css/tooltip.scss b/src/containers/shared/css/tooltip.scss index 032bea9e8..a1c6d5e4b 100644 --- a/src/containers/shared/css/tooltip.scss +++ b/src/containers/shared/css/tooltip.scss @@ -13,7 +13,7 @@ img { height: 16px; - margin: 0px 5px; + margin: 0 5px; vertical-align: middle; } @@ -26,7 +26,7 @@ max-width: 100px; color: $black-10; font-size: 10px; - letter-spacing: 0px; + letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; } @@ -49,7 +49,7 @@ font-size: 10px; } - &.paystring { + &.tooltip-paystring { width: 274px; font-size: 12px; @@ -58,4 +58,8 @@ margin-bottom: 8px; } } + + &.tooltip-missing { + background: rgb(120 0 0 / 90%); + } } From b4a484548969b54d99168586b0aeba5b3d070cf6 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Tue, 16 Jan 2024 20:13:18 -0600 Subject: [PATCH 06/18] additional tooltip cleanup --- src/containers/Ledgers/LedgerMetrics.jsx | 5 ++- src/containers/Ledgers/Ledgers.jsx | 2 +- src/containers/Network/Hexagons.jsx | 2 -- src/containers/shared/components/Tooltip.tsx | 37 +++++++------------- 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/containers/Ledgers/LedgerMetrics.jsx b/src/containers/Ledgers/LedgerMetrics.jsx index af665879c..14309c09a 100644 --- a/src/containers/Ledgers/LedgerMetrics.jsx +++ b/src/containers/Ledgers/LedgerMetrics.jsx @@ -31,12 +31,11 @@ class LedgerMetrics extends Component { } showTooltip = (event) => { - const { nUnl } = this.state.data + const { data } = this.state this.setState({ tooltip: { - data: { nUnl }, + data: { nUnl: data.nUnl }, mode: 'nUnl', - v: nUnl, x: event.currentTarget.offsetLeft, y: event.currentTarget.offsetTop, }, diff --git a/src/containers/Ledgers/Ledgers.jsx b/src/containers/Ledgers/Ledgers.jsx index 19324ee08..e9f8bc7f9 100644 --- a/src/containers/Ledgers/Ledgers.jsx +++ b/src/containers/Ledgers/Ledgers.jsx @@ -248,7 +248,7 @@ class Ledgers extends Component { render() { const { ledgers, selected, tooltip } = this.state - const { t, language, isOnline } = this.props + const { isOnline } = this.props return (
{isOnline && ledgers.length > 0 ? ( diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index 4a84e46e2..d569406fd 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -5,7 +5,6 @@ import { useWindowSize } from 'usehooks-ts' import { hexbin } from 'd3-hexbin' import { Loader } from '../shared/components/Loader' import { Tooltip } from '../shared/components/Tooltip' -import { useLanguage } from '../shared/hooks' import './css/hexagons.scss' const MAX_WIDTH = 1200 @@ -50,7 +49,6 @@ const prepareHexagons = (data, list, height, radius, prev = []) => { } export const Hexagons = ({ list, data }) => { - const language = useLanguage() const { width } = useWindowSize() const [tooltip, setToolip] = useState() const [hexagons, setHexagons] = useState([]) diff --git a/src/containers/shared/components/Tooltip.tsx b/src/containers/shared/components/Tooltip.tsx index 6449b7e46..42fcb0e24 100644 --- a/src/containers/shared/components/Tooltip.tsx +++ b/src/containers/shared/components/Tooltip.tsx @@ -34,15 +34,12 @@ export const Tooltip = ({ tooltip }: { tooltip?: TooltipInstance }) => { const { data } = tooltip - const renderNegativeUnlTooltip = () => { - const list = data.nUnl.map((key) => { + const renderNegativeUnlTooltip = () => + data.nUnl.map((key) => { const short = key.substr(0, 8) return
{`${short}...`}
}) - return list - } - const renderValidatorTooltip = () => { const { v = {}, pubkey, time } = data const key = v.master_key || pubkey @@ -74,26 +71,18 @@ export const Tooltip = ({ tooltip }: { tooltip?: TooltipInstance }) => { ) } - const renderMissingValidators = () => { - const { missing } = data - const list = missing.map((d) => ( -
- {d.domain || d.master_key} -
- )) - - return ( - <> -
{t('missing')}:
- {list} - - ) - } + const renderMissingValidators = () => ( + <> +
{t('missing')}:
+ {data.missing.map((d) => ( +
+ {d.domain || d.master_key} +
+ ))} + + ) - const renderNFTId = () => { - const { tokenId } = data - return
{tokenId}
- } + const renderNFTId = () =>
{data.tokenId}
const renderPayStringToolTip = () => ( <> From 1b4439eedd874cd2d4d915b17f774043e973c6c2 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Thu, 18 Jan 2024 11:47:29 -0600 Subject: [PATCH 07/18] fix bug after merging tooltip PR --- src/containers/Ledgers/useTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/Ledgers/useTooltip.tsx b/src/containers/Ledgers/useTooltip.tsx index 3a0a32396..e107f79b1 100644 --- a/src/containers/Ledgers/useTooltip.tsx +++ b/src/containers/Ledgers/useTooltip.tsx @@ -31,7 +31,7 @@ export const TooltipProvider: FC = ({ children }) => { data: any, ) => { setTooltip({ - ...data, + data, mode, x: event.currentTarget.offsetLeft, y: event.currentTarget.offsetTop, From b12f8a594a7c99b06f60d662ce80d451e552ff90 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Thu, 18 Jan 2024 11:50:04 -0600 Subject: [PATCH 08/18] fix validator tool bug after merge --- src/containers/Ledgers/LedgerEntryValidator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index 4c38a85ad..1096e5deb 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -24,7 +24,7 @@ export const LedgerEntryValidator = ({ tabIndex={index} className={className} onMouseOver={(e) => - showTooltip('validator', e, { validator, v: validator }) + showTooltip('validator', e, { ...validator, v: validator }) } onFocus={() => {}} onKeyUp={() => {}} From 55e21837075673d4e1325b3395b204a6028b657a Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Thu, 18 Jan 2024 16:03:31 -0600 Subject: [PATCH 09/18] moved useTooltip to be along side Tooltip --- .../Ledgers/LedgerEntryHashTrustedCount.tsx | 2 +- src/containers/Ledgers/LedgerEntryTransaction.tsx | 2 +- src/containers/Ledgers/LedgerEntryValidator.tsx | 2 +- src/containers/Ledgers/LedgerMetrics.tsx | 3 +-- src/containers/Ledgers/Ledgers.tsx | 3 +-- src/containers/Ledgers/index.tsx | 2 +- src/containers/Network/Hexagons.jsx | 3 +-- .../PayStrings/PayStringHeader/index.tsx | 2 +- .../shared/components/{ => Tooltip}/Tooltip.tsx | 14 +++++++------- src/containers/shared/components/Tooltip/index.ts | 2 ++ .../components/Tooltip}/useTooltip.tsx | 0 11 files changed, 17 insertions(+), 18 deletions(-) rename src/containers/shared/components/{ => Tooltip}/Tooltip.tsx (89%) create mode 100644 src/containers/shared/components/Tooltip/index.ts rename src/containers/{Ledgers => shared/components/Tooltip}/useTooltip.tsx (100%) diff --git a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx index aad4fbaab..6a788176f 100644 --- a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx +++ b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { useTooltip } from './useTooltip' +import { useTooltip } from '../shared/components/Tooltip' import { Hash, ValidatorResponse } from './types' export const LedgerEntryHashTrustedCount = ({ diff --git a/src/containers/Ledgers/LedgerEntryTransaction.tsx b/src/containers/Ledgers/LedgerEntryTransaction.tsx index ee9f20f06..acf884051 100644 --- a/src/containers/Ledgers/LedgerEntryTransaction.tsx +++ b/src/containers/Ledgers/LedgerEntryTransaction.tsx @@ -3,7 +3,7 @@ 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 './useTooltip' +import { useTooltip } from '../shared/components/Tooltip' export const LedgerEntryTransaction = ({ transaction, diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index 1096e5deb..02d148ac5 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -1,5 +1,5 @@ import { useSelectedValidator } from './useSelectedValidator' -import { useTooltip } from './useTooltip' +import { useTooltip } from '../shared/components/Tooltip' export const LedgerEntryValidator = ({ validator, diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx index d320238c5..b1c10dec4 100644 --- a/src/containers/Ledgers/LedgerMetrics.tsx +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -1,11 +1,10 @@ import { useTranslation } from 'react-i18next' -import { Tooltip } from '../shared/components/Tooltip' +import { Tooltip, useTooltip } from '../shared/components/Tooltip' import { renderXRP } from '../shared/utils' import PauseIcon from '../shared/images/ic_pause.svg' import ResumeIcon from '../shared/images/ic_play.svg' import './css/ledgerMetrics.scss' import { useIsOnline } from '../shared/SocketContext' -import { useTooltip } from './useTooltip' import { useLanguage } from '../shared/hooks' const DEFAULTS = { diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index db24c8a01..263e91702 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -1,4 +1,4 @@ -import { Tooltip } from '../shared/components/Tooltip' +import { Tooltip, useTooltip } from '../shared/components/Tooltip' import './css/ledgers.scss' import DomainLink from '../shared/components/DomainLink' import { Loader } from '../shared/components/Loader' @@ -8,7 +8,6 @@ import { RouteLink } from '../shared/routing' import { VALIDATOR_ROUTE } from '../App/routes' import { LedgerListEntry } from './LedgerListEntry' import { useSelectedValidator } from './useSelectedValidator' -import { useTooltip } from './useTooltip' export const Ledgers = ({ // paused, diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index 3343a8ad4..17ad460b5 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -12,7 +12,7 @@ import { Ledger, ValidatorResponse } from './types' import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' import { useIsOnline } from '../shared/SocketContext' -import { TooltipProvider } from './useTooltip' +import { TooltipProvider } from '../shared/components/Tooltip' import { SelectedValidatorProvider } from './useSelectedValidator' const FETCH_INTERVAL_MILLIS = 5 * 60 * 1000 diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index d569406fd..db0eb2a2f 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -70,9 +70,8 @@ export const Hexagons = ({ list, data }) => { const showTooltip = (event, tooltipData) => { setToolip({ - ...tooltipData, + data: { ...tooltipData, v: list[tooltipData.pubkey] }, mode: 'validator', - v: list[tooltipData.pubkey], x: event.nativeEvent.offsetX, y: event.nativeEvent.offsetY, }) diff --git a/src/containers/PayStrings/PayStringHeader/index.tsx b/src/containers/PayStrings/PayStringHeader/index.tsx index 5a8e18c75..b5bc4ea38 100644 --- a/src/containers/PayStrings/PayStringHeader/index.tsx +++ b/src/containers/PayStrings/PayStringHeader/index.tsx @@ -1,7 +1,7 @@ import { useState, useRef } from 'react' import PayStringLogomark from '../../shared/images/PayString_Logomark.png' import QuestIcon from '../../shared/images/hover_question.svg' -import { Tooltip } from '../../shared/components/Tooltip' +import { Tooltip } from '../../shared/components/Tooltip/Tooltip' import './styles.scss' export interface PayStringHeaderProps { diff --git a/src/containers/shared/components/Tooltip.tsx b/src/containers/shared/components/Tooltip/Tooltip.tsx similarity index 89% rename from src/containers/shared/components/Tooltip.tsx rename to src/containers/shared/components/Tooltip/Tooltip.tsx index 42fcb0e24..48da1d52f 100644 --- a/src/containers/shared/components/Tooltip.tsx +++ b/src/containers/shared/components/Tooltip/Tooltip.tsx @@ -1,12 +1,12 @@ import { CSSProperties } from 'react' import { useTranslation } from 'react-i18next' -import successIcon from '../images/success.png' -import { localizeDate } from '../utils' -import '../css/tooltip.scss' -import PayStringToolTip from '../images/paystring_tooltip.svg' -import { TxStatus } from './TxStatus' -import { TxLabel } from './TxLabel' -import { useLanguage } from '../hooks' +import successIcon from '../../images/success.png' +import { localizeDate } from '../../utils' +import '../../css/tooltip.scss' +import PayStringToolTip from '../../images/paystring_tooltip.svg' +import { TxStatus } from '../TxStatus' +import { TxLabel } from '../TxLabel' +import { useLanguage } from '../../hooks' const PADDING_Y = 20 const DATE_OPTIONS = { diff --git a/src/containers/shared/components/Tooltip/index.ts b/src/containers/shared/components/Tooltip/index.ts new file mode 100644 index 000000000..a7dae4c91 --- /dev/null +++ b/src/containers/shared/components/Tooltip/index.ts @@ -0,0 +1,2 @@ +export * from './Tooltip' +export * from './useTooltip' diff --git a/src/containers/Ledgers/useTooltip.tsx b/src/containers/shared/components/Tooltip/useTooltip.tsx similarity index 100% rename from src/containers/Ledgers/useTooltip.tsx rename to src/containers/shared/components/Tooltip/useTooltip.tsx From 85f711328aaafa1147ed1a2bf52db211e8002911 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Thu, 18 Jan 2024 17:22:47 -0600 Subject: [PATCH 10/18] optimize through useMemo and fix validator hexagon tooltip. --- .../Ledgers/useSelectedValidator.tsx | 13 ++++++--- src/containers/Network/Hexagons.jsx | 21 ++++----------- src/containers/Network/Validators.tsx | 5 +++- .../shared/components/Tooltip/useTooltip.tsx | 27 ++++++++++++++----- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/containers/Ledgers/useSelectedValidator.tsx b/src/containers/Ledgers/useSelectedValidator.tsx index ef8042ee4..da3b31ab3 100644 --- a/src/containers/Ledgers/useSelectedValidator.tsx +++ b/src/containers/Ledgers/useSelectedValidator.tsx @@ -4,6 +4,7 @@ import { FC, SetStateAction, useContext, + useMemo, useState, } from 'react' @@ -22,10 +23,16 @@ export const SelectedValidatorContext = export const SelectedValidatorProvider: FC = ({ children }) => { const [selectedValidator, setSelectedValidator] = useState() + const selectedValidatorValues = useMemo( + () => ({ + selectedValidator, + setSelectedValidator, + }), + [selectedValidator], + ) + return ( - + {children} ) diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index db0eb2a2f..016cee431 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -4,7 +4,7 @@ import { useWindowSize } from 'usehooks-ts' import { hexbin } from 'd3-hexbin' import { Loader } from '../shared/components/Loader' -import { Tooltip } from '../shared/components/Tooltip' +import { Tooltip, useTooltip } from '../shared/components/Tooltip' import './css/hexagons.scss' const MAX_WIDTH = 1200 @@ -50,9 +50,9 @@ const prepareHexagons = (data, list, height, radius, prev = []) => { export const Hexagons = ({ list, data }) => { const { width } = useWindowSize() - const [tooltip, setToolip] = useState() const [hexagons, setHexagons] = useState([]) const { width: gridWidth, height: gridHeight, radius } = getDimensions(width) + const { tooltip, showTooltip, hideTooltip } = useTooltip() const bin = hexbin() .extent([ [0, 0], @@ -68,19 +68,6 @@ export const Hexagons = ({ list, data }) => { } }, [data, list, width, gridHeight, radius]) - const showTooltip = (event, tooltipData) => { - setToolip({ - data: { ...tooltipData, v: list[tooltipData.pubkey] }, - mode: 'validator', - x: event.nativeEvent.offsetX, - y: event.nativeEvent.offsetY, - }) - } - - const hideTooltip = () => { - setToolip(null) - } - const renderHexagon = (d, theHex) => { const { cookie, pubkey, ledger_hash: ledgerHash } = d const fill = `#${ledgerHash.substr(0, 6)}` @@ -90,7 +77,9 @@ export const Hexagons = ({ list, data }) => { key={`${pubkey}${cookie}${ledgerHash}`} transform={`translate(${d.x},${d.y})`} className="hexagon updated" - onMouseOver={(e) => showTooltip(e, d)} + onMouseOver={(e) => + showTooltip('validator', e, { ...d, v: list[d.pubkey] }) + } onFocus={() => {}} onMouseLeave={hideTooltip} > diff --git a/src/containers/Network/Validators.tsx b/src/containers/Network/Validators.tsx index 8d5c1fcd4..0f0ae5d8c 100644 --- a/src/containers/Network/Validators.tsx +++ b/src/containers/Network/Validators.tsx @@ -15,6 +15,7 @@ import { useLanguage } from '../shared/hooks' import { Hexagons } from './Hexagons' import { StreamValidator, ValidatorResponse } from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' +import { TooltipProvider } from '../shared/components/Tooltip' export const Validators = () => { const language = useLanguage() @@ -102,7 +103,9 @@ export const Validators = () => { )} { // @ts-ignore - Work around for complex type assignment issues - + + + }
{t('validators_found')}: diff --git a/src/containers/shared/components/Tooltip/useTooltip.tsx b/src/containers/shared/components/Tooltip/useTooltip.tsx index e107f79b1..05f5395a1 100644 --- a/src/containers/shared/components/Tooltip/useTooltip.tsx +++ b/src/containers/shared/components/Tooltip/useTooltip.tsx @@ -5,6 +5,7 @@ import { MouseEvent, SetStateAction, useContext, + useMemo, useState, } from 'react' @@ -12,7 +13,11 @@ export interface TooltipContextType { tooltip?: any setTooltip: Dispatch> hideTooltip: () => void - showTooltip: (mode: string, event: MouseEvent, data: any) => void + showTooltip: ( + mode: string, + event: MouseEvent | MouseEvent, + data: any, + ) => void } export const TooltipContext = createContext({ @@ -27,21 +32,29 @@ export const TooltipProvider: FC = ({ children }) => { const hideTooltip = () => setTooltip(undefined) const showTooltip = ( mode: string, - event: MouseEvent, + event: MouseEvent, data: any, ) => { setTooltip({ data, mode, - x: event.currentTarget.offsetLeft, - y: event.currentTarget.offsetTop, + x: event.currentTarget?.offsetLeft ?? event.nativeEvent.offsetX, + y: event.currentTarget?.offsetTop ?? event.nativeEvent.offsetY, }) } + const tooltipValues = useMemo( + () => ({ + tooltip, + setTooltip, + hideTooltip, + showTooltip, + }), + [tooltip], + ) + return ( - + {children} ) From 6f1ae86b8b73ea9d8e324c75900f1056d116510f Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Thu, 18 Jan 2024 17:25:02 -0600 Subject: [PATCH 11/18] fix hexagon tooltip --- src/containers/Network/Hexagons.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index d569406fd..db0eb2a2f 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -70,9 +70,8 @@ export const Hexagons = ({ list, data }) => { const showTooltip = (event, tooltipData) => { setToolip({ - ...tooltipData, + data: { ...tooltipData, v: list[tooltipData.pubkey] }, mode: 'validator', - v: list[tooltipData.pubkey], x: event.nativeEvent.offsetX, y: event.nativeEvent.offsetY, }) From f37f3571d6432318e469ace2d084e9c946ff1b12 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Fri, 19 Jan 2024 19:52:40 -0600 Subject: [PATCH 12/18] Add pausing logic back. --- src/containers/App/index.tsx | 2 +- src/containers/Ledgers/Ledgers.tsx | 8 ++++--- src/containers/Ledgers/index.tsx | 4 +--- .../Ledgers/test/LedgersPage.test.js | 4 ++-- .../shared/hooks/usePreviousWithPausing.tsx | 21 +++++++++++++++++++ 5 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 src/containers/shared/hooks/usePreviousWithPausing.tsx diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index 246627e07..29e96fa80 100644 --- a/src/containers/App/index.tsx +++ b/src/containers/App/index.tsx @@ -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' diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index 263e91702..e81960d3c 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -8,19 +8,21 @@ import { RouteLink } from '../shared/routing' import { VALIDATOR_ROUTE } from '../App/routes' import { LedgerListEntry } from './LedgerListEntry' import { useSelectedValidator } from './useSelectedValidator' +import { usePreviousWithPausing } from '../shared/hooks/usePreviousWithPausing' export const Ledgers = ({ - // paused, + paused, ledgers = [], unlCount, validators = {}, }: { - // paused: boolean + paused: boolean ledgers: any[] unlCount?: number validators: any }) => { const { selectedValidator } = useSelectedValidator() + const localLedgers = usePreviousWithPausing(ledgers, paused) const isOnline = useIsOnline() const { tooltip } = useTooltip() @@ -46,7 +48,7 @@ export const Ledgers = ({ )}
- {ledgers.map((ledger) => ( + {localLedgers?.map((ledger) => ( { +export const LedgersPage = () => { const { trackScreenLoaded } = useAnalytics() const [validators, setValidators] = useState< Record @@ -102,5 +102,3 @@ const LedgersPage = () => {
) } - -export default LedgersPage diff --git a/src/containers/Ledgers/test/LedgersPage.test.js b/src/containers/Ledgers/test/LedgersPage.test.js index e5440ac46..78a5a12d5 100644 --- a/src/containers/Ledgers/test/LedgersPage.test.js +++ b/src/containers/Ledgers/test/LedgersPage.test.js @@ -5,7 +5,7 @@ import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Provider } from 'react-redux' import i18n from '../../../i18n/testConfig' -import Ledgers from '../index' +import { LedgersPage } from '../index' import { initialState } from '../../../rootReducer' import SocketContext from '../../shared/SocketContext' import NetworkContext from '../../shared/NetworkContext' @@ -91,7 +91,7 @@ describe('Ledgers Page container', () => { - + diff --git a/src/containers/shared/hooks/usePreviousWithPausing.tsx b/src/containers/shared/hooks/usePreviousWithPausing.tsx new file mode 100644 index 000000000..b912a2775 --- /dev/null +++ b/src/containers/shared/hooks/usePreviousWithPausing.tsx @@ -0,0 +1,21 @@ +import { useLayoutEffect, useState } from 'react' + +/** + * A hook that prevents a value from being updated until it is unpaused. + * @param value - The value that is applied when paused is false + * @param paused - Should the value be updated + */ +export function usePreviousWithPausing( + value: T, + paused: boolean, +): T | undefined { + const [val, setVal] = useState() + + useLayoutEffect(() => { + if (!paused) { + setVal(value) + } + }, [paused, value]) // this code will run when the value of 'value' changes + + return val // in the end, return the current ref value. +} From 42e46af529e5f0a101f72e469f4d2d70790affc8 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Fri, 19 Jan 2024 20:14:29 -0600 Subject: [PATCH 13/18] Fix linting error and code to appease tsc --- src/containers/Ledgers/LedgerMetrics.tsx | 4 ++-- .../shared/components/Tooltip/useTooltip.tsx | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx index b1c10dec4..507359e5d 100644 --- a/src/containers/Ledgers/LedgerMetrics.tsx +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -18,7 +18,7 @@ const DEFAULTS = { } export const LedgerMetrics = ({ - data, + data: suppliedData, onPause, paused, }: { @@ -26,7 +26,7 @@ export const LedgerMetrics = ({ onPause: any paused: boolean }) => { - data = { ...DEFAULTS, ...data } + const data = { ...DEFAULTS, ...suppliedData } const { tooltip, showTooltip, hideTooltip } = useTooltip() const { t } = useTranslation() const isOnline = useIsOnline() diff --git a/src/containers/shared/components/Tooltip/useTooltip.tsx b/src/containers/shared/components/Tooltip/useTooltip.tsx index 05f5395a1..a386564e2 100644 --- a/src/containers/shared/components/Tooltip/useTooltip.tsx +++ b/src/containers/shared/components/Tooltip/useTooltip.tsx @@ -38,8 +38,14 @@ export const TooltipProvider: FC = ({ children }) => { setTooltip({ data, mode, - x: event.currentTarget?.offsetLeft ?? event.nativeEvent.offsetX, - y: event.currentTarget?.offsetTop ?? event.nativeEvent.offsetY, + x: + event.currentTarget instanceof HTMLElement + ? event.currentTarget.offsetLeft + : event.nativeEvent.offsetX, + y: + event.currentTarget instanceof HTMLElement + ? event.currentTarget.offsetTop + : event.nativeEvent.offsetY, }) } From 0ccd51f7f724f1c57299f0a68a13c003c55e2e54 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Mon, 22 Jan 2024 17:27:01 -0600 Subject: [PATCH 14/18] start using new useStreams --- src/containers/Ledgers/LedgerEntryHash.tsx | 93 ++--- .../Ledgers/LedgerEntryTransactions.tsx | 20 ++ .../Ledgers/LedgerEntryValidator.tsx | 17 +- src/containers/Ledgers/LedgerListEntry.tsx | 27 +- src/containers/Ledgers/Ledgers.tsx | 31 +- src/containers/Ledgers/index.tsx | 17 +- src/containers/shared/hooks/useStreams.tsx | 321 ++++++++++++++++++ 7 files changed, 438 insertions(+), 88 deletions(-) create mode 100644 src/containers/Ledgers/LedgerEntryTransactions.tsx create mode 100644 src/containers/shared/hooks/useStreams.tsx diff --git a/src/containers/Ledgers/LedgerEntryHash.tsx b/src/containers/Ledgers/LedgerEntryHash.tsx index d9be2a969..16306247b 100644 --- a/src/containers/Ledgers/LedgerEntryHash.tsx +++ b/src/containers/Ledgers/LedgerEntryHash.tsx @@ -1,52 +1,59 @@ import { useTranslation } from 'react-i18next' +import { memo } from 'react' 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 && - return ( -
-
-
-
{hash.hash.substr(0, 6)}
- {validated} -
-
-
-
{t('total')}:
- {hash.validations.length} +export const LedgerEntryHash = memo( + ({ + 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 && + return ( +
+
+
+
{hash.hash.substr(0, 6)}
+ {validated}
- -
-
- {hash.validations.map((validation, i) => ( - +
+
{t('total')}:
+ {hash.validations.length} +
+ - ))} +
+
+ {hash.validations.map((validation, i) => ( + + ))} +
-
- ) -} + ) + }, + (prevProps, nextProps) => + prevProps.unlCount === nextProps.unlCount && + Object.keys(prevProps.hash.validations || {}).length === + Object.keys(nextProps.hash.validations || {}).length, +) diff --git a/src/containers/Ledgers/LedgerEntryTransactions.tsx b/src/containers/Ledgers/LedgerEntryTransactions.tsx new file mode 100644 index 000000000..62f851b82 --- /dev/null +++ b/src/containers/Ledgers/LedgerEntryTransactions.tsx @@ -0,0 +1,20 @@ +import { memo } from 'react' +import { Loader } from '../shared/components/Loader' +import { LedgerEntryTransaction } from './LedgerEntryTransaction' + +export const LedgerEntryTransactions = memo( + ({ transactions }: { transactions: any[] }) => ( + <> + {transactions == null && } +
+ {transactions?.map((tx) => ( + + ))} +
+ + ), + (prevProps, nextProps) => + prevProps.transactions && + nextProps.transactions && + prevProps.transactions.length === nextProps.transactions.length, +) diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index 02d148ac5..cecc7f94c 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -13,13 +13,18 @@ export const LedgerEntryValidator = ({ const trusted = validator.unl ? 'trusted' : '' const unselected = selectedValidator ? 'unselected' : '' - const selected = selectedValidator === validator.pubkey ? 'selected' : '' - const className = `validation ${trusted} ${unselected} ${selected} ${validator.pubkey}` + const selected = + selectedValidator && + (selectedValidator === validator.master_key || + selectedValidator === validator.signing_key) + ? 'selected' + : '' + const className = `validation ${trusted} ${unselected} ${selected} ${validator.master_key}` const partial = validator.partial ?
: null return (
hideTooltip()} onClick={() => setSelectedValidator( - selectedValidator === validator.pubkey ? undefined : validator.pubkey, + selectedValidator && + (selectedValidator === validator.master_key || + selectedValidator === validator.signing_key) + ? undefined + : validator.master_key || validator.signing_key, ) } > diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index 72100af1e..1257cb413 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -1,11 +1,13 @@ import { useTranslation } from 'react-i18next' import { Loader } from '../shared/components/Loader' -import { Ledger, ValidatorResponse } from './types' +import { ValidatorResponse } from './types' import { RouteLink } from '../shared/routing' import { LEDGER_ROUTE } from '../App/routes' import { Amount } from '../shared/components/Amount' import { LedgerEntryTransaction } from './LedgerEntryTransaction' import { LedgerEntryHash } from './LedgerEntryHash' +import { Ledger } from '../shared/hooks/useStreams' +import { LedgerEntryTransactions } from './LedgerEntryTransactions' const SIGMA = '\u03A3' @@ -34,37 +36,32 @@ export const LedgerListEntry = ({ validators: { [pubkey: string]: ValidatorResponse } }) => { const { t } = useTranslation() - const time = ledger.close_time - ? new Date(ledger.close_time).toLocaleTimeString() + const time = ledger.closeTime + ? new Date(ledger.closeTime).toLocaleTimeString() : null const transactions = ledger.transactions || [] return ( -
+
- +
{time}
{/* Render Transaction Count (can be 0) */} - {ledger.txn_count !== undefined && ( + {ledger.txCount !== undefined && (
- {t('txn_count')}:{ledger.txn_count.toLocaleString()} + {t('txn_count')}:{ledger.txCount.toLocaleString()}
)} {/* Render Total Fees (can be 0) */} - {ledger.total_fees !== undefined && ( + {ledger.totalFees !== undefined && (
{SIGMA} {t('fees')}: - +
)} - {ledger.transactions == null && } -
- {transactions.map((tx) => ( - - ))} -
+
{ledger.hashes.map((hash) => ( diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index e81960d3c..b77f600af 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -9,26 +9,29 @@ import { VALIDATOR_ROUTE } from '../App/routes' import { LedgerListEntry } from './LedgerListEntry' import { useSelectedValidator } from './useSelectedValidator' import { usePreviousWithPausing } from '../shared/hooks/usePreviousWithPausing' +import { Ledger, useStreams } from '../shared/hooks/useStreams' export const Ledgers = ({ paused, - ledgers = [], unlCount, validators = {}, }: { paused: boolean - ledgers: any[] unlCount?: number validators: any }) => { const { selectedValidator } = useSelectedValidator() - const localLedgers = usePreviousWithPausing(ledgers, paused) + const { ledgers } = useStreams() + const localLedgers = usePreviousWithPausing>( + ledgers, + paused, + ) const isOnline = useIsOnline() const { tooltip } = useTooltip() return (
- {isOnline && ledgers.length > 0 ? ( + {isOnline && ledgers ? ( <>
@@ -48,14 +51,18 @@ export const Ledgers = ({ )}
- {localLedgers?.map((ledger) => ( - - ))}{' '} + {localLedgers && + Object.values(localLedgers) + .reverse() + .slice(0, 20) + ?.map((ledger) => ( + + ))}{' '}
{' '} diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index f496b51cb..68fc09421 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -5,15 +5,14 @@ import { useQuery } from 'react-query' import axios from 'axios' import Log from '../shared/log' import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils' -import Streams from '../shared/components/Streams' import { LedgerMetrics } from './LedgerMetrics' import { Ledgers } from './Ledgers' -import { Ledger, ValidatorResponse } from './types' +import { ValidatorResponse } from './types' import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' -import { useIsOnline } from '../shared/SocketContext' import { TooltipProvider } from '../shared/components/Tooltip' import { SelectedValidatorProvider } from './useSelectedValidator' +import { useStreams } from '../shared/hooks/useStreams' const FETCH_INTERVAL_MILLIS = 5 * 60 * 1000 @@ -22,11 +21,9 @@ export const LedgersPage = () => { const [validators, setValidators] = useState< Record >({}) - const [ledgers, setLedgers] = useState([]) + const { metrics } = useStreams() const [paused, setPaused] = useState(false) - const [metrics, setMetrics] = useState(undefined) const [unlCount, setUnlCount] = useState(undefined) - const { isOnline } = useIsOnline() const { t } = useTranslation() const network = useContext(NetworkContext) @@ -75,13 +72,6 @@ export const LedgersPage = () => { return (
- {isOnline && ( - - )} { + axios + .get('/api/v1/metrics') + .then((result) => result.data) + .catch((e) => Log.error(e)) + +// TODO: use useQuery +const fetchNegativeUNL = async (rippledSocket) => + getNegativeUNL(rippledSocket) + .then((data) => { + if (data === undefined) throw new Error('undefined nUNL') + + return data + }) + .catch((e) => { + Log.error(e) + return [] + }) + +// TODO: use useQuery +const fetchQuorum = async (rippledSocket) => + getQuorum(rippledSocket) + .then((data) => { + if (data === undefined) throw new Error('undefined quorum') + return data + }) + .catch((e) => Log.error(e)) + +export const useStreams = () => { + const [ledgers, setLedgers] = useState>([]) + const ledgersRef = useRef>(ledgers) + const firstLedgerRef = useRef(0) + const [validators, setValidators] = useState>({}) + const validationQueue = useRef([]) + const socket = useContext(SocketContext) + + // metrics + const [loadFee, setLoadFee] = useState('--') + const [txnSec, setTxnSec] = useState('--') + const [txnLedger, setTxnLedger] = useState('--') + const [ledgerInterval, setLedgerInterval] = useState('--') + const [avgFee, setAvgFee] = useState('--') + const [quorum, setQuorum] = useState('--') + const [nUnl, setNUnl] = useState([]) + + function addLedger(index: number | string) { + if (!firstLedgerRef.current) { + firstLedgerRef.current = Number(index) + } + if (firstLedgerRef.current > Number(index)) { + return + } + + // TODO: only keep 20 + if (!(index in ledgers)) { + setLedgers((previousLedgers) => ({ + [index]: { + index: Number(index), + seen: Date.now(), + hashes: [], + transactions: [], + }, + ...previousLedgers, + })) + } + } + + // TODO: use useQuery + function updateMetricsFromServer() { + fetchMetrics().then((serverMetrics) => { + setTxnSec(serverMetrics.txn_sec) + setTxnLedger(serverMetrics.txn_ledger) + setAvgFee(serverMetrics.avg_fee) + setLedgerInterval(serverMetrics.ledger_interval) + }) + } + + function updateMetrics() { + const ledgerChain = Object.values(ledgers) + .sort((a, b) => a.index - b.index) + .slice(-100) + + let time = 0 + let fees = 0 + let timeCount = 0 + let txCount = 0 + let txWithFeesCount = 0 + let ledgerCount = 0 + + ledgerChain.forEach((d, i) => { + const next = ledgerChain[i + 1] + if (next && next.seen && d.seen) { + time += next.seen - d.seen + timeCount += 1 + } + + if (d.totalFees) { + fees += d.totalFees + txWithFeesCount += d.txCount ?? 0 + } + if (d.txCount) { + txCount += d.txCount + } + ledgerCount += 1 + }) + + setTxnSec(time ? ((txCount / time) * 1000).toFixed(2) : '--') + setTxnLedger(ledgerCount ? (txCount / ledgerCount).toFixed(2) : '--') + setLedgerInterval(timeCount ? (time / timeCount / 1000).toFixed(3) : '--') + setAvgFee(txWithFeesCount ? (fees / txWithFeesCount).toPrecision(4) : '--') + } + + function updateQuorum() { + fetchQuorum(socket).then((newQuorum) => { + setQuorum(newQuorum) + }) + } + + function updateNegativeUNL() { + fetchNegativeUNL(socket).then((newNUnl) => { + setNUnl(newNUnl) + }) + } + + function onLedger(data: LedgerStream) { + if (!ledgersRef.current[data.ledger_index]) { + addLedger(data.ledger_index) + } + + if (process.env.VITE_ENVIRONMENT !== 'custom') { + updateMetricsFromServer() + } else { + updateMetrics() + } + + setLoadFee((data.fee_base / XRP_BASE).toString()) + if (data.ledger_index % 256 === 0 || quorum === '--') { + updateNegativeUNL() + updateQuorum() + } + + getLedger(socket, { ledger_hash: data.ledger_hash }) + .then(summarizeLedger) + .then((ledgerSummary) => { + setLedgers((previousLedgers) => { + Object.assign(previousLedgers[data.ledger_index] ?? {}, { + txCount: data.txn_count, + closeTime: convertRippleDate(data.ledger_time), + transactions: ledgerSummary.transactions, + totalFees: ledgerSummary.total_fees, // fix type + }) + const ledger = previousLedgers[Number(ledgerSummary.ledger_index)] + const matchingHashIndex = ledger?.hashes.findIndex( + (hash) => hash.hash === ledgerSummary.ledger_hash, + ) + const matchingHash = ledger?.hashes[matchingHashIndex] + if (matchingHash) { + matchingHash.unselected = false + matchingHash.validated = true + } + if (ledger && matchingHash) { + ledger.hashes[matchingHashIndex] = { + ...matchingHash, + } + } + + return { ...previousLedgers } + }) + }) + } + + const onValidation = (data: ValidationStream) => { + if (!ledgersRef.current[Number(data.ledger_index)]) { + addLedger(data.ledger_index) + } + if (firstLedgerRef.current <= Number(data.ledger_index)) { + validationQueue.current.push(data) + } + } + + function processValidationQueue() { + setTimeout(processValidationQueue, THROTTLE) + + if (validationQueue.current.length < 1) { + return + } + // copy the queue and clear it so we aren't adding more while processing + const queue = [...validationQueue.current] + validationQueue.current = [] + setLedgers((previousLedgers) => { + queue.forEach((validation) => { + const ledger = previousLedgers[Number(validation.ledger_index)] + const matchingHashIndex = ledger?.hashes.findIndex( + (hash) => hash.hash === validation.ledger_hash, + ) + let matchingHash = ledger?.hashes[matchingHashIndex] + if (!matchingHash) { + matchingHash = { + hash: validation.ledger_hash, + validated: false, + unselected: false, + validations: [], + time: convertRippleDate(validation.signing_time), + cookie: validation.cookie, + } + ledger.hashes.push(matchingHash) + } + matchingHash.validations = [...matchingHash.validations, validation] + if (ledger) { + ledger.hashes = [...(ledger?.hashes || [])] + ledger.hashes[matchingHashIndex] = { + ...matchingHash, + } + } + }) + return { ...previousLedgers } + }) + setValidators((previousValidators) => { + const newValidators: any = { ...previousValidators } + queue.forEach((validation) => { + newValidators[validation.validation_public_key] = { + ...previousValidators[validation.validation_public_key], + cookie: validation.cookie, + ledger_index: Number(validation.ledger_index), + ledger_hash: validation.ledger_hash, + pubkey: validation.validation_public_key, + partial: !validation.full, + time: convertRippleDate(validation.signing_time), + last: Date.now(), + } + }) + return newValidators + }) + } + + useEffect(() => { + const interval = setTimeout(processValidationQueue, THROTTLE) + + if (socket) { + socket.send({ + command: 'subscribe', + streams: ['ledger', 'validations'], + }) + socket.on('ledger', onLedger as any) + socket.on('validation', onValidation as any) + } + + return () => { + clearTimeout(interval) + if (socket) { + socket.send({ + command: 'unsubscribe', + streams: ['ledger', 'validations'], + }) + socket.off('ledger', onLedger) + socket.off('validation', onValidation) + } + } + }, [socket]) + + useEffect(() => { + ledgersRef.current = ledgers + }, [ledgers]) + + return { + ledgers, + validators, + metrics: { + load_fee: loadFee, + txn_sec: txnSec, + txn_ledger: txnLedger, + ledger_interval: ledgerInterval, + avg_fee: avgFee, + quorum, + nUnl, + }, + } +} From 82818362c5fe8ca1af067115a6d93db37a05a773 Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Tue, 23 Jan 2024 17:46:39 -0600 Subject: [PATCH 15/18] switch useStreams over to using a context to better share state --- src/containers/Ledgers/LedgerEntryHash.tsx | 8 +- .../Ledgers/LedgerEntryTransactions.tsx | 2 +- .../Ledgers/LedgerEntryValidator.tsx | 34 +++---- src/containers/Ledgers/LedgerListEntry.tsx | 7 +- src/containers/Ledgers/LedgerMetrics.tsx | 4 +- src/containers/Ledgers/Ledgers.tsx | 7 +- src/containers/Ledgers/index.tsx | 29 +++--- src/containers/helpers/contextFactory.ts | 24 +++++ .../components/Streams/StreamsContext.tsx | 13 +++ .../Streams/StreamsProvider.tsx} | 95 +++++++++---------- .../shared/components/Streams/types.ts | 30 ++++++ 11 files changed, 149 insertions(+), 104 deletions(-) create mode 100644 src/containers/helpers/contextFactory.ts create mode 100644 src/containers/shared/components/Streams/StreamsContext.tsx rename src/containers/shared/{hooks/useStreams.tsx => components/Streams/StreamsProvider.tsx} (78%) create mode 100644 src/containers/shared/components/Streams/types.ts diff --git a/src/containers/Ledgers/LedgerEntryHash.tsx b/src/containers/Ledgers/LedgerEntryHash.tsx index 16306247b..4f97d39da 100644 --- a/src/containers/Ledgers/LedgerEntryHash.tsx +++ b/src/containers/Ledgers/LedgerEntryHash.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next' import { memo } from 'react' import SuccessIcon from '../shared/images/success.svg' -import { LedgerEntryValidator } from './LedgerEntryValidator' +import { LedgerEntryValidation } from './LedgerEntryValidator' import { LedgerEntryHashTrustedCount } from './LedgerEntryHashTrustedCount' import { ValidatorResponse } from './types' @@ -42,10 +42,10 @@ export const LedgerEntryHash = memo(
{hash.validations.map((validation, i) => ( - ))}
diff --git a/src/containers/Ledgers/LedgerEntryTransactions.tsx b/src/containers/Ledgers/LedgerEntryTransactions.tsx index 62f851b82..6ce29e6b2 100644 --- a/src/containers/Ledgers/LedgerEntryTransactions.tsx +++ b/src/containers/Ledgers/LedgerEntryTransactions.tsx @@ -5,7 +5,7 @@ import { LedgerEntryTransaction } from './LedgerEntryTransaction' export const LedgerEntryTransactions = memo( ({ transactions }: { transactions: any[] }) => ( <> - {transactions == null && } + {!transactions && }
{transactions?.map((tx) => ( diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index cecc7f94c..80952bc4d 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -1,35 +1,30 @@ +import classNames from 'classnames' import { useSelectedValidator } from './useSelectedValidator' import { useTooltip } from '../shared/components/Tooltip' -export const LedgerEntryValidator = ({ - validator, +export const LedgerEntryValidation = ({ + validation, index, }: { - validator: any + validation: any index: number }) => { const { showTooltip, hideTooltip } = useTooltip() const { selectedValidator, setSelectedValidator } = useSelectedValidator() - - const trusted = validator.unl ? 'trusted' : '' - const unselected = selectedValidator ? 'unselected' : '' - const selected = - selectedValidator && - (selectedValidator === validator.master_key || - selectedValidator === validator.signing_key) - ? 'selected' - : '' - const className = `validation ${trusted} ${unselected} ${selected} ${validator.master_key}` - const partial = validator.partial ?
: null + const className = classNames( + 'validation', + validation.unl && 'trusted', + selectedValidator && 'unselected', + selectedValidator === validation.validation_public_key && 'selected', + ) return (
- showTooltip('validator', e, { ...validator, v: validator }) + showTooltip('validation', e, { ...validation, v: validation }) } onFocus={() => {}} onKeyUp={() => {}} @@ -37,14 +32,13 @@ export const LedgerEntryValidator = ({ onClick={() => setSelectedValidator( selectedValidator && - (selectedValidator === validator.master_key || - selectedValidator === validator.signing_key) + selectedValidator === validation.validation_public_key ? undefined - : validator.master_key || validator.signing_key, + : validation.validation_public_key, ) } > - {partial} + {validation.partial &&
}
) } diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index 1257cb413..7d29935e9 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -1,13 +1,11 @@ import { useTranslation } from 'react-i18next' -import { Loader } from '../shared/components/Loader' import { ValidatorResponse } from './types' import { RouteLink } from '../shared/routing' import { LEDGER_ROUTE } from '../App/routes' import { Amount } from '../shared/components/Amount' -import { LedgerEntryTransaction } from './LedgerEntryTransaction' import { LedgerEntryHash } from './LedgerEntryHash' -import { Ledger } from '../shared/hooks/useStreams' import { LedgerEntryTransactions } from './LedgerEntryTransactions' +import { Ledger } from '../shared/components/Streams/types' const SIGMA = '\u03A3' @@ -39,7 +37,6 @@ export const LedgerListEntry = ({ const time = ledger.closeTime ? new Date(ledger.closeTime).toLocaleTimeString() : null - const transactions = ledger.transactions || [] return (
@@ -61,7 +58,7 @@ export const LedgerListEntry = ({
)} - +
{ledger.hashes.map((hash) => ( diff --git a/src/containers/Ledgers/LedgerMetrics.tsx b/src/containers/Ledgers/LedgerMetrics.tsx index 507359e5d..83dd48fec 100644 --- a/src/containers/Ledgers/LedgerMetrics.tsx +++ b/src/containers/Ledgers/LedgerMetrics.tsx @@ -6,6 +6,7 @@ import ResumeIcon from '../shared/images/ic_play.svg' import './css/ledgerMetrics.scss' import { useIsOnline } from '../shared/SocketContext' import { useLanguage } from '../shared/hooks' +import { useStreams } from '../shared/components/Streams/StreamsContext' const DEFAULTS = { load_fee: '--', @@ -18,14 +19,13 @@ const DEFAULTS = { } export const LedgerMetrics = ({ - data: suppliedData, onPause, paused, }: { - data: any onPause: any paused: boolean }) => { + const { metrics: suppliedData } = useStreams() const data = { ...DEFAULTS, ...suppliedData } const { tooltip, showTooltip, hideTooltip } = useTooltip() const { t } = useTranslation() diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index b77f600af..3f25a9711 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -9,19 +9,18 @@ import { VALIDATOR_ROUTE } from '../App/routes' import { LedgerListEntry } from './LedgerListEntry' import { useSelectedValidator } from './useSelectedValidator' import { usePreviousWithPausing } from '../shared/hooks/usePreviousWithPausing' -import { Ledger, useStreams } from '../shared/hooks/useStreams' +import { useStreams } from '../shared/components/Streams/StreamsContext' +import { Ledger } from '../shared/components/Streams/types' export const Ledgers = ({ paused, unlCount, - validators = {}, }: { paused: boolean unlCount?: number - validators: any }) => { const { selectedValidator } = useSelectedValidator() - const { ledgers } = useStreams() + const { ledgers, validators } = useStreams() const localLedgers = usePreviousWithPausing>( ledgers, paused, diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index 68fc09421..7d91741f5 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -12,7 +12,7 @@ import { useAnalytics } from '../shared/analytics' import NetworkContext from '../shared/NetworkContext' import { TooltipProvider } from '../shared/components/Tooltip' import { SelectedValidatorProvider } from './useSelectedValidator' -import { useStreams } from '../shared/hooks/useStreams' +import { StreamsProvider } from '../shared/components/Streams/StreamsProvider' const FETCH_INTERVAL_MILLIS = 5 * 60 * 1000 @@ -21,7 +21,6 @@ export const LedgersPage = () => { const [validators, setValidators] = useState< Record >({}) - const { metrics } = useStreams() const [paused, setPaused] = useState(false) const [unlCount, setUnlCount] = useState(undefined) const { t } = useTranslation() @@ -72,22 +71,16 @@ export const LedgersPage = () => { return (
- - - pause()} - paused={paused} - /> - - - - - + + + + pause()} paused={paused} /> + + + + + +
) } diff --git a/src/containers/helpers/contextFactory.ts b/src/containers/helpers/contextFactory.ts new file mode 100644 index 000000000..fd9f1a80e --- /dev/null +++ b/src/containers/helpers/contextFactory.ts @@ -0,0 +1,24 @@ +import { createContext, useContext } from 'react' + +type NameProps = { + hook: string + provider: string +} + +export const contextFactory = ( + { hook, provider }: NameProps, + initialState?: T, +) => { + const Context = createContext(initialState) + + const useContextFactory = (): T => { + const context = useContext(Context) + + if (context === undefined) { + throw new Error(`${hook} must be used in a child of ${provider}`) + } + return context + } + + return [Context, useContextFactory] as const +} diff --git a/src/containers/shared/components/Streams/StreamsContext.tsx b/src/containers/shared/components/Streams/StreamsContext.tsx new file mode 100644 index 000000000..c83ecfb0d --- /dev/null +++ b/src/containers/shared/components/Streams/StreamsContext.tsx @@ -0,0 +1,13 @@ +import { contextFactory } from '../../../helpers/contextFactory' +import { Ledger, Metrics } from './types' + +const [StreamsContext, useStreams] = contextFactory<{ + metrics: Metrics + ledgers: Record + validators: Record +}>({ + hook: 'useStreams', + provider: 'StreamsProvider', +}) + +export { StreamsContext, useStreams } diff --git a/src/containers/shared/hooks/useStreams.tsx b/src/containers/shared/components/Streams/StreamsProvider.tsx similarity index 78% rename from src/containers/shared/hooks/useStreams.tsx rename to src/containers/shared/components/Streams/StreamsProvider.tsx index 9f18ea708..8f5203f07 100644 --- a/src/containers/shared/hooks/useStreams.tsx +++ b/src/containers/shared/components/Streams/StreamsProvider.tsx @@ -1,45 +1,18 @@ -import { useContext, useEffect, useRef, useState } from 'react' +import { FC, useContext, useEffect, useRef, useState } from 'react' import axios from 'axios' import type { LedgerStream, ValidationStream } from 'xrpl' -import SocketContext from '../SocketContext' -import { getLedger } from '../../../rippled/lib/rippled' -import { convertRippleDate } from '../../../rippled/lib/convertRippleDate' -import { summarizeLedger } from '../../../rippled/lib/summarizeLedger' -import Log from '../log' -import { getNegativeUNL, getQuorum } from '../../../rippled' -import { XRP_BASE } from '../transactionUtils' +import SocketContext from '../../SocketContext' +import { getLedger } from '../../../../rippled/lib/rippled' +import { convertRippleDate } from '../../../../rippled/lib/convertRippleDate' +import { summarizeLedger } from '../../../../rippled/lib/summarizeLedger' +import Log from '../../log' +import { getNegativeUNL, getQuorum } from '../../../../rippled' +import { XRP_BASE } from '../../transactionUtils' +import { StreamsContext } from './StreamsContext' +import { Ledger } from './types' const THROTTLE = 150 -export interface LedgerHash { - hash: string - validated: boolean - validations: ValidationStream[] - unselected: boolean - time: number - cookie?: string -} - -export interface Ledger { - transactions: any[] - index: number - hashes: LedgerHash[] - seen: number - txCount?: number - closeTime: number - totalFees: number -} - -export interface Metrics { - load_fee: string - txn_sec: string - txn_ledger: string - ledger_interval: string - avg_fee: string - quorum: string - nUnl: string[] -} - // TODO: use useQuery const fetchMetrics = () => axios @@ -69,7 +42,7 @@ const fetchQuorum = async (rippledSocket) => }) .catch((e) => Log.error(e)) -export const useStreams = () => { +export const StreamsProvider: FC = ({ children }) => { const [ledgers, setLedgers] = useState>([]) const ledgersRef = useRef>(ledgers) const firstLedgerRef = useRef(0) @@ -87,6 +60,7 @@ export const useStreams = () => { const [nUnl, setNUnl] = useState([]) function addLedger(index: number | string) { + // Only add new ledgers that are newer than the last one added. if (!firstLedgerRef.current) { firstLedgerRef.current = Number(index) } @@ -101,7 +75,8 @@ export const useStreams = () => { index: Number(index), seen: Date.now(), hashes: [], - transactions: [], + transactions: undefined, + txCount: undefined, }, ...previousLedgers, })) @@ -167,38 +142,49 @@ export const useStreams = () => { function onLedger(data: LedgerStream) { if (!ledgersRef.current[data.ledger_index]) { + // The ledger closed, but we did not have an existing entry likely because the page just loaded and its + // validations came in before we connected to the websocket. addLedger(data.ledger_index) } if (process.env.VITE_ENVIRONMENT !== 'custom') { + // In custom mode we populate metrics from ledgers loaded into memory updateMetricsFromServer() } else { + // Make call to metrics tracked on the backend updateMetrics() } setLoadFee((data.fee_base / XRP_BASE).toString()) + + // After each flag ledger we should check the UNL and quorum which are correlated and can only update every flag ledger. if (data.ledger_index % 256 === 0 || quorum === '--') { updateNegativeUNL() updateQuorum() } + // TODO: Set fields before getting full ledger info + // set validated hash + // set closetime + getLedger(socket, { ledger_hash: data.ledger_hash }) .then(summarizeLedger) .then((ledgerSummary) => { setLedgers((previousLedgers) => { - Object.assign(previousLedgers[data.ledger_index] ?? {}, { - txCount: data.txn_count, - closeTime: convertRippleDate(data.ledger_time), - transactions: ledgerSummary.transactions, - totalFees: ledgerSummary.total_fees, // fix type - }) - const ledger = previousLedgers[Number(ledgerSummary.ledger_index)] + const ledger = Object.assign( + previousLedgers[data.ledger_index] ?? {}, + { + txCount: ledgerSummary.transactions.length, + closeTime: convertRippleDate(data.ledger_time), + transactions: ledgerSummary.transactions, + totalFees: ledgerSummary.total_fees, // fix type + }, + ) const matchingHashIndex = ledger?.hashes.findIndex( (hash) => hash.hash === ledgerSummary.ledger_hash, ) const matchingHash = ledger?.hashes[matchingHashIndex] if (matchingHash) { - matchingHash.unselected = false matchingHash.validated = true } if (ledger && matchingHash) { @@ -207,6 +193,9 @@ export const useStreams = () => { } } + // eslint-disable-next-line no-param-reassign + previousLedgers[data.ledger_index] = ledger + return { ...previousLedgers } }) }) @@ -221,13 +210,14 @@ export const useStreams = () => { } } + // Process validations in chunks to make re-renders more manageable. function processValidationQueue() { setTimeout(processValidationQueue, THROTTLE) if (validationQueue.current.length < 1) { return } - // copy the queue and clear it so we aren't adding more while processing + // copy the queue and clear it, so we aren't adding more while processing const queue = [...validationQueue.current] validationQueue.current = [] setLedgers((previousLedgers) => { @@ -241,7 +231,6 @@ export const useStreams = () => { matchingHash = { hash: validation.ledger_hash, validated: false, - unselected: false, validations: [], time: convertRippleDate(validation.signing_time), cookie: validation.cookie, @@ -305,7 +294,9 @@ export const useStreams = () => { ledgersRef.current = ledgers }, [ledgers]) - return { + // TODO: retrieve most recent ledger to populate the screen + + const value = { ledgers, validators, metrics: { @@ -318,4 +309,8 @@ export const useStreams = () => { nUnl, }, } + + return ( + {children} + ) } diff --git a/src/containers/shared/components/Streams/types.ts b/src/containers/shared/components/Streams/types.ts new file mode 100644 index 000000000..f71634d41 --- /dev/null +++ b/src/containers/shared/components/Streams/types.ts @@ -0,0 +1,30 @@ +import { ValidationStream } from 'xrpl' + +export interface LedgerHash { + hash: string + validated: boolean + validations: ValidationStream[] + time: number + cookie?: string +} + +export interface Ledger { + transactions: any[] + index: number + hashes: LedgerHash[] + seen: number + txCount?: number + closeTime: number + totalFees: number +} + +export interface Metrics { + load_fee: string + txn_sec: string + txn_ledger: string + ledger_interval: string + avg_fee: string + quorum: string + nUnl: string[] + base_fee?: string +} From 1e61d0b076ed9ee744bb668d3151c3b26d4fd5ac Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Wed, 24 Jan 2024 13:30:39 -0600 Subject: [PATCH 16/18] request most recent validated ledger on page load to reduce time screen is empty --- src/containers/Ledgers/css/ledgers.scss | 7 +- .../components/Streams/StreamsProvider.tsx | 70 ++++++++++--------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/containers/Ledgers/css/ledgers.scss b/src/containers/Ledgers/css/ledgers.scss index b1e8f8047..d330c96f9 100644 --- a/src/containers/Ledgers/css/ledgers.scss +++ b/src/containers/Ledgers/css/ledgers.scss @@ -128,7 +128,6 @@ $ledger-width: 196px; min-height: 170px; padding: $ledgers-margin-large; border: $ledgers-border; - border-bottom: 0; color: $black-40; font-size: 10px; line-height: 12px; @@ -228,7 +227,7 @@ $ledger-width: 196px; .hash { overflow: hidden; - padding: 0px 32px 32px; + padding: 0 32px 32px; border: 1px solid $black-60; border-top: 0; background: rgba($black-80, 0.7); @@ -236,6 +235,10 @@ $ledger-width: 196px; font-size: 15px; text-align: left; + &:last-child:not(:first-child) { + border-bottom: 0; + } + .bar { height: 2px; margin: 0px -32px; diff --git a/src/containers/shared/components/Streams/StreamsProvider.tsx b/src/containers/shared/components/Streams/StreamsProvider.tsx index 8f5203f07..918221278 100644 --- a/src/containers/shared/components/Streams/StreamsProvider.tsx +++ b/src/containers/shared/components/Streams/StreamsProvider.tsx @@ -167,38 +167,41 @@ export const StreamsProvider: FC = ({ children }) => { // set validated hash // set closetime - getLedger(socket, { ledger_hash: data.ledger_hash }) - .then(summarizeLedger) - .then((ledgerSummary) => { - setLedgers((previousLedgers) => { - const ledger = Object.assign( - previousLedgers[data.ledger_index] ?? {}, - { - txCount: ledgerSummary.transactions.length, - closeTime: convertRippleDate(data.ledger_time), - transactions: ledgerSummary.transactions, - totalFees: ledgerSummary.total_fees, // fix type - }, - ) - const matchingHashIndex = ledger?.hashes.findIndex( - (hash) => hash.hash === ledgerSummary.ledger_hash, - ) - const matchingHash = ledger?.hashes[matchingHashIndex] - if (matchingHash) { - matchingHash.validated = true - } - if (ledger && matchingHash) { - ledger.hashes[matchingHashIndex] = { - ...matchingHash, - } - } + getLedger(socket, { ledger_hash: data.ledger_hash }).then( + populateFromLedgerResponse, + ) + } - // eslint-disable-next-line no-param-reassign - previousLedgers[data.ledger_index] = ledger + const populateFromLedgerResponse = (ledger: Promise) => { + const ledgerSummary = summarizeLedger(ledger) + setLedgers((previousLedgers) => { + const ledger = Object.assign( + previousLedgers[ledgerSummary.ledger_index] ?? {}, + { + txCount: ledgerSummary.transactions.length, + closeTime: convertRippleDate(ledgerSummary.ledger_time), + transactions: ledgerSummary.transactions, + totalFees: ledgerSummary.total_fees, // fix type + }, + ) + const matchingHashIndex = ledger?.hashes.findIndex( + (hash) => hash.hash === ledgerSummary.ledger_hash, + ) + const matchingHash = ledger?.hashes[matchingHashIndex] + if (matchingHash) { + matchingHash.validated = true + } + if (ledger && matchingHash) { + ledger.hashes[matchingHashIndex] = { + ...matchingHash, + } + } - return { ...previousLedgers } - }) - }) + // eslint-disable-next-line no-param-reassign + previousLedgers[ledgerSummary.ledger_index] = ledger + + return { ...previousLedgers } + }) } const onValidation = (data: ValidationStream) => { @@ -275,6 +278,11 @@ export const StreamsProvider: FC = ({ children }) => { }) socket.on('ledger', onLedger as any) socket.on('validation', onValidation as any) + + // Load in the most recent validated ledger to prevent the page from being empty until the next validations come in. + getLedger(socket, { ledger_index: 'current' }).then( + populateFromLedgerResponse, + ) } return () => { @@ -294,8 +302,6 @@ export const StreamsProvider: FC = ({ children }) => { ledgersRef.current = ledgers }, [ledgers]) - // TODO: retrieve most recent ledger to populate the screen - const value = { ledgers, validators, From 3736aaf0d60533c533aa3d4cc60ea908d462a27d Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Wed, 24 Jan 2024 16:32:31 -0600 Subject: [PATCH 17/18] refactor network page to use `useStreams` --- src/containers/Network/Hexagons.jsx | 13 +- src/containers/Network/Validators.tsx | 124 +++++++++--------- src/containers/Network/index.tsx | 17 ++- .../components/Streams/StreamsProvider.tsx | 2 +- 4 files changed, 87 insertions(+), 69 deletions(-) diff --git a/src/containers/Network/Hexagons.jsx b/src/containers/Network/Hexagons.jsx index 016cee431..68c5b2c14 100644 --- a/src/containers/Network/Hexagons.jsx +++ b/src/containers/Network/Hexagons.jsx @@ -63,7 +63,13 @@ export const Hexagons = ({ list, data }) => { useEffect(() => { if (width > 0) { setHexagons((prevHexagons) => - prepareHexagons(data, list, gridHeight, radius, prevHexagons), + prepareHexagons( + Object.values(data), + list, + gridHeight, + radius, + prevHexagons, + ), ) } }, [data, list, width, gridHeight, radius]) @@ -116,8 +122,3 @@ export const Hexagons = ({ list, data }) => {
) } - -Hexagons.propTypes = { - list: PropTypes.shape({}).isRequired, - data: PropTypes.arrayOf(PropTypes.shape({})).isRequired, // eslint-disable-line -} diff --git a/src/containers/Network/Validators.tsx b/src/containers/Network/Validators.tsx index 0f0ae5d8c..8926eaa88 100644 --- a/src/containers/Network/Validators.tsx +++ b/src/containers/Network/Validators.tsx @@ -1,9 +1,8 @@ -import { useContext, useState } from 'react' +import { useContext, useMemo, useState } from 'react' import axios from 'axios' import { useTranslation } from 'react-i18next' import { useQuery } from 'react-query' import NetworkTabs from './NetworkTabs' -import Streams from '../shared/components/Streams' import ValidatorsTable from './ValidatorsTable' import Log from '../shared/log' import { @@ -16,44 +15,27 @@ import { Hexagons } from './Hexagons' import { StreamValidator, ValidatorResponse } from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' import { TooltipProvider } from '../shared/components/Tooltip' +import { useStreams } from '../shared/components/Streams/StreamsContext' export const Validators = () => { const language = useLanguage() const { t } = useTranslation() - const [vList, setVList] = useState>({}) - const [validations, setValidations] = useState([]) - const [metrics, setMetrics] = useState({}) + const { validators: validatorsFromValidations, metrics } = useStreams() const [unlCount, setUnlCount] = useState(0) const network = useContext(NetworkContext) - useQuery(['fetchValidatorsData'], () => fetchData(), { - refetchInterval: (returnedData, _) => - returnedData == null - ? FETCH_INTERVAL_ERROR_MILLIS - : FETCH_INTERVAL_MILLIS, - refetchOnMount: true, - enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, - }) - - function mergeLatest( - validators: Record, - live: Record, - ): Record { - const updated: Record = {} - const keys = new Set(Object.keys(validators).concat(Object.keys(live))) - keys.forEach((d: string) => { - const newData: StreamValidator = validators[d] || live[d] - if (newData.ledger_index == null && live[d] && live[d].ledger_index) { - // VHS uses `current_index` instead of `ledger_index` - // If `ledger_index` isn't defined, then we're still using the VHS data, - // instead of the Streams data - newData.ledger_index = live[d].ledger_index - newData.ledger_hash = live[d].ledger_hash - } - updated[d] = newData - }) - return updated - } + const { data: validatorsFromVHS } = useQuery( + ['fetchValidatorsData'], + () => fetchData(), + { + refetchInterval: (returnedData, _) => + returnedData == null + ? FETCH_INTERVAL_ERROR_MILLIS + : FETCH_INTERVAL_MILLIS, + refetchOnMount: true, + enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, + }, + ) function fetchData() { const url = `${process.env.VITE_DATA_URL}/validators/${network}` @@ -67,44 +49,66 @@ export const Validators = () => { newValidatorList[v.signing_key] = v }) - setVList(() => mergeLatest(newValidatorList, vList)) setUnlCount(validators.filter((d: any) => Boolean(d.unl)).length) - return true // indicating success in getting the data + return newValidatorList }) .catch((e) => Log.error(e)) } - const updateValidators = (newValidations: StreamValidator[]) => { - // @ts-ignore - Work around type assignment for complex validation data types - setValidations(newValidations) - setVList((value) => { - const newValidatorsList: Record = { ...value } - newValidations.forEach((validation: any) => { - newValidatorsList[validation.pubkey] = { - ...value[validation.pubkey], - signing_key: validation.pubkey, - ledger_index: validation.ledger_index, - ledger_hash: validation.ledger_hash, - } - }) - return mergeLatest(newValidatorsList, value) + const merged = useMemo(() => { + if ( + !validatorsFromVHS || + !( + validatorsFromValidations && + Object.keys(validatorsFromValidations).length + ) + ) { + return + } + const updated: Record = {} + const keys = new Set( + Object.keys(validatorsFromVHS).concat( + Object.keys(validatorsFromValidations), + ), + ) + keys.forEach((d: string) => { + const newData: StreamValidator = + validatorsFromVHS[d] || validatorsFromValidations[d] + if ( + newData.ledger_index == null && + validatorsFromValidations[d] && + validatorsFromValidations[d].ledger_index + ) { + // VHS uses `current_index` instead of `ledger_index` + // If `ledger_index` isn't defined, then we're still using the VHS data, + // instead of the Streams data + newData.ledger_index = validatorsFromValidations[d].ledger_index + } + if (newData.current_index == null) { + newData.signing_key = validatorsFromValidations[d].pubkey + } + // latest hash and time comes from the validations stream + if (validatorsFromValidations[d]) { + newData.time = validatorsFromValidations[d].time + newData.ledger_hash = validatorsFromValidations[d].ledger_hash + } + + updated[d] = newData }) - } + return Object.values(updated) + }, [validatorsFromVHS, validatorsFromValidations]) + + const validatorCount = useMemo( + () => merged && Object.keys(merged).length, + [merged], + ) - const validatorCount = Object.keys(vList).length return (
- {network && ( - - )} { // @ts-ignore - Work around for complex type assignment issues - + }
@@ -121,7 +125,7 @@ export const Validators = () => {
- +
) diff --git a/src/containers/Network/index.tsx b/src/containers/Network/index.tsx index 70888bc9d..f5db28a52 100644 --- a/src/containers/Network/index.tsx +++ b/src/containers/Network/index.tsx @@ -10,6 +10,19 @@ import { UpgradeStatus } from './UpgradeStatus' import { Nodes } from './Nodes' import NoMatch from '../NoMatch' import './css/style.scss' +import { StreamsProvider } from '../shared/components/Streams/StreamsProvider' + +export const ValidatorsPage = () => ( + + + +) + +export const UpgradeStatusPage = () => ( + + + +) export const Network = () => { const { trackScreenLoaded } = useAnalytics() @@ -32,8 +45,8 @@ export const Network = () => { } const Body = { - 'upgrade-status': UpgradeStatus, - validators: Validators, + 'upgrade-status': UpgradeStatusPage, + validators: ValidatorsPage, nodes: Nodes, }[tab] return ( diff --git a/src/containers/shared/components/Streams/StreamsProvider.tsx b/src/containers/shared/components/Streams/StreamsProvider.tsx index 918221278..a15e7862f 100644 --- a/src/containers/shared/components/Streams/StreamsProvider.tsx +++ b/src/containers/shared/components/Streams/StreamsProvider.tsx @@ -280,7 +280,7 @@ export const StreamsProvider: FC = ({ children }) => { socket.on('validation', onValidation as any) // Load in the most recent validated ledger to prevent the page from being empty until the next validations come in. - getLedger(socket, { ledger_index: 'current' }).then( + getLedger(socket, { ledger_index: 'validated' }).then( populateFromLedgerResponse, ) } From 1dc969088d3b5daacc86f5dc21df33c5d480415a Mon Sep 17 00:00:00 2001 From: Caleb Kniffen Date: Mon, 29 Jan 2024 13:20:16 -0600 Subject: [PATCH 18/18] move getting validators from VHS to a hook and move tooltip provider location --- src/containers/Ledgers/LedgerEntryHash.tsx | 82 +++++++------------ .../Ledgers/LedgerEntryHashTrustedCount.tsx | 61 +++++++------- .../Ledgers/LedgerEntryValidator.tsx | 9 +- src/containers/Ledgers/LedgerListEntry.tsx | 31 ++++--- src/containers/Ledgers/Ledgers.tsx | 27 +++--- src/containers/Ledgers/css/ledgers.scss | 7 +- src/containers/Ledgers/index.tsx | 67 +++------------ src/containers/Network/Validators.tsx | 53 ++---------- src/containers/Network/index.tsx | 9 +- .../components/Streams/StreamsProvider.tsx | 2 +- .../shared/components/Tooltip/Tooltip.tsx | 12 ++- .../VHSValidators/VHSValidatorsContext.tsx | 10 +++ .../VHSValidators/VHSValidatorsProvider.tsx | 62 ++++++++++++++ .../shared/components/VHSValidators/types.ts | 6 ++ 14 files changed, 212 insertions(+), 226 deletions(-) create mode 100644 src/containers/shared/components/VHSValidators/VHSValidatorsContext.tsx create mode 100644 src/containers/shared/components/VHSValidators/VHSValidatorsProvider.tsx create mode 100644 src/containers/shared/components/VHSValidators/types.ts diff --git a/src/containers/Ledgers/LedgerEntryHash.tsx b/src/containers/Ledgers/LedgerEntryHash.tsx index 4f97d39da..e9ae1fb4e 100644 --- a/src/containers/Ledgers/LedgerEntryHash.tsx +++ b/src/containers/Ledgers/LedgerEntryHash.tsx @@ -1,59 +1,39 @@ import { useTranslation } from 'react-i18next' -import { memo } from 'react' import SuccessIcon from '../shared/images/success.svg' import { LedgerEntryValidation } from './LedgerEntryValidator' import { LedgerEntryHashTrustedCount } from './LedgerEntryHashTrustedCount' -import { ValidatorResponse } from './types' -export const LedgerEntryHash = memo( - ({ - 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 && - return ( -
-
-
-
{hash.hash.substr(0, 6)}
- {validated} +export const LedgerEntryHash = ({ hash }: { hash: any }) => { + const { t } = useTranslation() + const shortHash = hash.hash.substr(0, 6) + const barStyle = { background: `#${shortHash}` } + const validated = hash.validated && + return ( +
+
+
+
{hash.hash.substr(0, 6)}
+ {validated} +
+
+
+
{t('total')}:
+ {hash.validations.length}
-
-
-
{t('total')}:
- {hash.validations.length} -
- +
+
+ {hash.validations.map((validation, i) => ( + -
-
- {hash.validations.map((validation, i) => ( - - ))} -
+ ))}
- ) - }, - (prevProps, nextProps) => - prevProps.unlCount === nextProps.unlCount && - Object.keys(prevProps.hash.validations || {}).length === - Object.keys(nextProps.hash.validations || {}).length, -) +
+ ) +} diff --git a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx index 6a788176f..c2016419f 100644 --- a/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx +++ b/src/containers/Ledgers/LedgerEntryHashTrustedCount.tsx @@ -1,56 +1,59 @@ import { useTranslation } from 'react-i18next' +import classNames from 'classnames' +import { useMemo } from 'react' +import { ValidationStream } from 'xrpl' import { useTooltip } from '../shared/components/Tooltip' -import { Hash, ValidatorResponse } from './types' +import { useVHSValidators } from '../shared/components/VHSValidators/VHSValidatorsContext' export const LedgerEntryHashTrustedCount = ({ - hash, - unlCount, - validators, + validations, }: { - hash: Hash - unlCount?: number - validators: { [pubkey: string]: ValidatorResponse } + validations: ValidationStream[] }) => { const { t } = useTranslation() const { hideTooltip, showTooltip } = useTooltip() - const className = hash.trusted_count < (unlCount || 0) ? 'missed' : '' + const { unl, validators } = useVHSValidators() - const getMissingValidators = () => { - const unl = {} - - Object.keys(validators).forEach((pubkey) => { - if (validators[pubkey].unl) { - unl[pubkey] = false + const status = useMemo(() => { + const missing = [...(unl || [])] + validations.forEach((v) => { + if (!validators?.[v.validation_public_key]) { + return } - }) - hash.validations.forEach((v) => { - if (unl[v.pubkey] !== undefined) { - delete unl[v.pubkey] + const missingIndex = missing.findIndex( + (assumedMissing) => assumedMissing === v.validation_public_key, + ) + if (missingIndex !== -1) { + missing.splice(missingIndex, 1) } }) - return Object.keys(unl).map((pubkey) => validators[pubkey]) - } - - const missing = - hash.trusted_count && className === 'missed' ? getMissingValidators() : null + return { + missing: missing.map((v) => validators?.[v]), + trustedCount: (unl?.length || 0) - missing.length, + } + }, [unl, validations]) - return hash.trusted_count ? ( + return status.trustedCount ? ( - missing && missing.length && showTooltip('missing', e, { missing }) - } + className={classNames( + status.trustedCount < status.missing.length && 'missed', + )} + onMouseMove={(e) => { + const { missing } = status + + missing.length && showTooltip('missing', e, { missing }) + }} onFocus={() => {}} onKeyUp={() => {}} onMouseLeave={() => hideTooltip()} >
{t('unl')}:
- {hash.trusted_count}/{unlCount} + {status.trustedCount}/{unl?.length}
) : null diff --git a/src/containers/Ledgers/LedgerEntryValidator.tsx b/src/containers/Ledgers/LedgerEntryValidator.tsx index 80952bc4d..c6a7c25f5 100644 --- a/src/containers/Ledgers/LedgerEntryValidator.tsx +++ b/src/containers/Ledgers/LedgerEntryValidator.tsx @@ -1,6 +1,7 @@ import classNames from 'classnames' import { useSelectedValidator } from './useSelectedValidator' import { useTooltip } from '../shared/components/Tooltip' +import { useVHSValidators } from '../shared/components/VHSValidators/VHSValidatorsContext' export const LedgerEntryValidation = ({ validation, @@ -11,9 +12,10 @@ export const LedgerEntryValidation = ({ }) => { const { showTooltip, hideTooltip } = useTooltip() const { selectedValidator, setSelectedValidator } = useSelectedValidator() + const { validators } = useVHSValidators() const className = classNames( 'validation', - validation.unl && 'trusted', + validators?.[validation.validation_public_key]?.unl && 'trusted', selectedValidator && 'unselected', selectedValidator === validation.validation_public_key && 'selected', ) @@ -24,7 +26,10 @@ export const LedgerEntryValidation = ({ tabIndex={index} className={className} onMouseOver={(e) => - showTooltip('validation', e, { ...validation, v: validation }) + showTooltip('validator', e, { + ...validation, + v: validators?.[validation.validation_public_key], + }) } onFocus={() => {}} onKeyUp={() => {}} diff --git a/src/containers/Ledgers/LedgerListEntry.tsx b/src/containers/Ledgers/LedgerListEntry.tsx index 7d29935e9..1424e1dc0 100644 --- a/src/containers/Ledgers/LedgerListEntry.tsx +++ b/src/containers/Ledgers/LedgerListEntry.tsx @@ -1,11 +1,15 @@ import { useTranslation } from 'react-i18next' -import { 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 { Ledger } from '../shared/components/Streams/types' +import { + Tooltip, + TooltipProvider, + useTooltip, +} from '../shared/components/Tooltip' const SIGMA = '\u03A3' @@ -24,15 +28,8 @@ const LedgerIndex = ({ ledgerIndex }: { ledgerIndex: number }) => { ) } -export const LedgerListEntry = ({ - ledger, - unlCount, - validators, -}: { - ledger: Ledger - unlCount?: number - validators: { [pubkey: string]: ValidatorResponse } -}) => { +export const LedgerListEntryInner = ({ ledger }: { ledger: Ledger }) => { + const { tooltip } = useTooltip() const { t } = useTranslation() const time = ledger.closeTime ? new Date(ledger.closeTime).toLocaleTimeString() @@ -62,14 +59,16 @@ export const LedgerListEntry = ({
{ledger.hashes.map((hash) => ( - + ))}
+
) } + +export const LedgerListEntry = ({ ledger }: { ledger: Ledger }) => ( + + + +) diff --git a/src/containers/Ledgers/Ledgers.tsx b/src/containers/Ledgers/Ledgers.tsx index 3f25a9711..153a1e47e 100644 --- a/src/containers/Ledgers/Ledgers.tsx +++ b/src/containers/Ledgers/Ledgers.tsx @@ -11,16 +11,12 @@ import { useSelectedValidator } from './useSelectedValidator' import { usePreviousWithPausing } from '../shared/hooks/usePreviousWithPausing' import { useStreams } from '../shared/components/Streams/StreamsContext' import { Ledger } from '../shared/components/Streams/types' +import { useVHSValidators } from '../shared/components/VHSValidators/VHSValidatorsContext' -export const Ledgers = ({ - paused, - unlCount, -}: { - paused: boolean - unlCount?: number -}) => { +export const Ledgers = ({ paused }: { paused: boolean }) => { + const { validators: validatorsFromVHS } = useVHSValidators() const { selectedValidator } = useSelectedValidator() - const { ledgers, validators } = useStreams() + const { ledgers } = useStreams() const localLedgers = usePreviousWithPausing>( ledgers, paused, @@ -34,10 +30,12 @@ export const Ledgers = ({ <>
- {selectedValidator && ( + {selectedValidator && validatorsFromVHS && (
- {validators[selectedValidator].domain && ( - + {validatorsFromVHS[selectedValidator].domain && ( + )} ( - + ))}{' '}
{' '} diff --git a/src/containers/Ledgers/css/ledgers.scss b/src/containers/Ledgers/css/ledgers.scss index d330c96f9..470f2bd08 100644 --- a/src/containers/Ledgers/css/ledgers.scss +++ b/src/containers/Ledgers/css/ledgers.scss @@ -98,11 +98,10 @@ $ledger-width: 196px; } .ledger { - overflow: visible; width: $ledger-width; flex-grow: 0; flex-shrink: 0; - margin-left: $ledgers-margin-large; + margin-right: $ledgers-margin-large; animation-duration: 0.4s; animation-name: ledger-enter; white-space: normal; @@ -114,12 +113,10 @@ $ledger-width: 196px; @keyframes ledger-enter { from { - width: 0; - margin-left: -$ledgers-margin-large; + margin-left: -$ledger-width; } to { - width: $ledger-width; margin-left: 0; } } diff --git a/src/containers/Ledgers/index.tsx b/src/containers/Ledgers/index.tsx index 7d91741f5..7ef7c2699 100644 --- a/src/containers/Ledgers/index.tsx +++ b/src/containers/Ledgers/index.tsx @@ -1,30 +1,18 @@ -import { useContext, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Helmet } from 'react-helmet-async' import { useTranslation } from 'react-i18next' -import { useQuery } from 'react-query' -import axios from 'axios' -import Log from '../shared/log' -import { FETCH_INTERVAL_ERROR_MILLIS } from '../shared/utils' import { LedgerMetrics } from './LedgerMetrics' import { Ledgers } from './Ledgers' -import { ValidatorResponse } from './types' import { useAnalytics } from '../shared/analytics' -import NetworkContext from '../shared/NetworkContext' import { TooltipProvider } from '../shared/components/Tooltip' import { SelectedValidatorProvider } from './useSelectedValidator' import { StreamsProvider } from '../shared/components/Streams/StreamsProvider' - -const FETCH_INTERVAL_MILLIS = 5 * 60 * 1000 +import { VHSValidatorsProvider } from '../shared/components/VHSValidators/VHSValidatorsProvider' export const LedgersPage = () => { const { trackScreenLoaded } = useAnalytics() - const [validators, setValidators] = useState< - Record - >({}) const [paused, setPaused] = useState(false) - const [unlCount, setUnlCount] = useState(undefined) const { t } = useTranslation() - const network = useContext(NetworkContext) useEffect(() => { trackScreenLoaded() @@ -33,53 +21,22 @@ export const LedgersPage = () => { } }, [trackScreenLoaded]) - const fetchValidators = () => { - const url = `${process.env.VITE_DATA_URL}/validators/${network}` - - return axios - .get(url) - .then((resp) => resp.data.validators) - .then((validatorResponse) => { - const newValidators: Record = {} - let newUnlCount = 0 - - validatorResponse.forEach((v: ValidatorResponse) => { - if (v.unl === process.env.VITE_VALIDATOR) { - newUnlCount += 1 - } - newValidators[v.signing_key] = v - }) - - setValidators(newValidators) - setUnlCount(newUnlCount) - return true - }) - .catch((e) => Log.error(e)) - } - - useQuery(['fetchValidatorData'], async () => fetchValidators(), { - refetchInterval: (returnedData, _) => - returnedData == null - ? FETCH_INTERVAL_ERROR_MILLIS - : FETCH_INTERVAL_MILLIS, - refetchOnMount: true, - enabled: !!network, - }) - const pause = () => setPaused(!paused) return (
- - - pause()} paused={paused} /> - - - - - + + + + pause()} paused={paused} /> + + + + + +
) diff --git a/src/containers/Network/Validators.tsx b/src/containers/Network/Validators.tsx index 8926eaa88..da9f62ebe 100644 --- a/src/containers/Network/Validators.tsx +++ b/src/containers/Network/Validators.tsx @@ -1,59 +1,20 @@ -import { useContext, useMemo, useState } from 'react' -import axios from 'axios' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useQuery } from 'react-query' import NetworkTabs from './NetworkTabs' import ValidatorsTable from './ValidatorsTable' -import Log from '../shared/log' -import { - localizeNumber, - FETCH_INTERVAL_MILLIS, - FETCH_INTERVAL_ERROR_MILLIS, -} from '../shared/utils' +import { localizeNumber } from '../shared/utils' import { useLanguage } from '../shared/hooks' import { Hexagons } from './Hexagons' -import { StreamValidator, ValidatorResponse } from '../shared/vhsTypes' -import NetworkContext from '../shared/NetworkContext' +import { StreamValidator } from '../shared/vhsTypes' import { TooltipProvider } from '../shared/components/Tooltip' import { useStreams } from '../shared/components/Streams/StreamsContext' +import { useVHSValidators } from '../shared/components/VHSValidators/VHSValidatorsContext' export const Validators = () => { const language = useLanguage() const { t } = useTranslation() const { validators: validatorsFromValidations, metrics } = useStreams() - const [unlCount, setUnlCount] = useState(0) - const network = useContext(NetworkContext) - - const { data: validatorsFromVHS } = useQuery( - ['fetchValidatorsData'], - () => fetchData(), - { - refetchInterval: (returnedData, _) => - returnedData == null - ? FETCH_INTERVAL_ERROR_MILLIS - : FETCH_INTERVAL_MILLIS, - refetchOnMount: true, - enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, - }, - ) - - function fetchData() { - const url = `${process.env.VITE_DATA_URL}/validators/${network}` - - return axios - .get(url) - .then((resp) => resp.data.validators) - .then((validators) => { - const newValidatorList: Record = {} - validators.forEach((v: ValidatorResponse) => { - newValidatorList[v.signing_key] = v - }) - - setUnlCount(validators.filter((d: any) => Boolean(d.unl)).length) - return newValidatorList - }) - .catch((e) => Log.error(e)) - } + const { validators: validatorsFromVHS, unl } = useVHSValidators() const merged = useMemo(() => { if ( @@ -115,10 +76,10 @@ export const Validators = () => { {t('validators_found')}: {localizeNumber(validatorCount, language)} - {unlCount !== 0 && ( + {unl?.length !== 0 && ( {' '} - ({t('unl')}: {unlCount}) + ({t('unl')}: {unl?.length}) )} diff --git a/src/containers/Network/index.tsx b/src/containers/Network/index.tsx index f5db28a52..dace43f78 100644 --- a/src/containers/Network/index.tsx +++ b/src/containers/Network/index.tsx @@ -11,16 +11,21 @@ import { Nodes } from './Nodes' import NoMatch from '../NoMatch' import './css/style.scss' import { StreamsProvider } from '../shared/components/Streams/StreamsProvider' +import { VHSValidatorsProvider } from '../shared/components/VHSValidators/VHSValidatorsProvider' export const ValidatorsPage = () => ( - + + + ) export const UpgradeStatusPage = () => ( - + + + ) diff --git a/src/containers/shared/components/Streams/StreamsProvider.tsx b/src/containers/shared/components/Streams/StreamsProvider.tsx index a15e7862f..325c09ca5 100644 --- a/src/containers/shared/components/Streams/StreamsProvider.tsx +++ b/src/containers/shared/components/Streams/StreamsProvider.tsx @@ -11,7 +11,7 @@ import { XRP_BASE } from '../../transactionUtils' import { StreamsContext } from './StreamsContext' import { Ledger } from './types' -const THROTTLE = 150 +const THROTTLE = 200 // TODO: use useQuery const fetchMetrics = () => diff --git a/src/containers/shared/components/Tooltip/Tooltip.tsx b/src/containers/shared/components/Tooltip/Tooltip.tsx index 48da1d52f..340cda0db 100644 --- a/src/containers/shared/components/Tooltip/Tooltip.tsx +++ b/src/containers/shared/components/Tooltip/Tooltip.tsx @@ -7,6 +7,7 @@ import PayStringToolTip from '../../images/paystring_tooltip.svg' import { TxStatus } from '../TxStatus' import { TxLabel } from '../TxLabel' import { useLanguage } from '../../hooks' +import { convertRippleDate } from '../../../../rippled/lib/convertRippleDate' const PADDING_Y = 20 const DATE_OPTIONS = { @@ -41,14 +42,21 @@ export const Tooltip = ({ tooltip }: { tooltip?: TooltipInstance }) => { }) const renderValidatorTooltip = () => { - const { v = {}, pubkey, time } = data + // eslint-disable-next-line camelcase + const { v = {}, pubkey, signing_time } = data const key = v.master_key || pubkey return ( <>
{v.domain}
{key}
-
{localizeDate(time, language, DATE_OPTIONS)}
+
+ {localizeDate( + convertRippleDate(signing_time), + language, + DATE_OPTIONS, + )} +
{v.unl && (
{v.unl} diff --git a/src/containers/shared/components/VHSValidators/VHSValidatorsContext.tsx b/src/containers/shared/components/VHSValidators/VHSValidatorsContext.tsx new file mode 100644 index 000000000..28621beb3 --- /dev/null +++ b/src/containers/shared/components/VHSValidators/VHSValidatorsContext.tsx @@ -0,0 +1,10 @@ +import { contextFactory } from '../../../helpers/contextFactory' +import { VHSValidatorsHookResult } from './types' + +const [VHSValidatorsContext, useVHSValidators] = + contextFactory({ + hook: 'useVHSValidators', + provider: 'VHSValidatorsProvider', + }) + +export { VHSValidatorsContext, useVHSValidators } diff --git a/src/containers/shared/components/VHSValidators/VHSValidatorsProvider.tsx b/src/containers/shared/components/VHSValidators/VHSValidatorsProvider.tsx new file mode 100644 index 000000000..1c9c00d03 --- /dev/null +++ b/src/containers/shared/components/VHSValidators/VHSValidatorsProvider.tsx @@ -0,0 +1,62 @@ +import { FC, useContext } from 'react' +import { useQuery } from 'react-query' +import axios from 'axios' +import { VHSValidatorsContext } from './VHSValidatorsContext' +import { FETCH_INTERVAL_ERROR_MILLIS, FETCH_INTERVAL_MILLIS } from '../../utils' +import { ValidatorResponse } from '../../vhsTypes' +import Log from '../../log' +import NetworkContext from '../../NetworkContext' +import { VHSValidatorsHookResult } from './types' + +export const VHSValidatorsProvider: FC = ({ children }) => { + const network = useContext(NetworkContext) + + const { data: value } = useQuery( + ['fetchValidatorsData'], + () => fetchVHSData(), + { + refetchInterval: 0, + refetchOnMount: true, + enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, + initialData: { + unl: undefined, + validators: undefined, + }, + }, + ) + + function fetchVHSData(): Promise { + const url = `${process.env.VITE_DATA_URL}/validators/${network}` + + return axios + .get(url) + .then((resp) => resp.data.validators) + .then((validators) => { + const newValidatorList: Record = {} + validators.forEach((v: ValidatorResponse) => { + newValidatorList[v.signing_key] = v + }) + + return { + validators: newValidatorList, + unl: validators + .filter((d: ValidatorResponse) => Boolean(d.unl)) + .map((d: ValidatorResponse) => d.signing_key), + } + }) + .catch((e) => { + Log.error(e) + + return { + unl: undefined, + validators: undefined, + } + }) + } + + return ( + + {children} + + ) +} diff --git a/src/containers/shared/components/VHSValidators/types.ts b/src/containers/shared/components/VHSValidators/types.ts new file mode 100644 index 000000000..d6f231144 --- /dev/null +++ b/src/containers/shared/components/VHSValidators/types.ts @@ -0,0 +1,6 @@ +import { ValidatorResponse } from '../../vhsTypes' + +export interface VHSValidatorsHookResult { + validators?: Record + unl?: string[] +}