feat(2034): governance home (#2299)
* feat(2034): quickly-assembled governance home * feat(2034): tweaks * feat(2034): tidied up new homepage * feat(2034): e2e test tweak * feat(2034): tweaks from PR comments and adjusted epoch progress bg-colour * feat(2034): fix linting
This commit is contained in:
parent
070f2a2045
commit
0a3509faad
@ -18,7 +18,7 @@ context(
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have GOVERNANCE header visible', function () {
|
it('should have GOVERNANCE header visible', function () {
|
||||||
cy.verify_page_header('Governance');
|
cy.verify_page_header('Proposals');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to see a working link for - find out more about Vega governance', function () {
|
it('should be able to see a working link for - find out more about Vega governance', function () {
|
||||||
|
@ -3,24 +3,28 @@ import classNames from 'classnames';
|
|||||||
interface HeadingProps {
|
interface HeadingProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
centerContent?: boolean;
|
centerContent?: boolean;
|
||||||
|
marginTop?: boolean;
|
||||||
marginBottom?: boolean;
|
marginBottom?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Heading = ({
|
export const Heading = ({
|
||||||
title,
|
title,
|
||||||
centerContent = true,
|
centerContent = true,
|
||||||
|
marginTop = true,
|
||||||
marginBottom = true,
|
marginBottom = true,
|
||||||
}: HeadingProps) => {
|
}: HeadingProps) => {
|
||||||
if (!title) return null;
|
if (!title) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
className={classNames('mt-10 mb-6', {
|
className={classNames('mb-6', {
|
||||||
'mx-auto': centerContent,
|
'mx-auto': centerContent,
|
||||||
|
'mt-10': marginTop,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<h1
|
<h1
|
||||||
className={classNames('font-alpha calt text-5xl', {
|
className={classNames('font-alpha calt text-5xl', {
|
||||||
|
'mt-0': !marginTop,
|
||||||
'mb-0': !marginBottom,
|
'mb-0': !marginBottom,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
@ -16,7 +16,7 @@ export const ProgressBar = ({ value }: ProgressBarProps) => {
|
|||||||
className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
|
className="relative h-2 bg-neutral-600 rounded-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="rounded-full bg-clouds bg-vega-yellow h-full absolute transition-[width] ease-in-out"
|
className="rounded-full bg-gradient-to-r from-[#4e05cf] to-vega-pink h-full absolute transition-[width] ease-in-out"
|
||||||
style={{ width }}
|
style={{ width }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
24
apps/token/src/hooks/use-refresh-validators.ts
Normal file
24
apps/token/src/hooks/use-refresh-validators.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { ObservableQuery } from '@apollo/client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useRefreshValidators = (
|
||||||
|
epochExpiry: string | undefined,
|
||||||
|
refetch: ObservableQuery['refetch']
|
||||||
|
) => {
|
||||||
|
return useEffect(() => {
|
||||||
|
const epochInterval = setInterval(() => {
|
||||||
|
if (!epochExpiry) return;
|
||||||
|
const now = Date.now();
|
||||||
|
const expiry = new Date(epochExpiry).getTime();
|
||||||
|
|
||||||
|
if (now > expiry) {
|
||||||
|
refetch();
|
||||||
|
clearInterval(epochInterval);
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(epochInterval);
|
||||||
|
};
|
||||||
|
}, [refetch, epochExpiry]);
|
||||||
|
};
|
@ -11,7 +11,7 @@
|
|||||||
"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",
|
||||||
"pageTitleGovernance": "Governance",
|
"pageTitleProposals": "Proposals",
|
||||||
"pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards",
|
"pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards",
|
||||||
"pageTitleWithdrawLp": "Withdraw SLP and Rewards",
|
"pageTitleWithdrawLp": "Withdraw SLP and Rewards",
|
||||||
"pageTitleRewards": "Rewards",
|
"pageTitleRewards": "Rewards",
|
||||||
@ -530,6 +530,8 @@
|
|||||||
"validatorTableIntro": "View the validator profile pitches and discussion",
|
"validatorTableIntro": "View the validator profile pitches and discussion",
|
||||||
"onTheForum": "on the forum",
|
"onTheForum": "on the forum",
|
||||||
"readMoreStaking": "Read more about staking on Vega",
|
"readMoreStaking": "Read more about staking on Vega",
|
||||||
|
"readMoreGovernance": "Read more about Vega governance",
|
||||||
|
"readMoreValidators": "Read more about validators",
|
||||||
"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.",
|
||||||
"ethTransactionModalTitle": "Ethereum Transactions",
|
"ethTransactionModalTitle": "Ethereum Transactions",
|
||||||
"confirmed": "Confirmed",
|
"confirmed": "Confirmed",
|
||||||
@ -719,5 +721,13 @@
|
|||||||
"Score": "Score",
|
"Score": "Score",
|
||||||
"performancePenalty": "Performance penalty",
|
"performancePenalty": "Performance penalty",
|
||||||
"overstaked": "Overstaked",
|
"overstaked": "Overstaked",
|
||||||
"overstakedPenalty": "Overstaked penalty"
|
"overstakedPenalty": "Overstaked penalty",
|
||||||
|
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject.",
|
||||||
|
"homeProposalsButtonText": "Browse, vote, and propose",
|
||||||
|
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
|
||||||
|
"homeValidatorsButtonText": "Browse, and stake",
|
||||||
|
"homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.",
|
||||||
|
"homeRewardsButtonText": "See rewards",
|
||||||
|
"homeVegaTokenIntro": "VEGA Token is a governance asset used to make and vote on proposals, and nominate validators.",
|
||||||
|
"homeVegaTokenButtonText": "Manage tokens"
|
||||||
}
|
}
|
||||||
|
@ -1,147 +1,219 @@
|
|||||||
import React from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import {
|
||||||
import { Heading, SubHeading } from '../../components/heading';
|
AsyncRenderer,
|
||||||
import { ExternalLinks } from '@vegaprotocol/react-helpers';
|
Button,
|
||||||
import { useAppState } from '../../contexts/app-state/app-state-context';
|
ExternalLink,
|
||||||
|
RoundedWrapper,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||||
import type { RouteChildProps } from '..';
|
import { useRefreshValidators } from '../../hooks/use-refresh-validators';
|
||||||
|
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||||
import Routes from '../routes';
|
import Routes from '../routes';
|
||||||
import { TokenDetails } from './token-details';
|
import {
|
||||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
ExternalLinks,
|
||||||
import { toBigNum } from '@vegaprotocol/react-helpers';
|
removePaginationWrapper,
|
||||||
import { useNodeDataQuery } from './__generated___/NodeData';
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { useNodesQuery } from '../staking/home/__generated___/Nodes';
|
||||||
|
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||||
|
import { getNotRejectedProposals } from '../proposals/proposals/proposals-container';
|
||||||
|
import { Heading } from '../../components/heading';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import type { RouteChildProps } from '..';
|
||||||
|
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
|
||||||
|
import type { NodesFragmentFragment } from '../staking/home/__generated___/Nodes';
|
||||||
|
|
||||||
const Home = ({ name }: RouteChildProps) => {
|
const nodesToShow = 6;
|
||||||
useDocumentTitle(name);
|
|
||||||
|
const HomeProposals = ({
|
||||||
|
proposals,
|
||||||
|
}: {
|
||||||
|
proposals: ProposalFieldsFragment[];
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { appState } = useAppState();
|
|
||||||
const { data } = useNodeDataQuery();
|
|
||||||
const totalAssociated = React.useMemo(() => {
|
|
||||||
return toBigNum(data?.nodeData?.stakedTotal || '0', appState.decimals);
|
|
||||||
}, [appState.decimals, data?.nodeData?.stakedTotal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className="mb-16" data-testid="home-proposals">
|
||||||
<Heading title={t('pageTitleHome')} />
|
<Heading title={t('Proposals')} />
|
||||||
<HomeSection>
|
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||||
<TokenDetails
|
<div className="flex items-center mb-8 gap-8">
|
||||||
totalSupply={appState.totalSupply}
|
<Link to={`${Routes.PROPOSALS}`}>
|
||||||
totalAssociated={totalAssociated}
|
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||||
/>
|
|
||||||
</HomeSection>
|
|
||||||
<HomeSection>
|
|
||||||
<SubHeading title={t('Token Vesting')} />
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'The vesting contract holds VEGA tokens until they have become unlocked.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Trans
|
|
||||||
i18nKey="Tokens are held in different <trancheLink>Tranches</trancheLink>. Each tranche has its own schedule for how the tokens are unlocked."
|
|
||||||
components={{
|
|
||||||
trancheLink: (
|
|
||||||
<Link
|
|
||||||
data-testid="tranches-link"
|
|
||||||
to={Routes.TRANCHES}
|
|
||||||
className="underline text-white"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'Once unlocked they can be redeemed from the contract so that you can transfer them between wallets.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<Link to={Routes.REDEEM}>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
data-testid="check-vesting-page-btn"
|
|
||||||
>
|
|
||||||
{t('Check to see if you can redeem unlocked VEGA tokens')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
</Link>
|
||||||
</HomeSection>
|
|
||||||
<HomeSection>
|
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
|
||||||
<SubHeading title={t('Use your Vega tokens')} />
|
{t(`readMoreGovernance`)}
|
||||||
<p>
|
</ExternalLink>
|
||||||
{t(
|
|
||||||
'To use your tokens on the Vega network they need to be associated with a Vega wallet/key.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'This can happen both while held in the vesting contract as well as when redeemed.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a
|
|
||||||
data-testid="get-vega-wallet-link"
|
|
||||||
href={ExternalLinks.VEGA_WALLET_URL}
|
|
||||||
className="underline text-white"
|
|
||||||
target="_blank"
|
|
||||||
rel="nofollow noreferrer"
|
|
||||||
>
|
|
||||||
{t('Get a Vega wallet')}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link
|
|
||||||
data-testid="associate-vega-tokens-link-on-homepage"
|
|
||||||
to={`${Routes.VALIDATORS}/associate`}
|
|
||||||
className="underline text-white"
|
|
||||||
>
|
|
||||||
{t('Associate VEGA tokens')}
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</HomeSection>
|
|
||||||
<div className="flex gap-12">
|
|
||||||
<div className="flex-1">
|
|
||||||
<HomeSection>
|
|
||||||
<SubHeading title={t('Staking')} />
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'VEGA token holders can nominate a validator node and receive staking rewards.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link to={Routes.VALIDATORS}>
|
|
||||||
<Button size="md" data-testid="staking-button-on-homepage">
|
|
||||||
{t('Nominate a validator')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</HomeSection>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<HomeSection>
|
|
||||||
<SubHeading title={t('Governance')} />
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'VEGA token holders can vote on proposed changes to the network and create proposals.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Link to={Routes.PROPOSALS}>
|
|
||||||
<Button size="md" data-testid="governance-button-on-homepage">
|
|
||||||
{t('View Governance proposals')}
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</HomeSection>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
<ul data-testid="home-proposal-list">
|
||||||
|
{proposals.map((proposal) => (
|
||||||
|
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
interface HomeNodesProps {
|
||||||
|
activeNodes: NodesFragmentFragment[];
|
||||||
|
consensusNodes: NodesFragmentFragment[];
|
||||||
|
trimmedActiveNodes: NodesFragmentFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
export const HomeSection = ({ children }: { children: React.ReactNode }) => {
|
const HomeNodes = ({
|
||||||
return <section className="mb-12">{children}</section>;
|
activeNodes,
|
||||||
|
consensusNodes,
|
||||||
|
trimmedActiveNodes,
|
||||||
|
}: HomeNodesProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const highlightedNodeData = [
|
||||||
|
{ title: t('active nodes'), length: activeNodes.length },
|
||||||
|
{ title: t('consensus nodes'), length: consensusNodes.length },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mb-12" data-testid="home-validators">
|
||||||
|
<Heading title={t('Validators')} />
|
||||||
|
<h3 className="mb-6">{t('homeValidatorsIntro')}</h3>
|
||||||
|
<div className="flex items-center mb-8 gap-8">
|
||||||
|
<Link to={Routes.STAKING}>
|
||||||
|
<Button size="md">{t('homeValidatorsButtonText')}</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<ExternalLink href={ExternalLinks.VALIDATOR_FORUM}>
|
||||||
|
{t(`readMoreValidators`)}
|
||||||
|
</ExternalLink>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-[repeat(6,_1fr)] items-center gap-x-6 gap-y-2">
|
||||||
|
{highlightedNodeData.map(({ title, length }, index) => (
|
||||||
|
<div key={index} className="col-span-3">
|
||||||
|
<Link to={Routes.VALIDATORS}>
|
||||||
|
<RoundedWrapper paddingBottom={true}>
|
||||||
|
<div className="flex flex-col items-center m-[-1rem] px-4 py-6 hover:bg-neutral-800">
|
||||||
|
<span className="text-5xl">{length}</span>
|
||||||
|
<span className="text-sm uppercase text-neutral-400">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
|
||||||
|
<div key={id} className="col-span-2">
|
||||||
|
<Link to={`${Routes.VALIDATORS}/${id}`}>
|
||||||
|
<RoundedWrapper paddingBottom={true} border={false}>
|
||||||
|
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
|
||||||
|
{avatarUrl && (
|
||||||
|
<img
|
||||||
|
className="h-6 w-6 rounded-full mr-2"
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={`Avatar icon for ${name}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="text-sm">{name}</span>
|
||||||
|
</div>
|
||||||
|
</RoundedWrapper>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeNodes.length > nodesToShow && (
|
||||||
|
<Link to={Routes.STAKING}>
|
||||||
|
<span className="underline">
|
||||||
|
And {activeNodes.length - nodesToShow} more...
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||||
|
useDocumentTitle(name);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
data: proposalsData,
|
||||||
|
loading: proposalsLoading,
|
||||||
|
error: proposalsError,
|
||||||
|
} = useProposalsQuery({
|
||||||
|
pollInterval: 5000,
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
errorPolicy: 'ignore',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: validatorsData,
|
||||||
|
error: validatorsError,
|
||||||
|
loading: validatorsLoading,
|
||||||
|
refetch,
|
||||||
|
} = useNodesQuery();
|
||||||
|
|
||||||
|
useRefreshValidators(validatorsData?.epoch.timestamps.expiry, refetch);
|
||||||
|
|
||||||
|
const proposals = useMemo(
|
||||||
|
() =>
|
||||||
|
proposalsData
|
||||||
|
? getNotRejectedProposals<ProposalFieldsFragment>(
|
||||||
|
proposalsData.proposalsConnection
|
||||||
|
).slice(0, 3)
|
||||||
|
: [],
|
||||||
|
[proposalsData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeNodes = removePaginationWrapper(
|
||||||
|
validatorsData?.nodesConnection.edges
|
||||||
|
);
|
||||||
|
|
||||||
|
const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow);
|
||||||
|
|
||||||
|
const consensusNodes = activeNodes.filter(
|
||||||
|
(node) =>
|
||||||
|
node.rankingScore.status ===
|
||||||
|
Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer
|
||||||
|
loading={proposalsLoading || validatorsLoading}
|
||||||
|
error={proposalsError || validatorsError}
|
||||||
|
data={proposalsData && validatorsData}
|
||||||
|
>
|
||||||
|
<HomeProposals proposals={proposals} />
|
||||||
|
|
||||||
|
<HomeNodes
|
||||||
|
activeNodes={activeNodes}
|
||||||
|
consensusNodes={consensusNodes}
|
||||||
|
trimmedActiveNodes={trimmedActiveNodes}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section className="grid grid-cols-2 gap-12 mb-16">
|
||||||
|
<div data-testid="home-rewards">
|
||||||
|
<Heading title={t('Rewards')} marginTop={false} />
|
||||||
|
<h3 className="mb-6">{t('homeRewardsIntro')}</h3>
|
||||||
|
<div className="flex items-center mb-8 gap-4">
|
||||||
|
<Link to={Routes.REWARDS}>
|
||||||
|
<Button size="md">{t('homeRewardsButtonText')}</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-testid="home-vega-token">
|
||||||
|
<Heading title={t('VEGA Token')} marginTop={false} />
|
||||||
|
<h3 className="mb-6">{t('homeVegaTokenIntro')}</h3>
|
||||||
|
<div className="flex items-center mb-8 gap-4">
|
||||||
|
<Link to={Routes.WITHDRAWALS}>
|
||||||
|
<Button size="md">{t('homeVegaTokenButtonText')}</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GovernanceHome;
|
||||||
|
@ -39,6 +39,7 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||||
'New some market'
|
'New some market'
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New market');
|
||||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||||
'A new some market'
|
'A new some market'
|
||||||
);
|
);
|
||||||
@ -66,6 +67,9 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||||
'New market id'
|
'New market id'
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||||
|
'Update market'
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.queryByTestId('proposal-description')
|
screen.queryByTestId('proposal-description')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@ -99,6 +103,7 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||||
'New asset: Fake currency'
|
'New asset: Fake currency'
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'Symbol: FAKE. ERC20 0x0'
|
'Symbol: FAKE. ERC20 0x0'
|
||||||
);
|
);
|
||||||
@ -125,6 +130,7 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||||
'Unknown proposal'
|
'Unknown proposal'
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'Symbol: BIA. Max faucet amount mint: 300'
|
'Symbol: BIA. Max faucet amount mint: 300'
|
||||||
);
|
);
|
||||||
@ -153,6 +159,9 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||||
'Network parameter'
|
'Network parameter'
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||||
|
'Network parameter'
|
||||||
|
);
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'Network key to Network value'
|
'Network key to Network value'
|
||||||
);
|
);
|
||||||
@ -175,6 +184,7 @@ describe('Proposal header', () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||||
expect(
|
expect(
|
||||||
screen.queryByTestId('proposal-description')
|
screen.queryByTestId('proposal-description')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@ -202,6 +212,7 @@ describe('Proposal header', () => {
|
|||||||
// For a rationale over 100 chars, we expect the header to be truncated at
|
// For a rationale over 100 chars, we expect the header to be truncated at
|
||||||
// 100 chars with ellipsis and the details-one element to contain the rest.
|
// 100 chars with ellipsis and the details-one element to contain the rest.
|
||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
||||||
'Class aptent taciti sociosqu ad litora torquent per conubia'
|
'Class aptent taciti sociosqu ad litora torquent per conubia'
|
||||||
);
|
);
|
||||||
@ -226,6 +237,7 @@ describe('Proposal header', () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform');
|
expect(screen.getByTestId('proposal-title')).toHaveTextContent('freeform');
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||||
expect(
|
expect(
|
||||||
screen.queryByTestId('proposal-description')
|
screen.queryByTestId('proposal-description')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
@ -247,6 +259,9 @@ describe('Proposal header', () => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||||
|
'Update asset'
|
||||||
|
);
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'Update asset'
|
'Update asset'
|
||||||
);
|
);
|
@ -1,20 +1,23 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
import { Intent, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
import { shorten } from '@vegaprotocol/react-helpers';
|
import { shorten } from '@vegaprotocol/react-helpers';
|
||||||
import { SubHeading } from '../../../../components/heading';
|
import { Heading, SubHeading } from '../../../../components/heading';
|
||||||
import type { ReactNode } from 'react';
|
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';
|
||||||
|
|
||||||
export const ProposalHeader = ({
|
export const ProposalHeader = ({
|
||||||
proposal,
|
proposal,
|
||||||
|
useSubHeading = true,
|
||||||
}: {
|
}: {
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
|
useSubHeading?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const change = proposal?.terms.change;
|
const change = proposal?.terms.change;
|
||||||
|
|
||||||
let details: ReactNode;
|
let details: ReactNode;
|
||||||
|
let proposalType: ReactNode;
|
||||||
|
|
||||||
let title = proposal?.rationale.title.trim();
|
let title = proposal?.rationale.title.trim();
|
||||||
let description = proposal?.rationale.description.trim();
|
let description = proposal?.rationale.description.trim();
|
||||||
@ -27,6 +30,7 @@ export const ProposalHeader = ({
|
|||||||
|
|
||||||
switch (change?.__typename) {
|
switch (change?.__typename) {
|
||||||
case 'NewMarket': {
|
case 'NewMarket': {
|
||||||
|
proposalType = t('NewMarket');
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
{t('Code')}: {change.instrument.code}.{' '}
|
{t('Code')}: {change.instrument.code}.{' '}
|
||||||
@ -45,10 +49,12 @@ export const ProposalHeader = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'UpdateMarket': {
|
case 'UpdateMarket': {
|
||||||
|
proposalType = t('UpdateMarket');
|
||||||
details = `${t('Market change')}: ${change.marketId}`;
|
details = `${t('Market change')}: ${change.marketId}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'NewAsset': {
|
case 'NewAsset': {
|
||||||
|
proposalType = t('NewAsset');
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
{t('Symbol')}: {change.symbol}.{' '}
|
{t('Symbol')}: {change.symbol}.{' '}
|
||||||
@ -65,6 +71,7 @@ export const ProposalHeader = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'UpdateNetworkParameter': {
|
case 'UpdateNetworkParameter': {
|
||||||
|
proposalType = t('NetworkParameter');
|
||||||
const parametersClasses = 'font-mono leading-none';
|
const parametersClasses = 'font-mono leading-none';
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
@ -80,10 +87,12 @@ export const ProposalHeader = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'NewFreeform': {
|
case 'NewFreeform': {
|
||||||
|
proposalType = t('Freeform');
|
||||||
details = `${t('FreeformProposal')}: ${proposal?.id}`;
|
details = `${t('FreeformProposal')}: ${proposal?.id}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'UpdateAsset': {
|
case 'UpdateAsset': {
|
||||||
|
proposalType = t('UpdateAsset');
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
`${t('Update asset')}`;
|
`${t('Update asset')}`;
|
||||||
@ -96,14 +105,28 @@ export const ProposalHeader = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-sm mb-2">
|
<div className="text-sm mb-2">
|
||||||
<header data-testid="proposal-title">
|
<div data-testid="proposal-title">
|
||||||
<SubHeading title={titleContent || t('Unknown proposal')} />
|
{useSubHeading ? (
|
||||||
</header>
|
<header>
|
||||||
{description && (
|
<SubHeading title={titleContent || t('Unknown proposal')} />
|
||||||
<div className="mb-4" data-testid="proposal-description">
|
</header>
|
||||||
{description}
|
) : (
|
||||||
</div>
|
<Heading title={titleContent || t('Unknown proposal')} />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
{proposalType && (
|
||||||
|
<div data-testid="proposal-type">
|
||||||
|
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<div data-testid="proposal-description">{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{details && <div data-testid="proposal-details">{details}</div>}
|
{details && <div data-testid="proposal-details">{details}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -74,7 +74,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
|||||||
return (
|
return (
|
||||||
<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} useSubHeading={false} />
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<ProposalChangeTable proposal={proposal} />
|
<ProposalChangeTable proposal={proposal} />
|
||||||
</div>
|
</div>
|
@ -192,7 +192,6 @@ describe('Proposals list item details', () => {
|
|||||||
networkParamsQueryMock,
|
networkParamsQueryMock,
|
||||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
|
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
|
||||||
]);
|
]);
|
||||||
screen.debug();
|
|
||||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||||
|
|
||||||
expect(await screen.findByText('You voted')).toBeInTheDocument();
|
expect(await screen.findByText('You voted')).toBeInTheDocument();
|
@ -1,3 +1,4 @@
|
|||||||
|
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
@ -11,13 +12,11 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
|||||||
if (!proposal || !proposal.id) return null;
|
if (!proposal || !proposal.id) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li id={proposal.id} data-testid="proposals-list-item">
|
||||||
className="py-4 border-b border-neutral-400 last:border-0"
|
<RoundedWrapper paddingBottom={true}>
|
||||||
id={proposal.id}
|
<ProposalHeader proposal={proposal} />
|
||||||
data-testid="proposals-list-item"
|
<ProposalsListItemDetails proposal={proposal} />
|
||||||
>
|
</RoundedWrapper>
|
||||||
<ProposalHeader proposal={proposal} />
|
|
||||||
<ProposalsListItemDetails proposal={proposal} />
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -81,7 +81,7 @@ afterAll(() => {
|
|||||||
describe('Proposals list', () => {
|
describe('Proposals list', () => {
|
||||||
it('Render a page title and link to the make proposal form', () => {
|
it('Render a page title and link to the make proposal form', () => {
|
||||||
render(renderComponent([]));
|
render(renderComponent([]));
|
||||||
expect(screen.getByText('Governance')).toBeInTheDocument();
|
expect(screen.getByText('Proposals')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
|
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
@ -51,7 +51,7 @@ export const ProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
<Heading
|
<Heading
|
||||||
centerContent={false}
|
centerContent={false}
|
||||||
marginBottom={false}
|
marginBottom={false}
|
||||||
title={t('pageTitleGovernance')}
|
title={t('pageTitleProposals')}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
className="xs:justify-self-end"
|
className="xs:justify-self-end"
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user