diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index 02d1367b3..9021ce774 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -560,5 +560,6 @@ "search_results_banner": null, "enable_amendment_name": null, "amendment_status": null, - "expected_date": null + "expected_date": null, + "base": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index 83fb30d98..255937b68 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -560,6 +560,6 @@ "search_results_banner": "Token search by name and account is now available! Try searching for USD", "enable_amendment_name": "Amendment Name", "amendment_status": "Amendment Status", - "expected_date": "Expected Date" - + "expected_date": "Expected Date", + "base": "Base" } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index 17965bd0a..5881acbb1 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -556,5 +556,6 @@ "search_results_banner": null, "enable_amendment_name": null, "amendment_status": null, - "expected_date": null + "expected_date": null, + "base": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index 33ac67e2d..ec12bae08 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -557,5 +557,6 @@ "search_results_banner": null, "enable_amendment_name": null, "amendment_status": null, - "expected_date": null + "expected_date": null, + "base": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index e5b667cac..4a87b7016 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -556,5 +556,6 @@ "search_results_banner": null, "enable_amendment_name": null, "amendment_status": null, - "expected_date": null + "expected_date": null, + "base": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index 0f5406903..3b8977435 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -554,5 +554,6 @@ "search_results_banner": null, "enable_amendment_name": null, "amendment_status": null, - "expected_date": null + "expected_date": null, + "base": null } diff --git a/src/containers/App/index.tsx b/src/containers/App/index.tsx index 1b5734800..0f3c8bf7d 100644 --- a/src/containers/App/index.tsx +++ b/src/containers/App/index.tsx @@ -17,7 +17,6 @@ import { ACCOUNT_ROUTE, LEDGER_ROUTE, LEDGERS_ROUTE, - NETWORK_ROUTE, NFT_ROUTE, PAYSTRING_ROUTE, TOKEN_ROUTE, @@ -26,12 +25,14 @@ import { AMENDMENTS_ROUTE, AMENDMENT_ROUTE, MPT_ROUTE, + NODES_ROUTE, + VALIDATORS_ROUTE, + UPGRADE_STATUS_ROUTE, } from './routes' import { LedgersPage as Ledgers } from '../Ledgers' import { Ledger } from '../Ledger' import { AccountsRouter } from '../Accounts/AccountsRouter' import { Transaction } from '../Transactions' -import { Network } from '../Network' import { Validator } from '../Validators' import { PayString } from '../PayStrings' import Token from '../Token' @@ -41,6 +42,9 @@ import { useCustomNetworks } from '../shared/hooks' import { Amendments } from '../Amendments' import { Amendment } from '../Amendment' import { MPT } from '../MPT/MPT' +import { Nodes } from '../Network/Nodes' +import { Validators } from '../Network/Validators' +import { UpgradeStatus } from '../Network/UpgradeStatus' export const AppWrapper = () => { const mode = process.env.VITE_ENVIRONMENT @@ -67,7 +71,9 @@ export const AppWrapper = () => { [LEDGER_ROUTE, Ledger], [ACCOUNT_ROUTE, AccountsRouter], [TRANSACTION_ROUTE, Transaction], - [NETWORK_ROUTE, Network], + [NODES_ROUTE, Nodes], + [VALIDATORS_ROUTE, Validators], + [UPGRADE_STATUS_ROUTE, UpgradeStatus], [AMENDMENTS_ROUTE, Amendments], [VALIDATOR_ROUTE, Validator], [PAYSTRING_ROUTE, PayString], diff --git a/src/containers/App/navigation.ts b/src/containers/App/navigation.ts index b53994af8..360527a1c 100644 --- a/src/containers/App/navigation.ts +++ b/src/containers/App/navigation.ts @@ -3,13 +3,16 @@ import { NavigationMenuAnyRoute } from '../Header/NavigationMenu' import { AMENDMENTS_ROUTE, LEDGERS_ROUTE, - NETWORK_ROUTE, - VALIDATOR_ROUTE, + NODES_ROUTE, + UPGRADE_STATUS_ROUTE, + VALIDATORS_ROUTE, } from './routes' const isNetwork = (path) => - path.indexOf(buildPath(NETWORK_ROUTE, {})) === 0 || - path.indexOf(buildPath(VALIDATOR_ROUTE, { identifier: '' })) === 0 + path.indexOf(buildPath(VALIDATORS_ROUTE, {})) === 0 || + path.indexOf(buildPath(NODES_ROUTE, {})) === 0 || + path.indexOf(buildPath(UPGRADE_STATUS_ROUTE, {})) === 0 || + path.indexOf(buildPath(AMENDMENTS_ROUTE, {})) === 0 // NOTE: for submenus, remove `path` field and add `children` array of objects export const navigationConfig: NavigationMenuAnyRoute[] = [ @@ -19,24 +22,20 @@ export const navigationConfig: NavigationMenuAnyRoute[] = [ current: (path: string) => !isNetwork(path), }, { - route: NETWORK_ROUTE, title: 'network', current: (path: string) => isNetwork(path), children: [ { - route: NETWORK_ROUTE, + route: NODES_ROUTE, title: 'nodes', - params: { tab: 'nodes' }, }, { - route: NETWORK_ROUTE, + route: VALIDATORS_ROUTE, title: 'validators', - params: { tab: 'validators' }, }, { - route: NETWORK_ROUTE, + route: UPGRADE_STATUS_ROUTE, title: 'upgrade_status', - params: { tab: 'upgrade-status' }, }, { route: AMENDMENTS_ROUTE, diff --git a/src/containers/App/routes.ts b/src/containers/App/routes.ts index b94707689..04bd2e993 100644 --- a/src/containers/App/routes.ts +++ b/src/containers/App/routes.ts @@ -18,10 +18,18 @@ export const LEDGER_ROUTE: RouteDefinition<{ path: `/ledgers/:identifier`, } -export const NETWORK_ROUTE: RouteDefinition<{ - tab?: 'nodes' | 'validators' | 'upgrade-status' +export const NODES_ROUTE: RouteDefinition = { + path: '/network/nodes', +} + +export const VALIDATORS_ROUTE: RouteDefinition<{ + tab?: 'uptime' | 'voting' }> = { - path: '/network/:tab?', + path: '/network/validators/:tab?', +} + +export const UPGRADE_STATUS_ROUTE: RouteDefinition = { + path: '/network/upgrade-status', } export const NFT_ROUTE: RouteDefinition<{ diff --git a/src/containers/Network/NetworkTabs.tsx b/src/containers/Network/NetworkTabs.tsx deleted file mode 100644 index f5ff1968c..000000000 --- a/src/containers/Network/NetworkTabs.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Tabs } from '../shared/components/Tabs' -import { buildPath } from '../shared/routing' -import { NETWORK_ROUTE } from '../App/routes' - -interface Props { - selected: string -} - -const NetworkTabs = (props: Props) => { - const { selected } = props - const tabs = ['nodes', 'validators', 'upgrade-status'] - return ( - - ) -} - -export default NetworkTabs diff --git a/src/containers/Network/Nodes.tsx b/src/containers/Network/Nodes.tsx index efa48ecaa..403021053 100644 --- a/src/containers/Network/Nodes.tsx +++ b/src/containers/Network/Nodes.tsx @@ -2,7 +2,6 @@ import { useContext } from 'react' import axios from 'axios' import { useTranslation } from 'react-i18next' import { useQuery } from 'react-query' -import NetworkTabs from './NetworkTabs' import { Map } from './Map' import { NodesTable } from './NodesTable' import Log from '../shared/log' @@ -15,6 +14,7 @@ import { import { useLanguage } from '../shared/hooks' import { NodeData, NodeResponse } from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' +import './css/style.scss' export const ledgerCompare = (a: NodeData, b: NodeData) => { const aLedger = a.validated_ledger.ledger_index @@ -78,6 +78,7 @@ export const Nodes = () => { return (
+
{t('nodes')}
{ // @ts-ignore - Work around for complex type assignment issues @@ -99,7 +100,6 @@ export const Nodes = () => { )}
-
diff --git a/src/containers/Network/UpgradeStatus.tsx b/src/containers/Network/UpgradeStatus.tsx index cdd599790..b5a6d77c4 100644 --- a/src/containers/Network/UpgradeStatus.tsx +++ b/src/containers/Network/UpgradeStatus.tsx @@ -1,28 +1,19 @@ import { useState, useContext } from 'react' -import { useTranslation } from 'react-i18next' import axios from 'axios' import { useQuery } from 'react-query' +import { useTranslation } from 'react-i18next' import BarChartVersion from './BarChartVersion' -import NetworkTabs from './NetworkTabs' -import Streams from '../shared/components/Streams' -import { Hexagons } from './Hexagons' import { FETCH_INTERVAL_MILLIS, FETCH_INTERVAL_ERROR_MILLIS, - localizeNumber, isEarlierVersion, } from '../shared/utils' -import { useLanguage } from '../shared/hooks' import Log from '../shared/log' -import { - NodeData, - NodeResponse, - StreamValidator, - ValidatorResponse, -} from '../shared/vhsTypes' +import { NodeData, NodeResponse, ValidatorResponse } from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' import { ledgerCompare } from './Nodes' import { Loader } from '../shared/components/Loader' +import './css/style.scss' interface NodeStats { nodePercent: number @@ -152,14 +143,10 @@ const handleNodeVersion = (version: string | undefined) => { } export const UpgradeStatus = () => { - const [vList, setVList] = useState>({}) - const [validations, setValidations] = useState([]) - const [unlCount, setUnlCount] = useState(0) + const { t } = useTranslation() const [validatorAggregation, setValidatorAggregation] = useState({}) const [nodeAggregation, setNodeAggregation] = useState({}) - const { t } = useTranslation() - const language = useLanguage() const network = useContext(NetworkContext) useQuery( @@ -200,10 +187,6 @@ export const UpgradeStatus = () => { validators.forEach((validator) => { newValidatorList[validator.signing_key] = validator }) - setVList(newValidatorList) - setUnlCount( - validators.filter((validator) => Boolean(validator.unl)).length, - ) setValidatorAggregation(aggregateValidators(validators)) return Object.values(newValidatorList) }) @@ -254,48 +237,10 @@ export const UpgradeStatus = () => { ) } - const updateValidators = (newValidations: StreamValidator[]) => { - setValidations(newValidations) - setVList((validatorList) => { - const newValidatorsList: Record = { - ...validatorList, - } - newValidations.forEach((validation) => { - if (validation.pubkey) { - newValidatorsList[validation.pubkey] = { - ...validatorList[validation.pubkey], - ledger_index: validation.ledger_index, - ledger_hash: validation.ledger_hash, - } - } - }) - return newValidatorsList - }) - } - - const validatorCount = Object.keys(vList).length - return (
- - { - // @ts-ignore - Work around for complex type assignment issues - - } -
- {t('validators_found')}: - - {localizeNumber(validatorCount, language)} - {unlCount !== 0 && ( - - {' '} - ({t('unl')}: {unlCount}) - - )} - -
+
{t('upgrade_status')}
- {Object.keys(validatorAggregation).length > 0 || Object.keys(nodeAggregation).length > 0 ? (
diff --git a/src/containers/Network/Validators.tsx b/src/containers/Network/Validators.tsx index 0f0ae5d8c..16590f02b 100644 --- a/src/containers/Network/Validators.tsx +++ b/src/containers/Network/Validators.tsx @@ -2,20 +2,31 @@ import { useContext, useState } from 'react' import axios from 'axios' import { useTranslation } from 'react-i18next' import { useQuery } from 'react-query' -import NetworkTabs from './NetworkTabs' +import { Helmet } from 'react-helmet-async' import Streams from '../shared/components/Streams' -import ValidatorsTable from './ValidatorsTable' +import { ValidatorsTable } from './ValidatorsTable' import Log from '../shared/log' import { localizeNumber, FETCH_INTERVAL_MILLIS, FETCH_INTERVAL_ERROR_MILLIS, + FETCH_INTERVAL_FEE_SETTINGS_MILLIS, } from '../shared/utils' import { useLanguage } from '../shared/hooks' import { Hexagons } from './Hexagons' -import { StreamValidator, ValidatorResponse } from '../shared/vhsTypes' +import { + FeeSettings, + StreamValidator, + ValidatorResponse, +} from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' import { TooltipProvider } from '../shared/components/Tooltip' +import './css/style.scss' +import { VALIDATORS_ROUTE } from '../App/routes' +import { useRouteParams } from '../shared/routing' +import ValidatorsTabs from './ValidatorsTabs' +import SocketContext from '../shared/SocketContext' +import { getServerState } from '../../rippled/lib/rippled' export const Validators = () => { const language = useLanguage() @@ -24,7 +35,12 @@ export const Validators = () => { const [validations, setValidations] = useState([]) const [metrics, setMetrics] = useState({}) const [unlCount, setUnlCount] = useState(0) + const [feeSettings, setFeeSettings] = useState( + undefined, + ) const network = useContext(NetworkContext) + const rippledSocket = useContext(SocketContext) + const { tab = 'uptime' } = useRouteParams(VALIDATORS_ROUTE) useQuery(['fetchValidatorsData'], () => fetchData(), { refetchInterval: (returnedData, _) => @@ -35,6 +51,15 @@ export const Validators = () => { enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, }) + useQuery(['fetchFeeSettingsData'], () => fetchFeeSettingsData(), { + refetchInterval: (returnedData, _) => + returnedData == null + ? FETCH_INTERVAL_ERROR_MILLIS + : FETCH_INTERVAL_FEE_SETTINGS_MILLIS, + refetchOnMount: true, + enabled: process.env.VITE_ENVIRONMENT !== 'custom' || !!network, + }) + function mergeLatest( validators: Record, live: Record, @@ -55,6 +80,20 @@ export const Validators = () => { return updated } + function fetchFeeSettingsData() { + if (tab === 'voting') { + getServerState(rippledSocket) + .then((res) => res.state) + .then((state) => { + setFeeSettings({ + base_fee: state.validated_ledger.base_fee, + reserve_base: state.validated_ledger.reserve_base, + reserve_inc: state.validated_ledger.reserve_inc, + }) + }) + } + } + function fetchData() { const url = `${process.env.VITE_DATA_URL}/validators/${network}` @@ -92,8 +131,27 @@ export const Validators = () => { } const validatorCount = Object.keys(vList).length + + const Body = { + uptime: ( + + ), + voting: ( + + ), + }[tab] return (
+
{t('validators')}
{network && ( {
- - + + + {Body}
) diff --git a/src/containers/Network/ValidatorsTable.jsx b/src/containers/Network/ValidatorsTable.jsx deleted file mode 100644 index 7649b8781..000000000 --- a/src/containers/Network/ValidatorsTable.jsx +++ /dev/null @@ -1,158 +0,0 @@ -import { Component } from 'react' -import PropTypes from 'prop-types' -import { withTranslation } from 'react-i18next' -import { Loader } from '../shared/components/Loader' -import SuccessIcon from '../shared/images/success.svg' -import DomainLink from '../shared/components/DomainLink' -import InfoIcon from '../shared/images/info.svg' -import './css/validatorsTable.scss' -import { RouteLink } from '../shared/routing' -import { LEDGER_ROUTE, VALIDATOR_ROUTE } from '../App/routes' - -class ValidatorsTable extends Component { - static getDerivedStateFromProps(nextProps, prevState) { - return nextProps.validators - ? { - validators: ValidatorsTable.sortValidators( - Object.values(nextProps.validators), - ), - metrics: nextProps.metrics, - } - : null - } - - static sortValidators = (data) => { - data.sort((a, b) => { - const aUnl = a.unl || 'zzz' - const bUnl = b.unl || 'zzz' - const aDomain = a.domain || 'zzz' - const bDomain = b.domain || 'zzz' - const aScore = a.agreement_30day ? a.agreement_30day.score : -1 - const bScore = b.agreement_30day ? b.agreement_30day.score : -1 - const aPubkey = a.master_key || a.signing_key - const bPubkey = b.master_key || b.signing_key - - // 1. Sort by whether the validator is on the UNL - if (aUnl > bUnl) return 1 - if (aUnl < bUnl) return -1 - // 2. Sort by the 30 day score (descending) - if (aScore < bScore) return 1 - if (aScore > bScore) return -1 - // 3. Sort alphabetically by the domain - if (aDomain > bDomain) return 1 - if (aDomain < bDomain) return -1 - // 4. Sort alphabetically by the public key - if (aPubkey > bPubkey) return 1 - if (aPubkey < bPubkey) return -1 - - return 0 - }) - - return data - } - - constructor(props) { - super(props) - this.state = {} - } - - static renderDomain = (domain) => domain && - - renderAgreement = (className, d) => { - const { t } = this.props - - return d ? ( - - {Number.parseFloat(d.score).toFixed(5)} - {d.incomplete && *} - - ) : ( - - ) - } - - renderValidator = (d) => { - const { metrics } = this.state - const color = d.ledger_hash ? `#${d.ledger_hash.substring(0, 6)}` : '' - const trusted = d.unl ? 'yes' : 'no' - const pubkey = d.master_key || d.signing_key - const onNegativeUnl = metrics.nUnl && metrics.nUnl.includes(pubkey) - const nUnl = onNegativeUnl ? 'yes' : 'no' - const ledgerIndex = d.ledger_index ?? d.current_index - - return ( - - - - {pubkey} - - - - {ValidatorsTable.renderDomain(d.domain)} - - - {d.unl && } - - - {onNegativeUnl && } - - {d.server_version} - {this.renderAgreement('h1', d.agreement_1h)} - {this.renderAgreement('h24', d.agreement_24h)} - {this.renderAgreement('d30', d.agreement_30day)} - - - {ledgerIndex} - - {d.partial && '*'} - - - ) - } - - render() { - const { t } = this.props - const { validators } = this.state - const content = validators ? ( - - - - - - - - - - - - - - - {validators.map(this.renderValidator)} -
{t('pubkey')}{t('domain')}{t('unl')}{t('nUnlCol')}{t('Version')}{t('1H')}{t('24H')}{t('30D')}{t('ledger')}
- ) : ( - - ) - - return
{content}
- } -} - -ValidatorsTable.propTypes = { - validators: PropTypes.arrayOf(PropTypes.shape({})), - t: PropTypes.func.isRequired, - metrics: PropTypes.shape({}).isRequired, -} - -ValidatorsTable.defaultProps = { - validators: null, -} - -export default withTranslation()(ValidatorsTable) diff --git a/src/containers/Network/ValidatorsTable.tsx b/src/containers/Network/ValidatorsTable.tsx new file mode 100644 index 000000000..60fe2715d --- /dev/null +++ b/src/containers/Network/ValidatorsTable.tsx @@ -0,0 +1,196 @@ +import { useTranslation } from 'react-i18next' +import { FeeSettings, StreamValidator } from '../shared/vhsTypes' +import { RouteLink } from '../shared/routing' +import { VALIDATOR_ROUTE, LEDGER_ROUTE } from '../App/routes' +import SuccessIcon from '../shared/images/success.svg' +import UpIcon from '../shared/images/ic_up.svg' +import DownIcon from '../shared/images/ic_down.svg' +import DomainLink from '../shared/components/DomainLink' +import InfoIcon from '../shared/images/info.svg' +import { Loader } from '../shared/components/Loader' +import './css/validatorsTable.scss' +import { useLanguage } from '../shared/hooks' +import { renderXRP } from '../shared/utils' + +const DROPS_TO_XRP_FACTOR = 1000000 + +interface ValidatorsTableProps { + validators: StreamValidator[] + metrics: any + tab: string + feeSettings?: FeeSettings +} + +const sortValidators = (data) => { + data.sort((a, b) => { + const aUnl = a.unl || 'zzz' + const bUnl = b.unl || 'zzz' + const aDomain = a.domain || 'zzz' + const bDomain = b.domain || 'zzz' + const aScore = a.agreement_30day ? a.agreement_30day.score : -1 + const bScore = b.agreement_30day ? b.agreement_30day.score : -1 + const aPubkey = a.master_key || a.signing_key + const bPubkey = b.master_key || b.signing_key + + // 1. Sort by whether the validator is on the UNL + if (aUnl > bUnl) return 1 + if (aUnl < bUnl) return -1 + // 2. Sort by the 30 day score (descending) + if (aScore < bScore) return 1 + if (aScore > bScore) return -1 + // 3. Sort alphabetically by the domain + if (aDomain > bDomain) return 1 + if (aDomain < bDomain) return -1 + // 4. Sort alphabetically by the public key + if (aPubkey > bPubkey) return 1 + if (aPubkey < bPubkey) return -1 + + return 0 + }) + + return data +} + +export const ValidatorsTable = (props: ValidatorsTableProps) => { + const { validators: rawValidators, metrics, tab, feeSettings } = props + const validators = rawValidators ? sortValidators(rawValidators) : undefined + const { t } = useTranslation() + const language = useLanguage() + + const renderDomain = (domain) => domain && + + const renderAgreement = (className, d) => + d ? ( + + {Number.parseFloat(d.score).toFixed(5)} + {d.incomplete && *} + + ) : ( + + ) + + const renderFeeVoting = (className, data, currentFee, pubkey) => + data ? ( + + {currentFee && + data !== currentFee && + (data > currentFee ? ( + + + + ) : ( + + + + ))} + {renderXRP(data / DROPS_TO_XRP_FACTOR, language)} + + ) : ( + + ) + + const renderValidator = (d) => { + const color = d.ledger_hash ? `#${d.ledger_hash.substring(0, 6)}` : '' + const trusted = d.unl ? 'yes' : 'no' + const pubkey = d.master_key || d.signing_key + const onNegativeUnl = metrics.nUnl && metrics.nUnl.includes(pubkey) + const nUnl = onNegativeUnl ? 'yes' : 'no' + const ledgerIndex = d.ledger_index ?? d.current_index + + return ( + + + + {pubkey} + + + {renderDomain(d.domain)} + + {d.unl && } + + + {onNegativeUnl && } + + {d.server_version} + {tab === 'uptime' ? ( + <> + {renderAgreement('h1', d.agreement_1h)} + {renderAgreement('h24', d.agreement_24h)} + {renderAgreement('d30', d.agreement_30day)} + + ) : ( + <> + {renderFeeVoting( + 'base', + d.reserve_base, + feeSettings?.reserve_base, + pubkey, + )} + {renderFeeVoting( + 'owner', + d.reserve_inc, + feeSettings?.reserve_inc, + pubkey, + )} + {renderFeeVoting( + 'base_fee', + d.base_fee, + feeSettings?.base_fee, + pubkey, + )} + + )} + + + + {ledgerIndex} + + {d.partial && '*'} + + + ) + } + + const content = validators ? ( + + + + + + + + + {tab === 'uptime' ? ( + <> + + + + + ) : ( + <> + {' '} + + + + + )} + + + + {validators.map(renderValidator)} +
{t('pubkey')}{t('domain')}{t('unl')}{t('nUnlCol')}{t('Version')}{t('1H')}{t('24H')}{t('30D')} + {t('base')} + {t('owner')}{t('base_fee')}{t('ledger')}
+ ) : ( + + ) + + return
{content}
+} diff --git a/src/containers/Network/ValidatorsTabs.tsx b/src/containers/Network/ValidatorsTabs.tsx new file mode 100644 index 000000000..bcd64fae2 --- /dev/null +++ b/src/containers/Network/ValidatorsTabs.tsx @@ -0,0 +1,21 @@ +import { Tabs } from '../shared/components/Tabs' +import { buildPath } from '../shared/routing' +import { VALIDATORS_ROUTE } from '../App/routes' + +interface Props { + selected: string +} + +const ValidatorsTabs = (props: Props) => { + const { selected } = props + const tabs = ['uptime', 'voting'] + return ( + + ) +} + +export default ValidatorsTabs diff --git a/src/containers/Network/css/style.scss b/src/containers/Network/css/style.scss index c2f1f30e0..0ad498ddf 100644 --- a/src/containers/Network/css/style.scss +++ b/src/containers/Network/css/style.scss @@ -39,4 +39,20 @@ max-width: 1500px; margin: auto; } + + .type { + display: inline-block; + margin-top: 80px; + margin-bottom: 32px; + margin-left: 16px; + margin-left: calc((100vw - 1500px) / 2); + margin-left: clamp(16px, calc((100vw - 1500px) / 2), calc((100vw - 1500px) / 2)); // Adjust based on wrap margin with min 16px + color: $white; + font-size: 32px; + @include for-size(tablet-portrait-up) { + font-size: 42px; + } + + @include bold; + } } diff --git a/src/containers/Network/css/validatorsTable.scss b/src/containers/Network/css/validatorsTable.scss index 0b2aab55d..ed1775702 100644 --- a/src/containers/Network/css/validatorsTable.scss +++ b/src/containers/Network/css/validatorsTable.scss @@ -5,13 +5,6 @@ min-height: 150px; table { - .pubkey, - .score.h1, - .score.d30, - .fee { - display: none; - } - .pubkey { max-width: 70px; @include for-size(tablet-portrait-up) { @@ -83,6 +76,25 @@ color: $orange-40; } + .fee-icon { + position: relative; + top: 1.5px; + margin-right: 4px; + } + + .vote { + white-space: nowrap; + } + } + + &.uptime-tab { + .pubkey, + .score.h1, + .score.d30, + .fee { + display: none; + } + @include for-size(tablet-portrait-up) { .score.d30 { display: table-cell; @@ -102,4 +114,32 @@ } } } + + &.voting-tab { + .pubkey, + .last-ledger, + .n-unl, + .version { + display: none; + } + + @include for-size(tablet-portrait-up) { + .n-unl, .version { + display: table-cell; + } + } + + @include for-size(tablet-landscape-up) { + .pubkey { + display: table-cell; + } + } + + @include for-size(desktop-up) { + .last-ledger { + display: table-cell; + } + } + } + } diff --git a/src/containers/Network/index.tsx b/src/containers/Network/index.tsx deleted file mode 100644 index 70888bc9d..000000000 --- a/src/containers/Network/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useContext, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { Helmet } from 'react-helmet-async' -import { NETWORK_ROUTE } from '../App/routes' -import { useAnalytics } from '../shared/analytics' -import { useRouteParams } from '../shared/routing' -import NetworkContext from '../shared/NetworkContext' -import { Validators } from './Validators' -import { UpgradeStatus } from './UpgradeStatus' -import { Nodes } from './Nodes' -import NoMatch from '../NoMatch' -import './css/style.scss' - -export const Network = () => { - const { trackScreenLoaded } = useAnalytics() - const { t } = useTranslation() - const { tab = 'nodes' } = useRouteParams(NETWORK_ROUTE) - const network = useContext(NetworkContext) - - useEffect(() => { - trackScreenLoaded() - }, [tab, trackScreenLoaded]) - - if (network === null) { - return ( - - ) - } - - const Body = { - 'upgrade-status': UpgradeStatus, - validators: Validators, - nodes: Nodes, - }[tab] - return ( - <> - - - - ) -} diff --git a/src/containers/Network/test/mockValidators.json b/src/containers/Network/test/mockValidators.json index d5c444fa3..e425574cb 100644 --- a/src/containers/Network/test/mockValidators.json +++ b/src/containers/Network/test/mockValidators.json @@ -22,7 +22,10 @@ "score": 0.98468, "missed": 120, "incomplete": true - } + }, + "base_fee":10, + "reserve_base":1000000, + "reserve_inc":200000 }, { "master_key": "nHDaxUL87HiVszvCamVu4A3Gecq6LTxKkUVNdzf3nqmuSywgRqu4", @@ -47,7 +50,10 @@ "score": 0.98, "missed": 1, "incomplete": true - } + }, + "base_fee": 200, + "reserve_base": 1000000, + "reserve_inc": 200000 }, { "master_key": "nHUroc3Q1ErBXs689SEi3nWEeM759Pn1LsZk27jMHJtiemHXVmJb", @@ -97,6 +103,9 @@ "score": 0.99964, "missed": 15, "incomplete": true - } + }, + "base_fee": 12, + "reserve_base": 1000000, + "reserve_inc": 100000 } ] diff --git a/src/containers/Network/test/nodes.test.js b/src/containers/Network/test/nodes.test.js index 7455912d5..e6bd6afd5 100644 --- a/src/containers/Network/test/nodes.test.js +++ b/src/containers/Network/test/nodes.test.js @@ -2,12 +2,12 @@ import { mount } from 'enzyme' import moxios from 'moxios' import { Route } from 'react-router-dom' import i18n from '../../../i18n/testConfig' -import { Network } from '../index' import mockNodes from './mockNodes.json' import NetworkContext from '../../shared/NetworkContext' import countries from '../../../../public/countries.json' import { QuickHarness } from '../../test/utils' -import { NETWORK_ROUTE } from '../../App/routes' +import { NODES_ROUTE } from '../../App/routes' +import { Nodes } from '../Nodes' jest.mock('usehooks-ts', () => ({ useWindowSize: () => ({ @@ -21,7 +21,7 @@ describe('Nodes Page container', () => { mount( - } /> + } /> , ) diff --git a/src/containers/Network/test/upgradeStatus.test.js b/src/containers/Network/test/upgradeStatus.test.js index 28d43d480..0c5ad5ffb 100644 --- a/src/containers/Network/test/upgradeStatus.test.js +++ b/src/containers/Network/test/upgradeStatus.test.js @@ -3,16 +3,16 @@ import moxios from 'moxios' import WS from 'jest-websocket-mock' import { Route } from 'react-router' import i18n from '../../../i18n/testConfig' -import { Network } from '../index' import SocketContext from '../../shared/SocketContext' import MockWsClient from '../../test/mockWsClient' import { QuickHarness } from '../../test/utils' -import { NETWORK_ROUTE } from '../../App/routes' import { + UpgradeStatus, aggregateData, aggregateNodes, aggregateValidators, } from '../UpgradeStatus' +import { UPGRADE_STATUS_ROUTE } from '../../App/routes' const undefinedValidatorsData = [ { @@ -113,7 +113,7 @@ describe('UpgradeStatus renders', () => { mount( - } /> + } /> , ) diff --git a/src/containers/Network/test/validators.test.js b/src/containers/Network/test/validators.test.js index e8523d7b3..5f6739702 100644 --- a/src/containers/Network/test/validators.test.js +++ b/src/containers/Network/test/validators.test.js @@ -3,13 +3,13 @@ import moxios from 'moxios' import WS from 'jest-websocket-mock' import { Route } from 'react-router' import i18n from '../../../i18n/testConfig' -import { Network } from '../index' import mockValidators from './mockValidators.json' import validationMessage from './mockValidation.json' import SocketContext from '../../shared/SocketContext' import MockWsClient from '../../test/mockWsClient' import { QuickHarness } from '../../test/utils' -import { NETWORK_ROUTE } from '../../App/routes' +import { VALIDATORS_ROUTE } from '../../App/routes' +import { Validators } from '../Validators' const WS_URL = 'ws://localhost:1234' @@ -20,7 +20,7 @@ describe('Validators Tab container', () => { mount( - } /> + } /> , ) diff --git a/src/containers/Network/test/validatorsTable.test.js b/src/containers/Network/test/validatorsTable.test.js index 2bd4e69b5..93367ebf7 100644 --- a/src/containers/Network/test/validatorsTable.test.js +++ b/src/containers/Network/test/validatorsTable.test.js @@ -2,7 +2,7 @@ import { mount } from 'enzyme' import { BrowserRouter as Router } from 'react-router-dom' import { I18nextProvider } from 'react-i18next' import i18n from '../../../i18n/testConfig' -import ValidatorsTable from '../ValidatorsTable' +import { ValidatorsTable } from '../ValidatorsTable' import validators from './mockValidators.json' import metrics from './metrics.json' @@ -23,8 +23,29 @@ describe('Validators table', () => { }) it('renders all parts', () => { - const wrapper = createWrapper({ validators, metrics }) + const tab = 'uptime' + const wrapper = createWrapper({ validators, metrics, tab }) expect(wrapper.find('tr').length).toBe(validators.length + 1) wrapper.unmount() }) + + it('renders uptime tab', () => { + const tab = 'uptime' + const wrapper = createWrapper({ validators, metrics, tab }) + expect(wrapper.find('.uptime-tab').length).toBe(1) + expect(wrapper.find('td.h1').at(0).text().trim()).toBe('1.00000') + expect(wrapper.find('td.h24').at(0).text().trim()).toBe('0.91729*') + expect(wrapper.find('td.d30').at(0).text().trim()).toBe('0.98468*') + wrapper.unmount() + }) + + it('renders voting tab', () => { + const tab = 'voting' + const wrapper = createWrapper({ validators, metrics, tab }) + expect(wrapper.find('.voting-tab').length).toBe(1) + expect(wrapper.find('td.base').at(0).text().trim()).toContain('1.00') + expect(wrapper.find('td.owner').at(0).text().trim()).toContain('0.20') + expect(wrapper.find('td.base_fee').at(0).text().trim()).toContain('0.00001') + wrapper.unmount() + }) }) diff --git a/src/containers/shared/css/tabs.scss b/src/containers/shared/css/tabs.scss index 3ffa955ec..d78164f61 100644 --- a/src/containers/shared/css/tabs.scss +++ b/src/containers/shared/css/tabs.scss @@ -16,6 +16,7 @@ color: $black-40; cursor: pointer; text-align: center; + text-transform: capitalize; &:hover, &:focus { diff --git a/src/containers/shared/images/ic_down.svg b/src/containers/shared/images/ic_down.svg new file mode 100644 index 000000000..24d406e4a --- /dev/null +++ b/src/containers/shared/images/ic_down.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/containers/shared/images/ic_up.svg b/src/containers/shared/images/ic_up.svg new file mode 100644 index 000000000..1f8082b27 --- /dev/null +++ b/src/containers/shared/images/ic_up.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index 9580b4754..f2959e1e5 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -22,6 +22,7 @@ export const FETCH_INTERVAL_VHS_MILLIS = 60 * 1000 // 1 minute export const FETCH_INTERVAL_NODES_MILLIS = 60000 export const FETCH_INTERVAL_ERROR_MILLIS = 300 export const FETCH_INTERVAL_XRP_USD_ORACLE_MILLIS = 60 * 1000 +export const FETCH_INTERVAL_FEE_SETTINGS_MILLIS = 10 * 60 * 1000 // 10 minutes export const DECIMAL_REGEX = /^\d+$/ export const HASH256_REGEX = /[0-9A-Fa-f]{64}/i diff --git a/src/containers/shared/vhsTypes.ts b/src/containers/shared/vhsTypes.ts index 3984b56af..e2b37f0f4 100644 --- a/src/containers/shared/vhsTypes.ts +++ b/src/containers/shared/vhsTypes.ts @@ -101,6 +101,15 @@ export interface StreamValidator extends ValidatorResponse { ledger_hash?: string pubkey?: string time?: string + base_fee?: number + reserve_base?: number + reserve_inc?: number +} + +export interface FeeSettings { + base_fee: number + reserve_base: number + reserve_inc: number } export interface AmendmentData { diff --git a/src/rippled/lib/rippled.js b/src/rippled/lib/rippled.js index 85900ffa9..d4bc588bf 100644 --- a/src/rippled/lib/rippled.js +++ b/src/rippled/lib/rippled.js @@ -509,6 +509,17 @@ const getServerInfo = (rippledSocket) => return resp }) +const getServerState = (rippledSocket) => + query(rippledSocket, { + command: 'server_state', + }).then((resp) => { + if (resp.error !== undefined || resp.error_message !== undefined) { + throw new Error(resp.error_message || resp.error, 500) + } + + return resp + }) + const getOffers = ( rippledSocket, currencyCode, @@ -653,6 +664,7 @@ export { getAccountTransactions, getNegativeUNL, getServerInfo, + getServerState, getOffers, getNFTInfo, getBuyNFToffers,