From 0ed55addcbae3146b3a0f76a6b008bb8db535bd1 Mon Sep 17 00:00:00 2001 From: Phu Pham Date: Mon, 18 Sep 2023 19:34:51 -0400 Subject: [PATCH] add voting page --- public/locales/en-US/translations.json | 7 ++ src/containers/App/routes.ts | 2 +- src/containers/Ledgers/LedgerMetrics.jsx | 2 +- src/containers/Validators/VotingTab.tsx | 122 ++++++++++++++++++++++- src/containers/Validators/index.tsx | 4 + src/containers/Validators/votingTab.scss | 90 +++++++++++++++++ src/containers/shared/vhsTypes.ts | 4 + 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 src/containers/Validators/votingTab.scss diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index b81edbd0d..023056fce 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -337,6 +337,13 @@ "unauthorize": "unauthorize", "missed_validations": "{{count}} missed validations", "incomplete": "incomplete", + "base_fee": "BASE FEE", + "reserve_base": "RESERVE BASE", + "reserve_inc": "RESERVE INC.", + "amendment_name": "Name", + "amendment_id": "Amendment ID", + "vote": "Vote", + "no_amendment_in_voting": "There is no amendment in voting for this network at the moment.", "required": "required", "source": "source", "destination": "destination", diff --git a/src/containers/App/routes.ts b/src/containers/App/routes.ts index c4a0247a2..949910f03 100644 --- a/src/containers/App/routes.ts +++ b/src/containers/App/routes.ts @@ -54,7 +54,7 @@ export const TRANSACTION_ROUTE: RouteDefinition<{ export const VALIDATOR_ROUTE: RouteDefinition<{ identifier: string - tab?: 'details' | 'history' + tab?: 'details' | 'history' | 'voting' }> = { path: `/validators/:identifier/:tab?`, } diff --git a/src/containers/Ledgers/LedgerMetrics.jsx b/src/containers/Ledgers/LedgerMetrics.jsx index 77306d19a..ebea92471 100644 --- a/src/containers/Ledgers/LedgerMetrics.jsx +++ b/src/containers/Ledgers/LedgerMetrics.jsx @@ -19,7 +19,7 @@ const DEFAULTS = { nUnl: [], } -const renderXRP = (d, language) => { +export const renderXRP = (d, language) => { const options = { ...CURRENCY_OPTIONS, currency: 'XRP' } return localizeNumber(d, language, options) } diff --git a/src/containers/Validators/VotingTab.tsx b/src/containers/Validators/VotingTab.tsx index 0bd80d4ca..595732dcc 100644 --- a/src/containers/Validators/VotingTab.tsx +++ b/src/containers/Validators/VotingTab.tsx @@ -1,4 +1,124 @@ import { useTranslation } from 'react-i18next' import { FC } from 'react' +import axios from 'axios' +import { useQuery } from 'react-query' -export const VotingTab = +import { ValidatorSupplemented } from '../shared/vhsTypes' +import { SimpleRow } from '../shared/components/Transaction/SimpleRow' + +import './votingTab.scss' +import { renderXRP } from '../Ledgers/LedgerMetrics' +import { + FETCH_INTERVAL_ERROR_MILLIS, + FETCH_INTERVAL_VHS_MILLIS, + SERVER_ERROR, +} from '../shared/utils' +import { useAnalytics } from '../shared/analytics' + +const DROPS_TO_XRP_FACTOR = 1000000 + +export const VotingTab: FC<{ + validatorData: ValidatorSupplemented + network: string | undefined +}> = ({ validatorData, network }) => { + const { t } = useTranslation() + const { trackException } = useAnalytics() + + const votedAmenments = new Set( + validatorData.amendments + ? validatorData.amendments.map((amendment) => amendment.id) + : [], + ) + const { data } = useQuery>( + ['fetchNetworkVotingData', network], + async () => fetchNetworkVote(network), + { + refetchInterval: (returnedData, _) => + returnedData == null + ? FETCH_INTERVAL_ERROR_MILLIS + : FETCH_INTERVAL_VHS_MILLIS, + refetchOnMount: true, + enabled: !!network, + }, + ) + + function fetchNetworkVote(networkID: string | undefined) { + const url = `${process.env.VITE_DATA_URL}/amendments/vote/${networkID}` + return axios + .get(url) + .then((resp) => resp.data) + .then((response) => + response.amendments.voting.amendments.map((amendment) => ({ + id: amendment.id, + name: amendment.name, + })), + ) + .catch((axiosError) => { + const status = + axiosError.response && axiosError.response.status + ? axiosError.response.status + : SERVER_ERROR + trackException(`${url} --- ${JSON.stringify(axiosError)}`) + return Promise.reject(status) + }) + } + + const renderAmendment = (id: string, name: string, voted: boolean) => ( +
+ + {name} + + {id} + {voted ? ( + + Yea + + ) : ( + + Nay + + )} +
+ ) + + return ( +
+
+
+
{t('base_fee')}
+
{renderXRP(validatorData.base_fee / DROPS_TO_XRP_FACTOR)}
+
+
+
{t('reserve_base')}
+
+ {renderXRP(validatorData.reserve_base / DROPS_TO_XRP_FACTOR)} +
+
+
+
{t('reserve_inc')}
+
+ {renderXRP(validatorData.reserve_inc / DROPS_TO_XRP_FACTOR)} +
+
+
+
Amendments
+
+
+ {data !== undefined && data.length > 0 ? ( + data.map((amendment) => { + let voted = false + if (votedAmenments.has(amendment.id)) { + voted = true + } + return renderAmendment(amendment.id, amendment.name, voted) + }) + ) : ( +
+
{t('no_amendment_in_voting')}
+
+ )} +
+
+
+ ) +} diff --git a/src/containers/Validators/index.tsx b/src/containers/Validators/index.tsx index 04c2200f9..1da9352d8 100644 --- a/src/containers/Validators/index.tsx +++ b/src/containers/Validators/index.tsx @@ -23,6 +23,7 @@ import { ValidatorReport, ValidatorSupplemented } from '../shared/vhsTypes' import NetworkContext from '../shared/NetworkContext' import { VALIDATOR_ROUTE } from '../App/routes' import { buildPath, useRouteParams } from '../shared/routing' +import { VotingTab } from './VotingTab' const ERROR_MESSAGES = { [NOT_FOUND]: { @@ -172,6 +173,9 @@ export const Validator = () => { case 'history': body = break + case 'voting': + body = data && + break default: body = data && break diff --git a/src/containers/Validators/votingTab.scss b/src/containers/Validators/votingTab.scss new file mode 100644 index 000000000..73e1eff47 --- /dev/null +++ b/src/containers/Validators/votingTab.scss @@ -0,0 +1,90 @@ +@import '../shared/css/variables'; + +.voting { + margin-left: 24px; + + .metrics { + &.metrics-voting { + width: auto; + padding-left: 0; + text-align: left; + + .cell { + display: flex; + justify-content: space-between; + margin: 0 !important; + text-align: left; + + @include for-size(tablet-landscape-up) { + display: inline-block; + } + } + } + } + + .amendment-label { + margin-top: 48px; + font-size: 24px; + font-weight: 700; + } + + .voting-amendment { + font-size: 14px; + + .row-amendment { + margin-top: 32px; + background-color: $black-80; + } + + .amendment-name { + color: $green-30 + } + + .row { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid $black-70; + } + + .label { + margin-top: 0; + font-weight: 100; + } + + .value { + overflow-x: scroll; + scrollbar-width: none; + text-align: end; + + &::-webkit-scrollbar { + /* WebKit */ + width: 0; + height: 0; + } + } + } + + .pill { + align-items: center; + justify-content: center; + padding: 2px 12px; + border-radius: 30px; + color: $black-100; + + &.yea { + background-color: $green-60; + } + + &.nay { + background-color: $black-30; + } + } + + .no-match { + &.no-match-amendments { + margin: 32px 0; + } + } +} diff --git a/src/containers/shared/vhsTypes.ts b/src/containers/shared/vhsTypes.ts index e12982f9c..e711969fb 100644 --- a/src/containers/shared/vhsTypes.ts +++ b/src/containers/shared/vhsTypes.ts @@ -88,6 +88,10 @@ export interface ValidatorSupplemented extends ValidatorResponse { ledger_hash: string // eslint-disable-next-line camelcase -- mimicking rippled last_ledger_time: string + amendments: Array<{ id: string; name: string }> + base_fee: number + reserve_base: number + reserve_inc: number } export interface StreamValidator extends ValidatorResponse {