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:
parent
e49ad9da6a
commit
071a9ab34b
@ -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:
|
||||||
|
@ -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';
|
||||||
|
@ -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');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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.",
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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} />
|
||||||
|
@ -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) => (
|
||||||
|
@ -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 />
|
||||||
)}
|
)}
|
||||||
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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)}
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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');
|
||||||
|
@ -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>
|
||||||
|
@ -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,
|
||||||
|
@ -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 && (
|
||||||
<>
|
<>
|
||||||
|
@ -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;
|
||||||
|
@ -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 />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
@ -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' ? (
|
||||||
<>
|
<>
|
||||||
|
1
libs/ui-toolkit/src/components/rounded-wrapper/index.ts
Normal file
1
libs/ui-toolkit/src/components/rounded-wrapper/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './rounded-wrapper';
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
),
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
Loading…
Reference in New Issue
Block a user