feat(2326): style Token validators (#2388)

* feat(2326): styled up validators intro as per designs, including site-wide tweaking to headers for consistency

* feat(2326): styled up reduced height epoch progress bar

* feat(2326): styled up inline 'reveal all validators' table row. Test failing

* feat(2326): failing test fixed

* feat(2326): components and styling for validator table

* feat(2326): making proposals section consistent with new validators styling

* feat(2326): extra consistency changes

* feat(2326): linting fixes

* feat(2326): a couple of e2e test fixes

* feat(2326): more e2e test fixes
This commit is contained in:
Sam Keen 2022-12-14 13:25:03 +00:00 committed by GitHub
parent e49ad9da6a
commit 071a9ab34b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 776 additions and 640 deletions

View File

@ -86,7 +86,7 @@ const classes = {
indicatorFailed: indicatorFailed:
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-red-600 bg-red-600 text-center text-white font-bold leading-5', 'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-red-600 bg-red-600 text-center text-white font-bold leading-5',
textFailed: textFailed:
'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-red-600', 'absolute top-0 -ml-10 text-center mt-16 w-32 text-xs font-medium uppercase text-vega-red',
indicatorComplete: indicatorComplete:
'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-vega-green-dark bg-vega-green-dark text-center text-white leading-5', 'rounded-full transition duration-500 ease-in-out h-12 w-12 py-3 border-2 border-vega-green-dark bg-vega-green-dark text-center text-white leading-5',
textComplete: textComplete:

View File

@ -1,4 +1,4 @@
const proposalDocumentationLink = '[data-testid="external-link"]'; const proposalDocumentationLink = '[data-testid="proposal-documentation-link"]';
const newProposalButton = '[data-testid="new-proposal-link"]'; const newProposalButton = '[data-testid="new-proposal-link"]';
const newProposalLink = '[data-testid="new-proposal-link"]'; const newProposalLink = '[data-testid="new-proposal-link"]';
const governanceDocsUrl = 'https://vega.xyz/governance'; const governanceDocsUrl = 'https://vega.xyz/governance';

View File

@ -25,7 +25,7 @@ context('Staking Page - verify elements on page', function () {
}); });
it('Should have STAKING ON VEGA header visible', function () { it('Should have STAKING ON VEGA header visible', function () {
cy.verify_page_header('Staking'); cy.verify_page_header('Validators');
}); });
it('Should have Staking Guide link visible', function () { it('Should have Staking Guide link visible', function () {
@ -94,10 +94,6 @@ context('Staking Page - verify elements on page', function () {
cy.wrap($pendingStake).should('contain.text', '0.00'); cy.wrap($pendingStake).should('contain.text', '0.00');
}); });
}); });
it('Should be able to see button to unhide top validators', function () {
cy.get('[data-testid="show-all-validators"]').should('be.visible');
});
} }
); );

View File

@ -1,9 +1,6 @@
import { format, formatDistanceStrict } from 'date-fns'; import { formatDistanceStrict } from 'date-fns';
import * as React from 'react'; import * as React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import arrow from '../../images/back.png';
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
import { ProgressBar } from '../progress-bar'; import { ProgressBar } from '../progress-bar';
export interface EpochCountdownProps { export interface EpochCountdownProps {
@ -58,8 +55,8 @@ export function EpochCountdown({
return ( return (
<div data-testid="epoch-countdown" className="epoch-countdown"> <div data-testid="epoch-countdown" className="epoch-countdown">
<div className="flex items-end"> <div className="flex items-end mb-3">
<h3 className="flex-1"> <h3 className="flex-1 m-0 text-sm">
{t('Epoch')} {id} {t('Epoch')} {id}
</h3> </h3>
<p className="text-sm m-0"> <p className="text-sm m-0">
@ -69,17 +66,6 @@ export function EpochCountdown({
</p> </p>
</div> </div>
<ProgressBar value={progress} /> <ProgressBar value={progress} />
<div className="flex text-sm">
<p>{format(startDate, DATE_FORMAT_DETAILED)}</p>
<div className="flex-1 text-center">
<img
className="inline-block w-[5px] rotate-180"
alt="arrow"
src={arrow}
/>
</div>
<p>{format(endDate, DATE_FORMAT_DETAILED)}</p>
</div>
</div> </div>
); );
} }

View File

@ -15,13 +15,37 @@ export const Heading = ({
return ( return (
<header <header
className={classNames('my-0', { className={classNames('mt-10 mb-6', {
'mx-auto': centerContent, 'mx-auto': centerContent,
})} })}
> >
<h1 className={classNames('font-alpha calt', { 'mb-0': !marginBottom })}> <h1
className={classNames('font-alpha calt text-5xl', {
'mb-0': !marginBottom,
})}
>
{title} {title}
</h1> </h1>
</header> </header>
); );
}; };
export const SubHeading = ({
title,
centerContent = false,
marginBottom = true,
}: HeadingProps) => {
if (!title) return null;
return (
<h2
className={classNames('text-2xl font-alpha calt uppercase', {
'mx-auto': centerContent,
'mb-0': !marginBottom,
'mb-4': marginBottom,
})}
>
{title}
</h2>
);
};

View File

@ -13,10 +13,10 @@ export const ProgressBar = ({ value }: ProgressBarProps) => {
aria-valuemin={0} aria-valuemin={0}
aria-valuenow={percent == null ? undefined : Math.round(percent)} aria-valuenow={percent == null ? undefined : Math.round(percent)}
role="progressbar" role="progressbar"
className="relative border h-[21px]" className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
> >
<div <div
className="bg-white h-full absolute transition-[width] ease-in-out" className="rounded-full bg-clouds bg-vega-yellow h-full absolute transition-[width] ease-in-out"
style={{ width }} style={{ width }}
/> />
</div> </div>

View File

@ -8,7 +8,6 @@
"pageTitleLiquidity": "Incentivised Liquidity Programme", "pageTitleLiquidity": "Incentivised Liquidity Programme",
"pageTitleRedemptionTranche": "Redeem from Tranche", "pageTitleRedemptionTranche": "Redeem from Tranche",
"pageTitleTranches": "Vesting tranches", "pageTitleTranches": "Vesting tranches",
"pageTitleStaking": "Staking",
"pageTitle404": "Page not found", "pageTitle404": "Page not found",
"pageTitleNotPermitted": "Can not proceed!", "pageTitleNotPermitted": "Can not proceed!",
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key", "pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
@ -17,6 +16,7 @@
"pageTitleWithdrawLp": "Withdraw SLP and Rewards", "pageTitleWithdrawLp": "Withdraw SLP and Rewards",
"pageTitleRewards": "Rewards", "pageTitleRewards": "Rewards",
"pageTitleRejectedProposals": "Rejected proposals", "pageTitleRejectedProposals": "Rejected proposals",
"pageTitleValidators": "Validators",
"Vesting": "Vesting", "Vesting": "Vesting",
"unstaked": "Unstaked", "unstaked": "Unstaked",
"of": "of", "of": "of",
@ -293,15 +293,7 @@
"Governance is coming soon": "Governance is coming soon", "Governance is coming soon": "Governance is coming soon",
"Staking is coming soon": "Staking is coming soon", "Staking is coming soon": "Staking is coming soon",
"VESTING VEGA TOKENS": "in vesting contract", "VESTING VEGA TOKENS": "in vesting contract",
"stakingStep1": "Step 1. Connect to a Vega Wallet", "stakingIntro": "Earn a share of trading fees and treasury rewards for each full epoch staked.",
"stakingStep1Text": "You will need a <vegaWalletLink>Vega Wallet</vegaWalletLink> to control stake and receive staking rewards.",
"stakingVegaWalletConnected": "Connected to Vega Wallet with public key {{key}}",
"stakingStep2": "Step 2. Associate tokens with a Vega Wallet",
"stakingAssociateConnectVega": "You need to connect to a Vega Wallet first",
"stakingAssociateConnectEth": "You need to connect to an Ethereum wallet first",
"stakingHasAssociated": "You have {{tokens}} $VEGA tokens associated.",
"stakingAssociateMoreButton": "Associate more $VEGA tokens with wallet",
"stakingDisassociateButton": "Disassociate $VEGA tokens from wallet",
"stakingConfirm": "Open your wallet app to confirm", "stakingConfirm": "Open your wallet app to confirm",
"associateButton": "Associate $VEGA tokens with wallet", "associateButton": "Associate $VEGA tokens with wallet",
"nodeQueryFailed": "Could not get data for validator {{node}}", "nodeQueryFailed": "Could not get data for validator {{node}}",
@ -535,13 +527,7 @@
"unsupportedChainIdError": "You're connected to an unsupported network", "unsupportedChainIdError": "You're connected to an unsupported network",
"userRejectionError": "Please authorise this website to access your Ethereum account", "userRejectionError": "Please authorise this website to access your Ethereum account",
"unknownEthereumConnectionError": "An unknown error occurred. Check the console in your browser's web developer tools for more details", "unknownEthereumConnectionError": "An unknown error occurred. Check the console in your browser's web developer tools for more details",
"stakingDescriptionTitle": "How does staking on Vega work?", "validatorTableIntro": "View the validator profile pitches and discussion",
"stakingDescription1": "1. VEGA is an ERC20 token. Associate it with a Vega wallet using the",
"stakingDescription2a": "2. Use this site and your Vega wallet to nominate a validator.",
"stakingDescription2b": "View the validator profile pitches and discussion",
"stakingDescription3": "3. Earn a share of trading fees and treasury rewards for each full epoch staked",
"stakingDescription4": "4. Move your stake if your validator is penalised",
"stakingBridge": "staking bridge",
"onTheForum": "on the forum", "onTheForum": "on the forum",
"readMoreStaking": "Read more about staking on Vega", "readMoreStaking": "Read more about staking on Vega",
"networkDown": "This site is not currently connecting to the network please try again later.", "networkDown": "This site is not currently connecting to the network please try again later.",

View File

@ -1,7 +1,11 @@
import { format, isFuture } from 'date-fns'; import { format, isFuture } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { CurrentProposalState } from '../current-proposal-state'; import { CurrentProposalState } from '../current-proposal-state';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
@ -17,6 +21,7 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
const terms = proposal?.terms; const terms = proposal?.terms;
return ( return (
<RoundedWrapper>
<KeyValueTable data-testid="proposal-change-table"> <KeyValueTable data-testid="proposal-change-table">
<KeyValueTableRow> <KeyValueTableRow>
{t('id')} {t('id')}
@ -68,5 +73,6 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
{proposal?.terms.change.__typename} {proposal?.terms.change.__typename}
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
); );
}; };

View File

@ -1,7 +1,8 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Lozenge } from '@vegaprotocol/ui-toolkit'; import { Lozenge } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { shorten } from '@vegaprotocol/react-helpers'; import { shorten } from '@vegaprotocol/react-helpers';
import { SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -96,12 +97,7 @@ export const ProposalHeader = ({
return ( return (
<div className="text-sm mb-2"> <div className="text-sm mb-2">
<header data-testid="proposal-title"> <header data-testid="proposal-title">
<h2 <SubHeading title={titleContent || t('Unknown proposal')} />
{...(title && title.length > titleContent.length && { title: title })}
className="text-lg mx-0 mt-0 mb-1 font-semibold"
>
{titleContent || t('Unknown proposal')}
</h2>
</header> </header>
{description && ( {description && (
<div className="mb-4" data-testid="proposal-description"> <div className="mb-4" data-testid="proposal-description">

View File

@ -1,13 +1,12 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../../../components/heading';
export const ProposalNotFound = () => { export const ProposalNotFound = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<section data-testid="proposal-not-found"> <section data-testid="proposal-not-found">
<header> <header>
<h2 className="text-lg mx-0 mt-0 mb-1 font-semibold"> <SubHeading title={t('ProposalNotFound')} />
{t('ProposalNotFound')}
</h2>
</header> </header>
<p>{t('ProposalNotFoundDetails')}</p> <p>{t('ProposalNotFoundDetails')}</p>
</section> </section>

View File

@ -1,5 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
@ -11,7 +12,7 @@ export const ProposalTermsJson = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<section> <section>
<h2>{t('proposalTerms')}</h2> <SubHeading title={t('proposalTerms')} />
<SyntaxHighlighter data={terms} /> <SyntaxHighlighter data={terms} />
</section> </section>
); );

View File

@ -3,16 +3,18 @@ import {
KeyValueTable, KeyValueTable,
KeyValueTableRow, KeyValueTableRow,
Thumbs, Thumbs,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { import {
formatNumber, formatNumber,
formatNumberPercentage, formatNumberPercentage,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks'; import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalType } from '../proposal/proposal';
interface ProposalVotesTableProps { interface ProposalVotesTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
@ -55,8 +57,10 @@ export const ProposalVotesTable = ({
: t('byLiquidityVote'); : t('byLiquidityVote');
return ( return (
<>
<SubHeading title={t('voteBreakdown')} />
<RoundedWrapper>
<KeyValueTable <KeyValueTable
title={t('voteBreakdown')}
data-testid="proposal-votes-table" data-testid="proposal-votes-table"
numerical={true} numerical={true}
headingLevel={4} headingLevel={4}
@ -92,7 +96,11 @@ export const ProposalVotesTable = ({
{isUpdateMarket && ( {isUpdateMarket && (
<KeyValueTableRow> <KeyValueTableRow>
{t('participationLPMet')} {t('participationLPMet')}
{participationLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />} {participationLPMet ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow> </KeyValueTableRow>
)} )}
<KeyValueTableRow> <KeyValueTableRow>
@ -147,12 +155,14 @@ export const ProposalVotesTable = ({
{t('yesPercentage')} {t('yesPercentage')}
{formatNumberPercentage(yesPercentage, 2)} {formatNumberPercentage(yesPercentage, 2)}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
{t('noPercentage')} {t('noPercentage')}
{formatNumberPercentage(noPercentage, 2)} {formatNumberPercentage(noPercentage, 2)}
</KeyValueTableRow> </KeyValueTableRow>
</> </>
)} )}
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
</>
); );
}; };

View File

@ -75,7 +75,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
<AsyncRenderer data={params} loading={loading} error={error}> <AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal"> <section data-testid="proposal">
<ProposalHeader proposal={proposal} /> <ProposalHeader proposal={proposal} />
<div className="mb-8"> <div className="mb-10">
<ProposalChangeTable proposal={proposal} /> <ProposalChangeTable proposal={proposal} />
</div> </div>
{proposal.terms.change.__typename === 'NewAsset' && {proposal.terms.change.__typename === 'NewAsset' &&
@ -87,7 +87,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
lifetimeLimit={proposal.terms.change.source.lifetimeLimit} lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/> />
) : null} ) : null}
<div className="mb-8"> <div className="mb-12">
<VoteDetails <VoteDetails
proposal={proposal} proposal={proposal}
proposalType={proposalType} proposalType={proposalType}
@ -95,7 +95,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
spamProtectionMinTokens={params?.spam_protection_voting_min_tokens} spamProtectionMinTokens={params?.spam_protection_voting_min_tokens}
/> />
</div> </div>
<div className="mb-8"> <div className="mb-10">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} /> <ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div> </div>
<ProposalTermsJson terms={proposal.terms} /> <ProposalTermsJson terms={proposal.terms} />

View File

@ -1,7 +1,7 @@
import { isFuture } from 'date-fns'; import { isFuture } from 'date-fns';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter'; import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes'; import Routes from '../../../routes';
@ -47,7 +47,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
return ( return (
<> <>
<div className="grid xs:grid-cols-2 items-center mb-4"> <div className="grid xs:grid-cols-2 items-center">
<Heading <Heading
centerContent={false} centerContent={false}
marginBottom={false} marginBottom={false}
@ -63,8 +63,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
</Button> </Button>
</Link> </Link>
</div> </div>
<div> <p className="mb-8">
<p className="mb-4">
{t( {t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.` `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network.`
)}{' '} )}{' '}
@ -76,12 +75,11 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
{t(`Find out more about Vega governance`)} {t(`Find out more about Vega governance`)}
</ExternalLink> </ExternalLink>
</p> </p>
</div>
{proposals.length > 0 && ( {proposals.length > 0 && (
<ProposalsListFilter setFilterString={setFilterString} /> <ProposalsListFilter setFilterString={setFilterString} />
)} )}
<section className="-mx-4 p-4 mb-8 bg-neutral-800"> <section className="-mx-4 p-4 mb-8 bg-neutral-800">
<h2 className="text-xl mb-2">{t('openProposals')}</h2> <SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ? ( {sortedProposals.open.length > 0 ? (
<ul data-testid="open-proposals"> <ul data-testid="open-proposals">
{sortedProposals.open.filter(filterPredicate).map((proposal) => ( {sortedProposals.open.filter(filterPredicate).map((proposal) => (
@ -95,7 +93,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
)} )}
</section> </section>
<section> <section>
<h2 className="text-xl mb-2">{t('closedProposals')}</h2> <SubHeading title={t('closedProposals')} />
{sortedProposals.closed.length > 0 ? ( {sortedProposals.closed.length > 0 ? (
<ul data-testid="closed-proposals"> <ul data-testid="closed-proposals">
{sortedProposals.closed.filter(filterPredicate).map((proposal) => ( {sortedProposals.closed.filter(filterPredicate).map((proposal) => (

View File

@ -9,6 +9,7 @@ import { useVoteInformation } from '../../hooks';
import { useUserVote } from './use-user-vote'; import { useUserVote } from './use-user-vote';
import { CurrentProposalStatus } from '../current-proposal-status'; import { CurrentProposalStatus } from '../current-proposal-status';
import { VoteButtonsContainer } from './vote-buttons'; import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal'; import { ProposalType } from '../proposal/proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
@ -53,7 +54,7 @@ export const VoteDetails = ({
<> <>
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<section> <section>
<h3 className="text-xl mb-2">{t('liquidityVotes')}</h3> <SubHeading title={t('liquidityVotes')} />
<p> <p>
<span> <span>
<CurrentProposalStatus proposal={proposal} /> <CurrentProposalStatus proposal={proposal} />
@ -102,7 +103,7 @@ export const VoteDetails = ({
</section> </section>
)} )}
<section> <section>
<h3 className="text-xl mb-2">{t('tokenVotes')}</h3> <SubHeading title={t('tokenVotes')} />
<p> <p>
<span> <span>
<CurrentProposalStatus proposal={proposal} /> <CurrentProposalStatus proposal={proposal} />
@ -176,8 +177,8 @@ export const VoteDetails = ({
<p>{t('votingThresholdInfo')}</p> <p>{t('votingThresholdInfo')}</p>
)} )}
{pubKey ? ( {pubKey ? (
<> <section className="mt-10">
<h3 className="text-xl mb-2">{t('yourVote')}</h3> <SubHeading title={t('yourVote')} />
{proposal && ( {proposal && (
<VoteButtonsContainer <VoteButtonsContainer
voteState={voteState} voteState={voteState}
@ -189,7 +190,7 @@ export const VoteDetails = ({
className="flex" className="flex"
/> />
)} )}
</> </section>
) : ( ) : (
<ConnectToVega /> <ConnectToVega />
)} )}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Heading } from '../../components/heading'; import { Heading, SubHeading } from '../../components/heading';
import { ExternalLinks } from '@vegaprotocol/react-helpers'; import { ExternalLinks } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../contexts/app-state/app-state-context'; import { useAppState } from '../../contexts/app-state/app-state-context';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
@ -32,7 +32,7 @@ const Home = ({ name }: RouteChildProps) => {
/> />
</HomeSection> </HomeSection>
<HomeSection> <HomeSection>
<h2>{t('Token Vesting')}</h2> <SubHeading title={t('Token Vesting')} />
<p> <p>
{t( {t(
'The vesting contract holds VEGA tokens until they have become unlocked.' 'The vesting contract holds VEGA tokens until they have become unlocked.'
@ -68,7 +68,7 @@ const Home = ({ name }: RouteChildProps) => {
</Link> </Link>
</HomeSection> </HomeSection>
<HomeSection> <HomeSection>
<h2 className="uppercase">{t('Use your Vega tokens')}</h2> <SubHeading title={t('Use your Vega tokens')} />
<p> <p>
{t( {t(
'To use your tokens on the Vega network they need to be associated with a Vega wallet/key.' 'To use your tokens on the Vega network they need to be associated with a Vega wallet/key.'
@ -103,7 +103,7 @@ const Home = ({ name }: RouteChildProps) => {
<div className="flex gap-12"> <div className="flex gap-12">
<div className="flex-1"> <div className="flex-1">
<HomeSection> <HomeSection>
<h2>{t('Staking')}</h2> <SubHeading title={t('Staking')} />
<p> <p>
{t( {t(
'VEGA token holders can nominate a validator node and receive staking rewards.' 'VEGA token holders can nominate a validator node and receive staking rewards.'
@ -120,7 +120,7 @@ const Home = ({ name }: RouteChildProps) => {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<HomeSection> <HomeSection>
<h2>{t('Governance')}</h2> <SubHeading title={t('Governance')} />
<p> <p>
{t( {t(
'VEGA token holders can vote on proposed changes to the network and create proposals.' 'VEGA token holders can vote on proposed changes to the network and create proposals.'
@ -143,5 +143,5 @@ const Home = ({ name }: RouteChildProps) => {
export default Home; export default Home;
export const HomeSection = ({ children }: { children: React.ReactNode }) => { export const HomeSection = ({ children }: { children: React.ReactNode }) => {
return <section className="mb-8">{children}</section>; return <section className="mb-12">{children}</section>;
}; };

View File

@ -2,7 +2,11 @@ import { useTranslation } from 'react-i18next';
import { Callout, Link, Intent, Splash } from '@vegaprotocol/ui-toolkit'; import { Callout, Link, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { useTranches } from '../../../hooks/use-tranches'; import { useTranches } from '../../../hooks/use-tranches';
import type { BigNumber } from '../../../lib/bignumber'; import type { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number'; import { formatNumber } from '../../../lib/format-number';
@ -43,6 +47,7 @@ export const TokenDetails = ({
return ( return (
<div className="token-details"> <div className="token-details">
<RoundedWrapper>
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow> <KeyValueTableRow>
{t('Token address').toUpperCase()} {t('Token address').toUpperCase()}
@ -78,13 +83,14 @@ export const TokenDetails = ({
{t('Circulating supply').toUpperCase()} {t('Circulating supply').toUpperCase()}
<TokenDetailsCirculating tranches={tranches} /> <TokenDetailsCirculating tranches={tranches} />
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
{t('Staked on Vega validator').toUpperCase()} {t('Staked on Vega validator').toUpperCase()}
<span data-testid="staked" className="font-mono"> <span data-testid="staked" className="font-mono">
{formatNumber(totalAssociated, 2)} {formatNumber(totalAssociated, 2)}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
</div> </div>
); );
}; };

View File

@ -2,8 +2,12 @@ import { useEthereumConfig } from '@vegaprotocol/web3';
import { StakingWalletsContainer } from './components/staking-wallets-container/staking-wallets-container'; import { StakingWalletsContainer } from './components/staking-wallets-container/staking-wallets-container';
import { AssociatePage } from './associate-page'; import { AssociatePage } from './associate-page';
import { AssociatePageNoVega } from './associate-page-no-vega'; import { AssociatePageNoVega } from './associate-page-no-vega';
import { Heading } from '../../../components/heading';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const AssociateContainer = () => { export const AssociateContainer = () => {
const { t } = useTranslation();
const { config } = useEthereumConfig(); const { config } = useEthereumConfig();
if (!config) { if (!config) {
@ -11,6 +15,8 @@ export const AssociateContainer = () => {
} }
return ( return (
<>
<Heading title={t('pageTitleAssociate')} />
<StakingWalletsContainer> <StakingWalletsContainer>
{({ address, pubKey }) => {({ address, pubKey }) =>
pubKey ? ( pubKey ? (
@ -24,6 +30,7 @@ export const AssociateContainer = () => {
) )
} }
</StakingWalletsContainer> </StakingWalletsContainer>
</>
); );
}; };

View File

@ -2,16 +2,25 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core'; import { useWeb3React } from '@web3-react/core';
import { EthConnectPrompt } from '../../../components/eth-connect-prompt'; import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
import { DisassociatePage } from './components/disassociate-page'; import { DisassociatePage } from './components/disassociate-page';
import { Heading } from '../../../components/heading';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const DisassociateContainer = () => { export const DisassociateContainer = () => {
const { t } = useTranslation();
const { account } = useWeb3React(); const { account } = useWeb3React();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
if (!account) { return (
return <EthConnectPrompt />; <>
} <Heading title={t('pageTitleDisassociate')} />
{!account ? (
return <DisassociatePage address={account} vegaKey={pubKey ?? ''} />; <EthConnectPrompt />
) : (
<DisassociatePage address={account} vegaKey={pubKey ?? ''} />
)}
</>
);
}; };
export default DisassociateContainer; export default DisassociateContainer;

View File

@ -45,7 +45,7 @@ export const EpochData = () => {
{data?.epoch && {data?.epoch &&
data.epoch.timestamps.start && data.epoch.timestamps.start &&
data?.epoch.timestamps.expiry && ( data?.epoch.timestamps.expiry && (
<div className="mb-8"> <div className="mb-10">
<EpochCountdown <EpochCountdown
id={data.epoch.id} id={data.epoch.id}
startDate={new Date(data.epoch.timestamps.start)} startDate={new Date(data.epoch.timestamps.start)}

View File

@ -1,42 +0,0 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { StakingIntro } from './staking-intro';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
}));
describe('Staking', () => {
it('should render the component', () => {
render(
<Router>
<StakingIntro />
</Router>
);
expect(screen.getByTestId('staking-intro')).toBeInTheDocument();
expect(screen.getByTestId('callout')).toBeInTheDocument();
expect(
screen.getByText(
'1. VEGA is an ERC20 token. Associate it with a Vega wallet using the'
)
).toBeInTheDocument();
expect(screen.getByTestId('staking-associate-link')).toBeInTheDocument();
expect(
screen.getByText(
'2. Use this site and your Vega wallet to nominate a validator. View the validator profile pitches and discussion'
)
).toBeInTheDocument();
expect(screen.getByTestId('validator-forum-link')).toBeInTheDocument();
expect(
screen.getByText(
'3. Earn a share of trading fees and treasury rewards for each full epoch staked'
)
).toBeInTheDocument();
expect(
screen.getByText('4. Move your stake if your validator is penalised')
).toBeInTheDocument();
expect(screen.getByTestId('staking-guide-link')).toBeInTheDocument();
});
});

View File

@ -1,60 +0,0 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Button,
Callout,
Intent,
Link as UTLink,
} from '@vegaprotocol/ui-toolkit';
import { createDocsLinks, ExternalLinks } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import Routes from '../../../routes/routes';
export const StakingIntro = () => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
return (
<section className="mb-8" data-testid="staking-intro">
<Callout
intent={Intent.Primary}
iconName="help"
title={t('stakingDescriptionTitle')}
>
<ol className="mb-4">
<li>
{t('stakingDescription1')}{' '}
<Link
to={Routes.ASSOCIATE}
className="underline"
data-testid="staking-associate-link"
>
{t('stakingBridge')}
</Link>
</li>
<li>
{t('stakingDescription2a')} {t('stakingDescription2b')}{' '}
<UTLink
href={ExternalLinks.VALIDATOR_FORUM}
target="_blank"
data-testid="validator-forum-link"
>
{t('onTheForum')}
</UTLink>
</li>
<li>{t('stakingDescription3')}</li>
<li>{t('stakingDescription4')}</li>
</ol>
{VEGA_DOCS_URL && (
<UTLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
target="_blank"
data-testid="staking-guide-link"
>
<Button>{t('readMoreStaking')}</Button>
</UTLink>
)}
</Callout>
</section>
);
};

View File

@ -1,15 +1,33 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { StakingIntro } from './staking-intro';
import { EpochData } from './epoch-data'; import { EpochData } from './epoch-data';
import { useEnvironment } from '@vegaprotocol/environment';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/react-helpers';
import { Heading } from '../../../components/heading';
import React from 'react';
export const Staking = () => { export const Staking = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
return ( return (
<> <>
<StakingIntro /> <Heading title={t('pageTitleValidators')} />
<section> <section>
<h2 className="text-2xl uppercase">{t('Validator nodes')}</h2> <p className="mb-12">
{t('stakingIntro')}{' '}
{VEGA_DOCS_URL && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
target="_blank"
data-testid="staking-guide-link"
className="text-white"
>
{t('readMoreStaking')}
</ExternalLink>
)}
</p>
<EpochData data-testid="epoch-data" /> <EpochData data-testid="epoch-data" />
</section> </section>
</> </>

View File

@ -212,10 +212,6 @@ describe('Consensus validators table', () => {
const grid = screen.getByTestId('consensus-validators-table'); const grid = screen.getByTestId('consensus-validators-table');
act(() => {
fireEvent.click(screen.getByTestId('show-all-validators'));
});
expect( expect(
grid.querySelector('[role="gridcell"][col-id="validator"]') grid.querySelector('[role="gridcell"][col-id="validator"]')
).toHaveTextContent('T-800 Terminator'); ).toHaveTextContent('T-800 Terminator');

View File

@ -1,3 +1,4 @@
import type { Dispatch, SetStateAction } from 'react';
import { forwardRef, useMemo, useRef, useState } from 'react'; import { forwardRef, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -25,22 +26,87 @@ import {
VotingPowerRenderer, VotingPowerRenderer,
} from './shared'; } from './shared';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community'; import type { ColDef, RowHeightParams } from 'ag-grid-community';
import type { ValidatorsTableProps } from './shared'; import type { ValidatorsTableProps } from './shared';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers'; import {
formatNumber,
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/react-helpers';
interface CanonisedConsensusNodeProps { interface CanonisedConsensusNodeProps {
id: string; id: string;
[ValidatorFields.RANKING_INDEX]: number;
[ValidatorFields.VALIDATOR]: { [ValidatorFields.VALIDATOR]: {
avatarUrl: string | null | undefined; avatarUrl: string | null | undefined;
name: string; name: string;
}; };
[ValidatorFields.STAKE]: string; [ValidatorFields.STAKE]: string;
[ValidatorFields.STAKE_SHARE]: string;
[ValidatorFields.PENDING_STAKE]: string;
[ValidatorFields.NORMALISED_VOTING_POWER]: string; [ValidatorFields.NORMALISED_VOTING_POWER]: string;
[ValidatorFields.UNNORMALISED_VOTING_POWER]: string | null;
[ValidatorFields.STAKE_SHARE]: string;
[ValidatorFields.STAKED_BY_DELEGATES]: string;
[ValidatorFields.STAKED_BY_OPERATOR]: string;
[ValidatorFields.PERFORMANCE_SCORE]: string;
[ValidatorFields.PERFORMANCE_PENALTY]: string;
[ValidatorFields.OVERSTAKED_AMOUNT]: string;
[ValidatorFields.OVERSTAKING_PENALTY]: string;
[ValidatorFields.TOTAL_PENALTIES]: string;
[ValidatorFields.PENDING_STAKE]: string;
} }
const getRowHeight = (params: RowHeightParams) => {
if (params.data.placeholderForTopThird) {
// Note: this value will change if the height of the top third cell renderer changes
return 138;
}
return 52;
};
const TopThirdCellRenderer = (
// @ts-ignore no exported type that matches params from AG-grid
params,
setHideTopThird: Dispatch<SetStateAction<boolean>>
) => {
const { t } = useTranslation();
return (
<a
href="/"
onClick={(e) => {
e.preventDefault();
setHideTopThird(false);
}}
className="grid grid-cols-[60px_1fr] w-full h-full py-4 px-0 text-sm text-white text-center overflow-scroll"
>
<div className="text-xs text-left px-3">
{params?.data?.rankingDisplay}
</div>
<div className="whitespace-normal px-3">
<div className="mb-4">
<Button
data-testid="show-all-validators"
rightIcon="arrow-right"
className="inline-flex items-center"
>
{t('Reveal top validators')}
</Button>
</div>
<p className="font-semibold text-white mb-0">
{t(
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
)}
</p>
<p>
{t(
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
)}
</p>
</div>
</a>
);
};
export const ConsensusValidatorsTable = ({ export const ConsensusValidatorsTable = ({
data, data,
previousEpochData, previousEpochData,
@ -147,24 +213,86 @@ export const ConsensusValidatorsTable = ({
// 1/3 of the validators and we really want people not to stake any more to // 1/3 of the validators and we really want people not to stake any more to
// that group, because we want to make it require as many difference // that group, because we want to make it require as many difference
// validators to collude as possible to halt the network, so we hide them. // validators to collude as possible to halt the network, so we hide them.
const removeTopThirdOfStakeScores = canonisedNodes.reduce( const { topThird, remaining } = canonisedNodes.reduce(
(acc, node) => { (acc, node) => {
if (acc.cumulativeScore < 3333) { if (acc.cumulativeScore < 100 / 3) {
acc.cumulativeScore += Number( acc.cumulativeScore += parseFloat(
node[ValidatorFields.NORMALISED_VOTING_POWER] node[ValidatorFields.NORMALISED_VOTING_POWER]
); );
acc.topThird.push(node);
return acc; return acc;
} }
acc.remaining.push(node); acc.remaining.push(node);
return acc; return acc;
}, },
{ remaining: [], cumulativeScore: 0 } as { { topThird: [], remaining: [], cumulativeScore: 0 } as {
topThird: CanonisedConsensusNodeProps[];
remaining: CanonisedConsensusNodeProps[]; remaining: CanonisedConsensusNodeProps[];
cumulativeScore: number; cumulativeScore: number;
} }
); );
return removeTopThirdOfStakeScores.remaining; // We need to combine the top third of validators into a single node, this
// way the combined values can be passed to AG grid so that the combined cell's
// values are correct for ordering.
const combinedTopThird = topThird.reduce((acc, node) => {
const {
[ValidatorFields.STAKE]: stake,
[ValidatorFields.STAKE_SHARE]: stakeShare,
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
} = node;
const {
[ValidatorFields.STAKE]: accStake,
[ValidatorFields.STAKE_SHARE]: accStakeShare,
[ValidatorFields.PENDING_STAKE]: accPendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
} = acc;
return {
...acc,
[ValidatorFields.STAKE]: formatNumber(
toBigNum(accStake, decimals).plus(toBigNum(stake, decimals)),
2
),
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
new BigNumber(parseFloat(accStakeShare)).plus(
new BigNumber(parseFloat(stakeShare))
),
2
),
[ValidatorFields.PENDING_STAKE]: formatNumber(
toBigNum(accPendingStake, decimals).plus(
toBigNum(pendingStake, decimals)
),
2
),
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
new BigNumber(parseFloat(normalisedVotingPower))
),
2
),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
new BigNumber(parseFloat(accTotalPenalties)).plus(
new BigNumber(parseFloat(totalPenalties))
),
2
),
};
});
return [
{
placeholderForTopThird: true,
rankingDisplay: topThird.length === 1 ? '1' : `1 - ${topThird.length}`,
...combinedTopThird,
},
...remaining,
];
}, [data, decimals, hideTopThird, previousEpochData, totalStake]); }, [data, decimals, hideTopThird, previousEpochData, totalStake]);
const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => { const ConsensusTable = forwardRef<AgGridReact>((_, gridRef) => {
@ -173,7 +301,7 @@ export const ConsensusValidatorsTable = ({
{ {
field: ValidatorFields.RANKING_INDEX, field: ValidatorFields.RANKING_INDEX,
headerName: '#', headerName: '#',
width: 40, width: 60,
pinned: 'left', pinned: 'left',
}, },
{ {
@ -227,36 +355,12 @@ export const ConsensusValidatorsTable = ({
return ( return (
<div data-testid="consensus-validators-table"> <div data-testid="consensus-validators-table">
{hideTopThird && (
<div className="mb-6 py-4 px-4 md:px-12 bg-neutral-900 text-sm text-center">
<div className="mb-4">
<Button
data-testid="show-all-validators"
icon="list"
className="inline-flex items-center"
onClick={() => setHideTopThird(false)}
>
{t('Reveal top validators')}
</Button>
</div>
<p className="font-semibold">
{t(
'Validators with too great a stake share will have the staking rewards for their delegators penalised.'
)}
</p>
<p className="mb-0">
{t(
'To avoid penalties and increase decentralisation of the network, delegate to validators below.'
)}
</p>
</div>
)}
{nodes.length > 0 && ( {nodes.length > 0 && (
<AgGrid <AgGrid
domLayout="autoHeight" domLayout="autoHeight"
style={{ width: '100%' }} style={{ width: '100%' }}
customThemeParams={NODE_LIST_GRID_STYLES} customThemeParams={NODE_LIST_GRID_STYLES}
rowHeight={52} getRowHeight={(params: RowHeightParams) => getRowHeight(params)}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
animateRows={true} animateRows={true}
suppressCellFocus={true} suppressCellFocus={true}
@ -267,6 +371,13 @@ export const ConsensusValidatorsTable = ({
onCellClicked={(event) => { onCellClicked={(event) => {
navigate(event.data.id); navigate(event.data.id);
}} }}
isFullWidthRow={(params) =>
params.rowNode.data.placeholderForTopThird
}
// @ts-ignore no exported type that matches params from AG-grid
fullWidthCellRenderer={(params) =>
TopThirdCellRenderer(params, setHideTopThird)
}
/> />
)} )}
</div> </div>

View File

@ -57,7 +57,7 @@ export const defaultColDef = {
resizable: true, resizable: true,
autoHeight: true, autoHeight: true,
comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b), comparator: (a: string, b: string) => parseFloat(a) - parseFloat(b),
cellStyle: { margin: '10px 0' }, cellStyle: { margin: '10px 0', padding: '0 12px' },
tooltipComponent: TooltipCellComponent, tooltipComponent: TooltipCellComponent,
tooltipShowDelay: 0, tooltipShowDelay: 0,
tooltipHideDelay: 0, tooltipHideDelay: 0,

View File

@ -3,11 +3,6 @@ import { Trans, useTranslation } from 'react-i18next';
import { ConsensusValidatorsTable } from './consensus-validators-table'; import { ConsensusValidatorsTable } from './consensus-validators-table';
import { StandbyPendingValidatorsTable } from './standby-pending-validators-table'; import { StandbyPendingValidatorsTable } from './standby-pending-validators-table';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type {
NodesQuery,
NodesFragmentFragment,
} from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
import { formatNumber } from '../../../../lib/format-number'; import { formatNumber } from '../../../../lib/format-number';
import { import {
createDocsLinks, createDocsLinks,
@ -15,8 +10,14 @@ import {
toBigNum, toBigNum,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { Link as UTLink } from '@vegaprotocol/ui-toolkit'; import { Link as UTLink } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type {
NodesQuery,
NodesFragmentFragment,
} from '../__generated___/Nodes';
import type { PreviousEpochQuery } from '../../__generated___/PreviousEpoch';
export interface ValidatorsTableProps { export interface ValidatorsTableProps {
data: NodesQuery | undefined; data: NodesQuery | undefined;
@ -99,7 +100,7 @@ export const ValidatorTables = ({
<div data-testid="validator-tables"> <div data-testid="validator-tables">
{consensusValidators.length > 0 && ( {consensusValidators.length > 0 && (
<> <>
<h2>{t('status-tendermint')}</h2> <SubHeading title={t('status-tendermint')} />
<ConsensusValidatorsTable <ConsensusValidatorsTable
data={consensusValidators} data={consensusValidators}
previousEpochData={previousEpochData} previousEpochData={previousEpochData}
@ -109,7 +110,7 @@ export const ValidatorTables = ({
)} )}
{standbyValidators.length > 0 && ( {standbyValidators.length > 0 && (
<> <>
<h2>{t('status-ersatz')}</h2> <SubHeading title={t('status-ersatz')} />
<p> <p>
<Trans <Trans
i18nKey="ersatzDescription" i18nKey="ersatzDescription"
@ -129,7 +130,7 @@ export const ValidatorTables = ({
)} )}
{pendingValidators.length > 0 && ( {pendingValidators.length > 0 && (
<> <>
<h2>{t('status-pending')}</h2> <SubHeading title={t('status-pending')} />
<p> <p>
{VEGA_DOCS_URL && ( {VEGA_DOCS_URL && (
<> <>

View File

@ -1,32 +1,11 @@
import React from 'react'; import { Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Outlet, useMatch } from 'react-router-dom';
import { Heading } from '../../components/heading';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..'; import type { RouteChildProps } from '..';
const StakingRouter = ({ name }: RouteChildProps) => { const StakingRouter = ({ name }: RouteChildProps) => {
useDocumentTitle(name); useDocumentTitle(name);
const { t } = useTranslation();
const associate = useMatch('/validators/associate');
const disassociate = useMatch('/validators/disassociate');
const title = React.useMemo(() => { return <Outlet />;
if (associate) {
return t('pageTitleAssociate');
} else if (disassociate) {
return t('pageTitleDisassociate');
}
return t('pageTitleStaking');
}, [associate, disassociate, t]);
return (
<>
<Heading title={title} />
<Outlet />
</>
);
}; };
export default StakingRouter; export default StakingRouter;

View File

@ -8,6 +8,8 @@ import {
removePaginationWrapper, removePaginationWrapper,
toBigNum, toBigNum,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { Link } from 'react-router-dom';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { EpochCountdown } from '../../../components/epoch-countdown'; import { EpochCountdown } from '../../../components/epoch-countdown';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { ConnectToVega } from '../../../components/connect-to-vega'; import { ConnectToVega } from '../../../components/connect-to-vega';
@ -16,6 +18,8 @@ import { ValidatorTable } from './validator-table';
import { YourStake } from './your-stake'; import { YourStake } from './your-stake';
import NodeContainer from './nodes-container'; import NodeContainer from './nodes-container';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { Heading, SubHeading } from '../../../components/heading';
import Routes from '../../routes';
import type { StakingQuery } from './__generated___/Staking'; import type { StakingQuery } from './__generated___/Staking';
import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch'; import type { PreviousEpochQuery } from '../__generated___/PreviousEpoch';
@ -100,11 +104,18 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
return ( return (
<div data-testid="staking-node"> <div data-testid="staking-node">
<h2 data-test-id="validator-node-title" className="text-2xl break-word"> <div className="flex items-center gap-1">
{nodeInfo.name <Icon name={'chevron-left'} />
? t('validatorTitle', { nodeName: nodeInfo.name }) <Link className="underline" to={Routes.VALIDATORS}>
: t('validatorTitle', { nodeName: t('validatorTitleFallback') })} {t('All validators')}
</h2> </Link>
</div>
<Heading
title={
nodeInfo.name ||
t('validatorTitle', { nodeName: t('validatorTitleFallback') })
}
/>
<section className="mb-4"> <section className="mb-4">
<ValidatorTable <ValidatorTable
node={nodeInfo} node={nodeInfo}
@ -113,7 +124,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
/> />
</section> </section>
{data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && ( {data?.epoch.timestamps.start && data?.epoch.timestamps.expiry && (
<section className="mb-4"> <section className="mb-10">
<EpochCountdown <EpochCountdown
id={data.epoch.id} id={data.epoch.id}
startDate={new Date(data?.epoch.timestamps.start)} startDate={new Date(data?.epoch.timestamps.start)}
@ -142,7 +153,7 @@ export const StakingNode = ({ data, previousEpochData }: StakingNodeProps) => {
</> </>
) : ( ) : (
<> <>
<h2>{t('Connect to see your stake')}</h2> <SubHeading title={t('Connect to see your stake')} />
<ConnectToVega /> <ConnectToVega />
</> </>
)} )}

View File

@ -3,7 +3,6 @@ import * as Sentry from '@sentry/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { usePartyDelegationsLazyQuery } from './__generated___/PartyDelegations'; import { usePartyDelegationsLazyQuery } from './__generated___/PartyDelegations';
import { TokenInput } from '../../../components/token-input'; import { TokenInput } from '../../../components/token-input';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
@ -16,11 +15,6 @@ import {
Radio, Radio,
RadioGroup, RadioGroup,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type {
DelegateSubmissionBody,
UndelegateSubmissionBody,
} from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
useNetworkParam, useNetworkParam,
NetworkParams, NetworkParams,
@ -29,6 +23,12 @@ import {
removePaginationWrapper, removePaginationWrapper,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { useBalances } from '../../../lib/balances/balances-store'; import { useBalances } from '../../../lib/balances/balances-store';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { SubHeading } from '../../../components/heading';
import type {
DelegateSubmissionBody,
UndelegateSubmissionBody,
} from '@vegaprotocol/wallet';
export enum FormState { export enum FormState {
Default, Default,
@ -186,15 +186,17 @@ export const StakingForm = ({
return ( return (
<> <>
<h2>{t('Manage your stake')}</h2> <SubHeading title={t('Manage your stake')} />
{formState === FormState.Default && {formState === FormState.Default &&
availableStakeToAdd.isEqualTo(0) && availableStakeToAdd.isEqualTo(0) &&
availableStakeToRemove.isEqualTo(0) && ( availableStakeToRemove.isEqualTo(0) && (
<div> <div className="mb-4">
{lien.isGreaterThan(0) ? ( {lien.isGreaterThan(0) ? (
<span className="text-red">{t('stakeNodeWrongVegaKey')}</span> <span className="text-vega-red">
{t('stakeNodeWrongVegaKey')}
</span>
) : ( ) : (
<span className="text-red">{t('stakeNodeNone')}</span> <span className="text-vega-orange">{t('stakeNodeNone')}</span>
)} )}
</div> </div>
)} )}

View File

@ -4,12 +4,17 @@ import { useTranslation } from 'react-i18next';
import countryData from '../../../components/country-selector/country-data'; import countryData from '../../../components/country-selector/country-data';
import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit'; import { Link as UTLink, Link } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { formatNumber } from '../../../lib/format-number'; import { formatNumber } from '../../../lib/format-number';
import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers'; import { ExternalLinks, toBigNum } from '@vegaprotocol/react-helpers';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { SubHeading } from '../../../components/heading';
import { import {
getFormattedPerformanceScore, getFormattedPerformanceScore,
getNormalisedVotingPower, getNormalisedVotingPower,
@ -89,8 +94,10 @@ export const ValidatorTable = ({
); );
return ( return (
<div className="mb-8" data-testid="validator-table"> <div className="my-12" data-testid="validator-table">
<KeyValueTable data-testid="validator-table-profile" title={t('PROFILE')}> <SubHeading title={t('profile')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('id')}</span> <span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id"> <ValidatorTableCell dataTestId="validator-id">
@ -103,18 +110,21 @@ export const ValidatorTable = ({
<a href={node.infoUrl}>{node.infoUrl}</a> <a href={node.infoUrl}>{node.infoUrl}</a>
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
<span> <span>
<strong>{t('STATUS')}</strong> <strong>{t('STATUS')}</strong>
</span> </span>
<span data-testid="validator-status"> <span data-testid="validator-status">
<strong>{t(statusTranslationKey(node.rankingScore.status))}</strong> <strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
<div className="mb-6 text-sm"> <div className="mt-[-1.5rem] mb-10">
{t('stakingDescription2b')}{' '} {t('validatorTableIntro')}{' '}
<UTLink <UTLink
href={ExternalLinks.VALIDATOR_FORUM} href={ExternalLinks.VALIDATOR_FORUM}
target="_blank" target="_blank"
@ -124,7 +134,9 @@ export const ValidatorTable = ({
</UTLink> </UTLink>
</div> </div>
<KeyValueTable data-testid="validator-table-address" title={t('ADDRESS')}> <SubHeading title={t('ADDRESS')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span> <span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key"> <ValidatorTableCell dataTestId="validator-public-key">
@ -138,7 +150,7 @@ export const ValidatorTable = ({
t('not available')} t('not available')}
</ValidatorTableCell> </ValidatorTableCell>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span> <span>{t('ETHEREUM ADDRESS')}</span>
<span> <span>
<Link <Link
@ -151,8 +163,11 @@ export const ValidatorTable = ({
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
<KeyValueTable data-testid="validator-table-stake" title={t('STAKE')}> <SubHeading title={t('STAKE')} />
<RoundedWrapper>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span> <span>{t('STAKED BY OPERATOR')}</span>
<span data-testid="staked-by-operator"> <span data-testid="staked-by-operator">
@ -181,16 +196,16 @@ export const ValidatorTable = ({
{formatNumber(toBigNum(node.pendingStake, decimals))} {formatNumber(toBigNum(node.pendingStake, decimals))}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span> <span>{t('STAKE SHARE')}</span>
<span data-testid="stake-percentage">{stakePercentage}</span> <span data-testid="stake-percentage">{stakePercentage}</span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
<KeyValueTable <SubHeading title={t('PENALTIES')} />
data-testid="validator-table-penalties" <RoundedWrapper>
title={t('PENALTIES')} <KeyValueTable data-testid="validator-table-penalties">
>
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('OVERSTAKED AMOUNT')}</span> <span>{t('OVERSTAKED AMOUNT')}</span>
<span>{overstakedAmount.toString()}</span> <span>{overstakedAmount.toString()}</span>
@ -215,7 +230,7 @@ export const ValidatorTable = ({
{getPerformancePenalty(node.rankingScore.performanceScore)} {getPerformancePenalty(node.rankingScore.performanceScore)}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
<span> <span>
<strong>{t('TOTAL PENALTIES')}</strong> <strong>{t('TOTAL PENALTIES')}</strong>
</span> </span>
@ -224,16 +239,16 @@ export const ValidatorTable = ({
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
<KeyValueTable <SubHeading title={t('VOTING POWER')} />
data-testid="validator-table-voting-power" <RoundedWrapper>
title={t('VOTING POWER')} <KeyValueTable data-testid="validator-table-voting-power">
>
<KeyValueTableRow> <KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span> <span>{t('UNNORMALISED VOTING POWER')}</span>
<span>{getUnnormalisedVotingPower(validatorScore)}</span> <span>{getUnnormalisedVotingPower(validatorScore)}</span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
<span> <span>
<strong>{t('NORMALISED VOTING POWER')}</strong> <strong>{t('NORMALISED VOTING POWER')}</strong>
</span> </span>
@ -244,6 +259,7 @@ export const ValidatorTable = ({
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
</div> </div>
); );
}; };

View File

@ -1,7 +1,12 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit'; import {
KeyValueTable,
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../lib/format-number'; import { formatNumber } from '../../../lib/format-number';
import { SubHeading } from '../../../components/heading';
import type { BigNumber } from '../../../lib/bignumber'; import type { BigNumber } from '../../../lib/bignumber';
export interface YourStakeProps { export interface YourStakeProps {
@ -17,7 +22,8 @@ export const YourStake = ({
return ( return (
<div data-testid="your-stake"> <div data-testid="your-stake">
<h2>{t('Your stake')}</h2> <SubHeading title={t('Your stake')} />
<RoundedWrapper>
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow> <KeyValueTableRow>
{t('Your Stake On Node (This Epoch)')} {t('Your Stake On Node (This Epoch)')}
@ -25,13 +31,14 @@ export const YourStake = ({
{formatNumber(stakeThisEpoch)} {formatNumber(stakeThisEpoch)}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow noBorder={true}>
{t('Your Stake On Node (Next Epoch)')} {t('Your Stake On Node (Next Epoch)')}
<span data-testid="stake-next-epoch"> <span data-testid="stake-next-epoch">
{formatNumber(stakeNextEpoch)} {formatNumber(stakeNextEpoch)}
</span> </span>
</KeyValueTableRow> </KeyValueTableRow>
</KeyValueTable> </KeyValueTable>
</RoundedWrapper>
</div> </div>
); );
}; };

View File

@ -4,6 +4,7 @@ import { useWeb3React } from '@web3-react/core';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../components/heading';
import { TrancheItem } from '../redemption/tranche-item'; import { TrancheItem } from '../redemption/tranche-item';
import { TrancheLabel } from './tranche-label'; import { TrancheLabel } from './tranche-label';
import { VestingChart } from './vesting-chart'; import { VestingChart } from './vesting-chart';
@ -29,7 +30,7 @@ export const Tranches = () => {
return ( return (
<section> <section>
<h2>{t('chartTitle')}</h2> <SubHeading title={t('chartTitle')} />
<p>{t('chartAbove')}</p> <p>{t('chartAbove')}</p>
<VestingChart /> <VestingChart />
<p>{t('chartBelow')}</p> <p>{t('chartBelow')}</p>

View File

@ -40,3 +40,4 @@ export * from './vega-logo';
export * from './traffic-light'; export * from './traffic-light';
export * from './toast'; export * from './toast';
export * from './notification'; export * from './notification';
export * from './rounded-wrapper';

View File

@ -44,9 +44,9 @@ Link.displayName = 'Link';
export const ExternalLink = ({ children, className, ...props }: LinkProps) => ( export const ExternalLink = ({ children, className, ...props }: LinkProps) => (
<Link <Link
className={classNames(className, 'inline-flex items-baseline')} className={classNames(className, 'inline-flex items-baseline')}
{...props}
target="_blank" target="_blank"
data-testid="external-link" data-testid="external-link"
{...props}
> >
{typeof children === 'string' ? ( {typeof children === 'string' ? (
<> <>

View File

@ -0,0 +1 @@
export * from './rounded-wrapper';

View File

@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import { RoundedWrapper } from './rounded-wrapper';
describe('Lozenge', () => {
it('should render successfully', () => {
const { baseElement } = render(
<RoundedWrapper>Rounded wrapper</RoundedWrapper>
);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
import { RoundedWrapper } from './rounded-wrapper';
import type { Story, Meta } from '@storybook/react';
import { KeyValueTable, KeyValueTableRow } from '../key-value-table';
export default {
component: RoundedWrapper,
title: 'RoundedWrapper',
} as Meta;
const Template: Story = (args) => (
<div className="mb-8">
<RoundedWrapper {...args} />
</div>
);
export const Default = Template.bind({});
Default.args = {
children: 'Rounded wrapper for presentation purposes',
paddingBottom: true,
};
export const SurroundingKeyValueTable = Template.bind({});
SurroundingKeyValueTable.args = {
children: (
<KeyValueTable>
<KeyValueTableRow>
Item 1<span>123.45</span>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
Item 2<span>543.21</span>
</KeyValueTableRow>
</KeyValueTable>
),
};

View File

@ -0,0 +1,23 @@
import classnames from 'classnames';
import type { ReactNode } from 'react';
export interface RoundedWrapperProps {
children?: ReactNode;
paddingBottom?: boolean;
}
export const RoundedWrapper = ({
children,
paddingBottom = false,
}: RoundedWrapperProps) => (
<div
className={classnames(
'rounded-xl mb-10 pt-4 px-4 border border-neutral-700',
{
'pb-4': paddingBottom,
}
)}
>
{children}
</div>
);