Compare commits

..
72 changed files with 1952 additions and 1866 deletions
@@ -1,2 +1 @@
export * from './network-parameters';
export * from './structure-network-params';
@@ -1,105 +1,82 @@
import { render, screen } from '@testing-library/react';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParametersTable } from './network-parameters';
import { MemoryRouter } from 'react-router-dom';
const renderComponent = (data: NetworkParamsQuery | undefined) => {
return render(
<MemoryRouter>
<NetworkParametersTable data={data} loading={false} />
</MemoryRouter>
);
};
const mockData = {
networkParametersConnection: {
edges: [
{
node: {
key: 'spam.protection.delegation.min.tokens',
value: '3',
},
},
{
node: {
key: 'spam.protection.voting.min.tokens',
value: '1',
},
},
{
node: {
key: 'reward.staking.delegation.minimumValidatorStake',
value: '2',
},
},
{
node: {
key: 'reward.asset',
value:
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
},
},
],
},
};
describe('NetworkParametersTable', () => {
it('renders headers correctly', () => {
renderComponent(mockData);
const allHeadings = screen.getAllByRole('heading');
expect(
allHeadings.map((h) => {
return {
text: h.textContent,
level: h.tagName,
testId: h.getAttribute('data-testid'),
};
})
).toEqual([
{
text: 'Network Parameters',
level: 'H1',
testId: 'network-param-header',
it('renders correctly when it has network params', () => {
const data: NetworkParamsQuery = {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
value: '1',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'market.fee.factors.infrastructureFee',
value: '0.0005',
},
},
],
},
{ text: 'Spam', level: 'H1', testId: 'spam' },
{ text: 'Protection', level: 'H2', testId: 'spam-protection' },
{ text: 'Delegation', level: 'H3', testId: 'spam-protection-delegation' },
{ text: 'Min', level: 'H4', testId: 'spam-protection-delegation-min' },
{ text: 'Voting', level: 'H3', testId: 'spam-protection-voting' },
{ text: 'Min', level: 'H4', testId: 'spam-protection-voting-min' },
{ text: 'Reward', level: 'H1', testId: 'reward' },
{ text: 'Staking', level: 'H2', testId: 'reward-staking' },
{ text: 'Delegation', level: 'H3', testId: 'reward-staking-delegation' },
]);
};
render(<NetworkParametersTable data={data} loading={false} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'market.fee.factors.infrastructureFee'
);
expect(rows[1].children[0]).toHaveTextContent(
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
});
it('renders network params correctly', () => {
renderComponent(mockData);
const delegationMinTokensRow = screen.getByTestId(
'spam-protection-delegation-min-tokens'
it('renders the rows in ascending order', () => {
const data: NetworkParamsQuery = {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'market.fee.factors.infrastructureFee',
value: '0.0005',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
value: '1',
},
},
],
},
};
render(<NetworkParametersTable data={data} loading={false} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
expect(delegationMinTokensRow).toHaveTextContent('0.000000000000000003');
const votingMinTokensRow = screen.getByTestId(
'spam-protection-voting-min-tokens'
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'market.fee.factors.infrastructureFee'
);
expect(votingMinTokensRow).toHaveTextContent('0.000000000000000001');
const minimumValidatorStakeRow = screen.getByTestId(
'reward-staking-delegation-minimumValidatorStake'
);
expect(minimumValidatorStakeRow).toHaveTextContent('2');
const assetRow = screen.getByTestId('reward-asset');
expect(assetRow).toHaveTextContent(
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55'
expect(rows[1].children[0]).toHaveTextContent(
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
});
it('does not render rows when is loading', () => {
renderComponent(undefined);
render(<NetworkParametersTable data={undefined} loading={true} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
@@ -1,8 +1,6 @@
import startCase from 'lodash/startCase';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import {
AsyncRenderer,
KeyValueTable,
KeyValueTableRow,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
@@ -14,12 +12,11 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
import orderBy from 'lodash/orderBy';
import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { structureNetworkParams } from './structure-network-params';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { GroupedParams } from './structure-network-params';
const PERCENTAGE_PARAMS = [
'governance.proposal.asset.requiredMajority',
@@ -61,54 +58,6 @@ const BIG_NUMBER_PARAMS = [
'governance.proposal.updateAsset.minVoterBalance',
];
export const renderGroupedParams = (
group: GroupedParams,
level: number,
parentKeys: string[] = []
) => {
const Header = `h${level}` as keyof JSX.IntrinsicElements;
const headerStyles = classNames('uppercase font-semibold', {
'pt-6 text-3xl underline': level === 1,
'pt-3 text-2xl': level === 2,
'pt-2 text-lg': level === 3,
'pt-2 text-default': level === 4,
});
return Object.entries(group).map(([key, value]) => {
const fullPath = [...parentKeys, key].join('.');
const isLeafNode = typeof value !== 'object';
const id = parentKeys.concat([key]).join('-');
return (
<div key={key}>
{!isLeafNode && (
<div id={id}>
<Link to={`#${id}`}>
<Header className={headerStyles} data-testid={id}>
{startCase(key)}
</Header>
</Link>
</div>
)}
{isLeafNode ? (
typeof value === 'string' ? (
<div data-testid={id}>
<NetworkParameterRow
key={fullPath}
row={{ key: fullPath, value: value }}
/>
</div>
) : null
) : (
<div className="pb-1">
{renderGroupedParams(value, level + 1, [...parentKeys, key])}
</div>
)}
</div>
);
});
};
export const NetworkParameterRow = ({
row: { key, value },
}: {
@@ -128,9 +77,7 @@ export const NetworkParameterRow = ({
>
{key}
{isSyntaxRow ? (
<div className="pb-2">
<SyntaxHighlighter data={JSON.parse(value)} />
</div>
<SyntaxHighlighter data={JSON.parse(value)} />
) : isNaN(Number(value)) ? (
value
) : BIG_NUMBER_PARAMS.includes(key) ? (
@@ -166,12 +113,17 @@ export const NetworkParametersTable = ({
loading={loading}
error={error}
render={(data) => {
const flatParams = removePaginationWrapper(
data.networkParametersConnection.edges
const ascParams = orderBy(
removePaginationWrapper(data.networkParametersConnection.edges),
(param) => param.key,
'asc'
);
const groupedParams = structureNetworkParams(flatParams);
return (
<div className="-mt-6">{renderGroupedParams(groupedParams, 1)}</div>
<KeyValueTable data-testid="parameters">
{(ascParams || []).map((row) => (
<NetworkParameterRow key={row.key} row={row} />
))}
</KeyValueTable>
);
}}
/>
@@ -1,109 +0,0 @@
import type { GroupedParams } from './structure-network-params';
import {
structureParams,
sortGroupedParams,
structureNetworkParams,
} from './structure-network-params';
describe('structureParams', () => {
it('should correctly structure params', () => {
const input = [
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
{ key: 'spam.protection.voting.min.tokens', value: '5' },
];
const output: GroupedParams = {
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
voting: {
min: {
tokens: '5',
},
},
},
},
};
expect(structureParams(input)).toEqual(output);
});
it('should handle top-level keys correctly', () => {
const input = [{ key: 'levelOne', value: '10' }];
const output = {
levelOne: '10',
};
expect(structureParams(input)).toEqual(output);
});
});
describe('sortGroupedParams', () => {
it('should correctly sort grouped params', () => {
const input: GroupedParams = {
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
reward: '50',
};
const output: GroupedParams = {
reward: '50',
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
};
expect(sortGroupedParams(input)).toEqual(output);
});
it('should handle already sorted keys', () => {
const input = {
a: '10',
b: {
c: '5',
d: '6',
},
};
expect(sortGroupedParams(input)).toEqual(input);
});
});
describe('structureNetworkParams', () => {
it('should structure and sort network params correctly', () => {
const input = [
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
{ key: 'reward.asset', value: '50' },
];
const output: GroupedParams = {
reward: {
asset: '50',
},
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
};
expect(structureNetworkParams(input)).toEqual(output);
});
it('should return an empty object if no params are provided', () => {
expect(structureNetworkParams([])).toEqual({});
});
});
@@ -1,107 +0,0 @@
/**
* Categorizes and sorts an array of key-value pairs of network params into a nested object structure.
*
* The function takes an array of network params where keys are dot-delimited
* strings representing nested categories (e.g., 'spam.protection.delegation.min.tokens').
*
* Why this is necessary:
* A flat key-value structure wouldn't provide the hierarchical information needed.
* Organizing network parameters like this allows the rendering of nested headers
* and their corresponding network params.
*
* It also ensures that items with the minimum amount of nesting are ordered first. This
* allows us to render these items first, before more deeply nested items are rendered with
* subheaders. This creates a more intuitive UI.
*
* For example, given the input:
* [
* { key: 'spam.protection.delegation.min.tokens', value: '10' },
* { key: 'spam.protection.voting.min.tokens', value: '5' },
* { key: 'reward.staking.delegation.minimumValidatorStake', value: '2' }
* { key: 'reward.asset', value: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55' }
* ]
*
* The output will be:
* {
* spam: {
* protection: {
* delegation: {
* min: {
* tokens: '10'
* }
* },
* voting: {
* min: {
* tokens: '5'
* }
* }
* }
* },
* reward: {
* asset: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
* staking: {
* delegation: {
* minimumValidatorStake: '2'
* }
* }
* }
* }
*
* @param {Array} params - An array of key-value pairs to categorize and sort.
* @returns {GroupedParams} - A nested object that groups the key-value pairs.
*/
export type GroupedParams = {
[key: string]: string | GroupedParams;
};
export const structureParams = (
params: { key: string; value: string }[]
): GroupedParams => {
const grouped: GroupedParams = {};
params.forEach(({ key, value }) => {
const parts = key.split('.');
let node: GroupedParams = grouped;
parts.forEach((part, i) => {
if (typeof node[part] === 'undefined') {
node[part] = i === parts.length - 1 ? value : {};
}
if (typeof node[part] === 'object') {
node = node[part] as GroupedParams;
}
});
});
return grouped;
};
export const sortGroupedParams = (
groupedParams: GroupedParams
): GroupedParams => {
const sorted: GroupedParams = {};
// Sort top-level keys first
Object.entries(groupedParams).forEach(([key, value]) => {
if (typeof value === 'string') {
sorted[key] = value;
}
});
Object.entries(groupedParams).forEach(([key, value]) => {
if (typeof value === 'object') {
sorted[key] = sortGroupedParams(value);
}
});
return sorted;
};
export const structureNetworkParams = (
params: { key: string; value: string }[]
) => {
const grouped = structureParams(params);
return sortGroupedParams(grouped);
};
+2 -14
View File
@@ -201,8 +201,6 @@
"STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote",
"UpdateNetworkParameter": "Network parameter",
"NewFreeform": "Freeform",
"setToPass": "Set to pass",
"setToFail": "Set to fail",
"tokenVotes": "Token votes",
"liquidityVotes": "Liquidity votes",
"castYourVote": "Cast your vote",
@@ -211,23 +209,13 @@
"against": "Against",
"majorityRequired": "Majority Required",
"participation": "Participation",
"majorityThreshold": "majority threshold",
"participationThreshold": "participation threshold",
"met": "met",
"notMet": "not met",
"met": "Met",
"notMet": "Not Met",
"governanceRequired": "Required",
"daysLeft": "{{daysLeft}} left to vote.",
"toVote": "to vote",
"voteFor": "Vote for",
"voteAgainst": "Vote against",
"tokenVote": "Token vote",
"tokenVotesFor": "Token votes for",
"tokenVotesAgainst": "Token votes against",
"totalTokensVoted": "Total tokens voted",
"liquidityProviderVote": "Liquidity provider vote",
"liquidityProviderVotesFor": "LP votes for",
"liquidityProviderVotesAgainst": "LP votes against",
"totalLiquidityProviderTokensVoted": "Total LP tokens voted",
"votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.",
"noGovernanceTokens": "You need some VEGA tokens to participate in governance",
"youVoted": "You voted",
+50 -23
View File
@@ -31,6 +31,10 @@ import {
orderByDate,
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../lib/bignumber';
const nodesToShow = 6;
@@ -43,34 +47,57 @@ const HomeProposals = ({
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
return (
<section className="mb-16" data-testid="home-proposals">
<Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<section className="mb-16" data-testid="home-proposals">
<Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<SubHeading title={t('latestProposals')} />
<ul data-testid="home-proposal-list" className="grid gap-6">
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
<SubHeading title={t('latestProposals')} />
<ul data-testid="home-proposal-list" className="grid gap-6">
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
</ul>
{proposals.map((proposal) => (
<ProposalsListItem
key={proposal.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
</AsyncRenderer>
);
};
@@ -16,6 +16,7 @@ import { ProposalHeader } from './proposal-header';
import {
lastWeek,
nextWeek,
mockNetworkParams,
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
@@ -47,6 +48,7 @@ const renderComponent = (
<ProposalHeader
proposal={proposal}
isListItem={isListItem}
networkParams={mockNetworkParams}
voteState={voteState}
/>
</VegaWalletContext.Provider>
@@ -8,19 +8,22 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
import { ProposalVotingStatus } from '../proposal-voting-status';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { FLAGS } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import type { VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown';
export const ProposalHeader = ({
proposal,
networkParams,
isListItem = true,
voteState,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@@ -143,7 +146,7 @@ export const ProposalHeader = ({
className="flex items-center gap-2"
data-testid={`user-voted-${voteState.toLowerCase()}`}
>
<div data-testid="you-voted-icon">
<div className="text-vega-green" data-testid="you-voted-icon">
<VegaIcon name={VegaIconNames.VOTE} size={24} />
</div>
<div>
@@ -182,7 +185,7 @@ export const ProposalHeader = ({
</div>
)}
<VoteBreakdown proposal={proposal} />
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
</>
);
};
@@ -0,0 +1 @@
export { ProposalVotesTable } from './proposal-votes-table';
@@ -0,0 +1,119 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalVotesTable } from './proposal-votes-table';
import { ProposalType } from '../proposal/proposal';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
const defaultProposal = generateProposal();
const defaultProposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(10),
no: generateNoVotes(0),
},
});
const updateMarketProposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
const renderComponent = (
proposal = defaultProposal,
proposalType = defaultProposalType
) =>
render(
<MockedProvider>
<AppStateProvider>
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</AppStateProvider>
</MockedProvider>
);
describe('Proposal Votes Table', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should show vote breakdown fields, excluding custom update market fields', () => {
renderComponent();
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Expected to pass')).toBeInTheDocument();
expect(screen.getByText('Token majority met')).toBeInTheDocument();
expect(screen.getByText('Token participation met')).toBeInTheDocument();
expect(screen.getByText('Tokens for proposal')).toBeInTheDocument();
expect(screen.getByText('Total Supply')).toBeInTheDocument();
expect(screen.getByText('Tokens against proposal')).toBeInTheDocument();
expect(screen.getByText('Participation required')).toBeInTheDocument();
expect(screen.getByText('Majority Required')).toBeInTheDocument();
expect(screen.getByText('Number of voting parties')).toBeInTheDocument();
expect(screen.getByText('Total tokens voted')).toBeInTheDocument();
expect(
screen.getByText('Total tokens voted percentage')
).toBeInTheDocument();
expect(screen.getByText('Number of votes for')).toBeInTheDocument();
expect(screen.getByText('Number of votes against')).toBeInTheDocument();
expect(screen.getByText('Yes percentage')).toBeInTheDocument();
expect(screen.getByText('No percentage')).toBeInTheDocument();
expect(screen.queryByText('Liquidity majority met')).toBeNull();
expect(screen.queryByText('Liquidity participation met')).toBeNull();
expect(screen.queryByText('Liquidity shares for proposal')).toBeNull();
});
it('displays different breakdown fields for update market proposal', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Liquidity majority met')).toBeInTheDocument();
expect(screen.getByText('Liquidity participation met')).toBeInTheDocument();
expect(
screen.getByText('Liquidity shares for proposal')
).toBeInTheDocument();
expect(screen.queryByText('Number of voting parties')).toBeNull();
expect(screen.queryByText('Total tokens voted')).toBeNull();
expect(screen.queryByText('Total tokens voted percentage')).toBeNull();
expect(screen.queryByText('Number of votes for')).toBeNull();
expect(screen.queryByText('Number of votes against')).toBeNull();
expect(screen.queryByText('Yes percentage')).toBeNull();
expect(screen.queryByText('No percentage')).toBeNull();
});
it('displays if an update market proposal will pass by token vote', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
});
it('displays if an update market proposal will pass by LP vote', () => {
renderComponent(
generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0, 1, '10'),
},
no: {
...generateNoVotes(0, 1, '0'),
},
},
}),
updateMarketProposalType
);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
});
});
@@ -0,0 +1,177 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
KeyValueTable,
KeyValueTableRow,
Thumbs,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils';
import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalVotesTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposalType: ProposalType | null;
}
export const ProposalVotesTable = ({
proposal,
proposalType,
}: ProposalVotesTableProps) => {
const { t } = useTranslation();
const {
appState: { totalSupply },
} = useAppState();
const [showDetails, setShowDetails] = useState(false);
const {
willPassByTokenVote,
willPassByLPVote,
totalTokensPercentage,
participationMet,
participationLPMet,
totalTokensVoted,
noPercentage,
yesPercentage,
noTokens,
yesTokens,
yesEquityLikeShareWeight,
yesVotes,
noVotes,
totalVotes,
requiredMajorityPercentage,
requiredParticipation,
majorityMet,
majorityLPMet,
} = useVoteInformation({ proposal });
const isUpdateMarket = proposalType === ProposalType.PROPOSAL_UPDATE_MARKET;
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLiquidityVote');
return (
<>
<CollapsibleToggle
toggleState={showDetails}
setToggleState={setShowDetails}
dataTestId="vote-breakdown-toggle"
>
<SubHeading title={t('voteBreakdown')} />
</CollapsibleToggle>
{showDetails && (
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable
data-testid="proposal-votes-table"
numerical={true}
headingLevel={4}
>
<KeyValueTableRow>
{t('expectedToPass')}
{isUpdateMarket ? (
updateMarketWillPass ? (
<Thumbs up={true} text={updateMarketVotePassMethod} />
) : (
<Thumbs up={false} />
)
) : willPassByTokenVote ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityMet')}
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('majorityLPMet')}
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('participationMet')}
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('participationLPMet')}
{participationLPMet ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('tokenForProposal')}
{formatNumber(yesTokens, 2)}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('tokenLPForProposal')}
{formatNumber(yesEquityLikeShareWeight, 2)}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('totalSupply')}
{formatNumber(totalSupply, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('tokensAgainstProposal')}
{formatNumber(noTokens, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('participationRequired')}
{formatNumberPercentage(requiredParticipation)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityRequired')}
{formatNumberPercentage(requiredMajorityPercentage)}
</KeyValueTableRow>
{!isUpdateMarket && (
<>
<KeyValueTableRow>
{t('numberOfVotingParties')}
{formatNumber(totalVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokensVotes')}
{formatNumber(totalTokensVoted, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokenVotedPercentage')}
{formatNumberPercentage(totalTokensPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfForVotes')}
{formatNumber(yesVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfAgainstVotes')}
{formatNumber(noVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('yesPercentage')}
{formatNumberPercentage(yesPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
{t('noPercentage')}
{formatNumberPercentage(noPercentage, 2)}
</KeyValueTableRow>
</>
)}
</KeyValueTable>
</RoundedWrapper>
)}
</>
);
};
@@ -0,0 +1 @@
export * from './proposal-voting-status';
@@ -0,0 +1,153 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
lastWeek,
mockWalletContext,
networkParamsQueryMock,
nextWeek,
mockNetworkParams,
} from '../../test-helpers/mocks';
import { ProposalVotingStatus } from './proposal-voting-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalVotingStatus
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</Router>
);
describe('ProposalVotingStatus', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-reached')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument();
});
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-reached')).toBeInTheDocument();
});
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
renderComponent(
generateProposal({
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument();
});
});
@@ -0,0 +1,174 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useVoteInformation } from '../../hooks';
import { BigNumber } from '../../../../lib/bignumber';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const statusClasses = (reached: boolean) =>
classNames('flex items-center gap-2 px-4 py-2 rounded-md', {
'bg-vega-green-700': reached,
'bg-vega-red-700': !reached,
});
const MajorityStatus = ({
reached,
requiredMajority,
}: {
reached: boolean;
requiredMajority: string | null | undefined;
}) => {
const { t } = useTranslation();
return (
<div
className={statusClasses(reached)}
data-testid="proposal-majority-status"
>
{reached ? <Icon name="tick" /> : <Icon name="cross" />}
{reached ? (
<div data-testid="majority-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityVotedForProposal')}
</>
) : (
t('requiredMajorityVotedForProposal')
)}
</div>
) : (
<div data-testid="majority-not-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityNotVotedForProposal')}
</>
) : (
t('requiredMajorityNotVotedForProposal')
)}
</div>
)}
</div>
);
};
const ParticipationStatus = ({ reached }: { reached: boolean }) => {
const { t } = useTranslation();
return (
<div className={statusClasses(reached)}>
{reached ? (
<>
<Icon name="tick" />
<div data-testid="participation-reached">
{t('minParticipationReached')}
</div>
</>
) : (
<>
<Icon name="cross" />
<div data-testid="participation-not-reached">
{t('minParticipationNotReached')}
</div>
</>
)}
</div>
);
};
export const ProposalVotingStatus = ({
proposal,
networkParams,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
}) => {
const { t } = useTranslation();
const { majorityMet, majorityLPMet, participationMet, participationLPMet } =
useVoteInformation({
proposal,
});
if (!proposal) {
return null;
}
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
let requiredVotingMajority = null;
let requiredVotingMajorityLP = null;
if (networkParams) {
switch (proposal.terms.change.__typename) {
case 'NewMarket':
requiredVotingMajority =
networkParams.governance_proposal_market_requiredMajority;
break;
case 'UpdateMarket':
requiredVotingMajority =
networkParams.governance_proposal_updateMarket_requiredMajority;
requiredVotingMajorityLP =
networkParams.governance_proposal_updateMarket_requiredMajorityLP;
break;
case 'NewAsset':
requiredVotingMajority =
networkParams.governance_proposal_asset_requiredMajority;
break;
case 'UpdateAsset':
requiredVotingMajority =
networkParams.governance_proposal_updateAsset_requiredMajority;
break;
case 'UpdateNetworkParameter':
requiredVotingMajority =
networkParams.governance_proposal_updateNetParam_requiredMajority;
break;
case 'NewFreeform':
requiredVotingMajority =
networkParams.governance_proposal_freeform_requiredMajority;
break;
}
}
if (isUpdateMarket) {
return (
<div className="mb-6">
<p>{t('Token vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="token-vote-statuses"
>
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
<p className="mt-4">{t('Liquidity provider vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="lp-vote-statuses"
>
<MajorityStatus
reached={majorityLPMet}
requiredMajority={requiredVotingMajorityLP}
/>{' '}
<ParticipationStatus reached={participationLPMet} />
</div>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-4 items-center mb-6">
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
);
};
@@ -34,6 +34,12 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
jest.mock('../vote-details', () => ({
VoteDetails: () => <div data-testid="proposal-vote-details"></div>,
}));
jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
@@ -98,6 +104,8 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
});
@@ -5,8 +5,9 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json';
import { ProposalVotesTable } from '../proposal-votes-table';
import { ProposalAssetDetails } from '../proposal-asset-details';
import { UserVote } from '../vote-details';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
@@ -21,6 +22,14 @@ import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
export enum ProposalType {
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
PROPOSAL_UPDATE_MARKET = 'PROPOSAL_UPDATE_MARKET',
PROPOSAL_NEW_ASSET = 'PROPOSAL_NEW_ASSET',
PROPOSAL_UPDATE_ASSET = 'PROPOSAL_UPDATE_ASSET',
PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER',
PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM',
}
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
@@ -71,32 +80,39 @@ export const Proposal = ({
}
let minVoterBalance = null;
let proposalType = null;
if (networkParams) {
switch (proposal.terms.change.__typename) {
case 'NewMarket':
minVoterBalance =
networkParams.governance_proposal_market_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NEW_MARKET;
break;
case 'UpdateMarket':
minVoterBalance =
networkParams.governance_proposal_updateMarket_minVoterBalance;
proposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
break;
case 'NewAsset':
minVoterBalance =
networkParams.governance_proposal_asset_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NEW_ASSET;
break;
case 'UpdateAsset':
minVoterBalance =
networkParams.governance_proposal_updateAsset_minVoterBalance;
proposalType = ProposalType.PROPOSAL_UPDATE_ASSET;
break;
case 'UpdateNetworkParameter':
minVoterBalance =
networkParams.governance_proposal_updateNetParam_minVoterBalance;
proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
break;
case 'NewFreeform':
minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
proposalType = ProposalType.PROPOSAL_FREEFORM;
break;
}
}
@@ -124,81 +140,91 @@ export const Proposal = ({
<ProposalHeader
proposal={proposal}
isListItem={false}
networkParams={networkParams}
voteState={voteState}
/>
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
</div>
)}
) : null}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</div>
)}
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<UserVote
proposal={proposal}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
</RoundedWrapper>
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
?.terms?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
/>
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
);
@@ -6,7 +6,11 @@ import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import { generateProposal } from '../../test-helpers/generate-proposals';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
@@ -89,6 +93,84 @@ describe('Proposals list item details', () => {
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '1000',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by LP vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(1000, 1000),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Renders proposal state: Open - 5 minutes left to vote', () => {
renderComponent(
generateProposal({
@@ -131,6 +213,43 @@ describe('Proposals list item details', () => {
);
});
it('Renders proposal state: Open - majority not reached', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
enactmentDatetime: nextWeek.toString(),
},
votes: {
no: generateNoVotes(1, 1000000000000000000),
yes: generateYesVotes(1, 1000000000000000000),
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Renders proposal state: Open - will pass', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(3000, 1000000000000000000),
no: generateNoVotes(0),
},
terms: {
closingDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass'
);
});
it('Renders proposal state: Rejected', () => {
renderComponent(
generateProposal({
@@ -11,6 +11,7 @@ import {
import Routes from '../../../routes';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useVoteInformation } from '../../hooks';
export const ProposalsListItemDetails = ({
proposal,
@@ -19,10 +20,18 @@ export const ProposalsListItemDetails = ({
}) => {
const { t } = useTranslation();
const state = proposal?.state;
const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({
proposal,
});
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLPVote');
const nowToEnactmentInHours = differenceInHours(
new Date(proposal?.terms.closingDatetime),
new Date()
);
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
let voteDetails: ReactNode;
let voteStatus: ReactNode;
@@ -69,11 +78,31 @@ export const ProposalsListItemDetails = ({
}
case ProposalState.STATE_OPEN: {
voteDetails = (
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
{t('left to vote')}
</span>
);
voteStatus =
(isUpdateMarket &&
(updateMarketWillPass ? (
<>
{t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod}
</>
) : (
<>
{t('currentlySetTo')} {t('fail')}
</>
))) ||
(willPassByTokenVote ? (
<>
{t('currentlySetTo')} {t('pass')}
</>
) : (
<>
{t('currentlySetTo')} {t('fail')}
</>
));
break;
}
case ProposalState.STATE_REJECTED: {
@@ -4,19 +4,28 @@ import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
interface ProposalsListItemProps {
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
networkParams: Partial<NetworkParamsResult> | null;
}
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
export const ProposalsListItem = ({
proposal,
networkParams,
}: ProposalsListItemProps) => {
const { voteState } = useUserVote(proposal?.id);
if (!proposal || !proposal.id) return null;
if (!proposal || !proposal.id || !networkParams) return null;
return (
<li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} voteState={voteState} />
<ProposalHeader
proposal={proposal}
networkParams={networkParams}
voteState={voteState}
/>
<ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper>
</li>
@@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import {
AsyncRenderer,
Button,
Toggle,
VegaIcon,
@@ -19,6 +20,10 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
@@ -70,6 +75,20 @@ export const ProposalsList = ({
lastBlockHeight,
}: ProposalsListProps) => {
const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const [filterString, setFilterString] = useState('');
const [closedProposalsView, setClosedProposalsView] =
useState<ClosedProposalsViewOptions>(
@@ -134,166 +153,181 @@ export const ProposalsList = ({
p?.party?.id?.toString().includes(filterString);
return (
<div data-testid="proposals-list">
<div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<div data-testid="proposals-list">
<div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
{DocsLinks && (
<div className="xs:justify-self-end" data-testid="new-proposal-link">
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</div>
</Button>
</ExternalLink>
</div>
{DocsLinks && (
<div
className="xs:justify-self-end"
data-testid="new-proposal-link"
>
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</div>
</Button>
</ExternalLink>
</div>
)}
</div>
<p className="mb-8">
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
{proposals.length > 0 && (
<ProposalsListFilter
filterString={filterString}
setFilterString={(value) => {
setFilterString(value);
if (value.length > 0) {
// If the filter is engaged, ensure the user is viewing governance proposals,
// as network upgrades do not have IDs to filter by and will be excluded.
setClosedProposalsView(
ClosedProposalsViewOptions.NetworkGovernance
);
}
}}
/>
)}
</div>
<p className="mb-8">
{t(
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} />
{proposals.length > 0 && (
<ProposalsListFilter
filterString={filterString}
setFilterString={(value) => {
setFilterString(value);
if (value.length > 0) {
// If the filter is engaged, ensure the user is viewing governance proposals,
// as network upgrades do not have IDs to filter by and will be excluded.
setClosedProposalsView(
ClosedProposalsViewOptions.NetworkGovernance
);
}
}}
/>
)}
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
<section className="relative">
<SubHeading title={t('closedProposals')} />
{sortedProposals.closed.length > 0 ||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
<>
{
// We need both the closed proposals and closed protocol upgrade
// proposals to be present for there to be a toggle. It also gets
// hidden if the user has filtered the list, as the upgrade proposals
// do not have the necessary fields for filtering.
sortedProposals.closed.length > 0 &&
sortedProtocolUpgradeProposals.closed.length > 0 &&
filterString.length < 1 && (
<div
className="grid w-full justify-end xl:-mt-12 pb-6"
data-testid="toggle-closed-proposals"
>
<div className="w-[440px]">
<Toggle
name="closed-proposals-toggle"
toggles={[
{
label: t(
ClosedProposalsViewOptions.NetworkGovernance
),
value: ClosedProposalsViewOptions.NetworkGovernance,
},
{
label: t(
ClosedProposalsViewOptions.NetworkUpgrades
),
value: ClosedProposalsViewOptions.NetworkUpgrades,
},
]}
checkedValue={closedProposalsView}
onChange={(e) =>
setClosedProposalsView(
e.target.value as ClosedProposalsViewOptions
)
}
/>
<section className="relative">
<SubHeading title={t('closedProposals')} />
{sortedProposals.closed.length > 0 ||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
<>
{
// We need both the closed proposals and closed protocol upgrade
// proposals to be present for there to be a toggle. It also gets
// hidden if the user has filtered the list, as the upgrade proposals
// do not have the necessary fields for filtering.
sortedProposals.closed.length > 0 &&
sortedProtocolUpgradeProposals.closed.length > 0 &&
filterString.length < 1 && (
<div
className="grid w-full justify-end xl:-mt-12 pb-6"
data-testid="toggle-closed-proposals"
>
<div className="w-[440px]">
<Toggle
name="closed-proposals-toggle"
toggles={[
{
label: t(
ClosedProposalsViewOptions.NetworkGovernance
),
value:
ClosedProposalsViewOptions.NetworkGovernance,
},
{
label: t(
ClosedProposalsViewOptions.NetworkUpgrades
),
value: ClosedProposalsViewOptions.NetworkUpgrades,
},
]}
checkedValue={closedProposalsView}
onChange={(e) =>
setClosedProposalsView(
e.target.value as ClosedProposalsViewOptions
)
}
/>
</div>
</div>
</div>
)
}
)
}
<ul data-testid="closed-proposals">
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkUpgrades && (
<div data-testid="closed-upgrade-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
</div>
)}
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkGovernance && (
<div data-testid="closed-governance-proposals">
{sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
<ul data-testid="closed-proposals">
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkUpgrades && (
<div data-testid="closed-upgrade-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
</div>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
</div>
)}
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkGovernance && (
<div data-testid="closed-governance-proposals">
{sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</div>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
</AsyncRenderer>
);
};
@@ -1,10 +1,15 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
interface ProposalsListProps {
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
@@ -12,6 +17,19 @@ interface ProposalsListProps {
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
]);
const [filterString, setFilterString] = useState('');
const filterPredicate = (
@@ -21,7 +39,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
p?.party?.id?.toString().includes(filterString);
return (
<>
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<Heading title={t('pageTitleRejectedProposals')} />
<ProposalsListFilter
filterString={filterString}
@@ -31,7 +53,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
{proposals.length > 0 ? (
<ul data-testid="rejected-proposals">
{proposals.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
) : (
@@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
</p>
)}
</section>
</>
</AsyncRenderer>
);
};
@@ -1 +0,0 @@
export * from './vote-breakdown';
@@ -1,348 +0,0 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
lastWeek,
mockWalletContext,
networkParamsQueryMock,
nextWeek,
} from '../../test-helpers/mocks';
import { VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteBreakdown proposal={proposal} />
</VegaWalletContext.Provider>
</MockedProvider>
</Router>
);
describe('VoteBreakdown', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
});
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
});
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
renderComponent(
generateProposal({
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(
screen.getByTestId('token-participation-not-met')
).toBeInTheDocument();
});
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '1000',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by liquidity vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(1000, fixedTokenValue),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0, fixedTokenValue),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Progress bar displays status - token majority', () => {
const yesVotes = 80;
const noVotes = 20;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
const element = screen.getByTestId('token-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${yesVotes}%`);
});
it('Progress bar displays status - token participation', () => {
const yesVotes = 40;
const noVotes = 20;
const totalVotes = yesVotes + noVotes;
const totalSupplyValue = mockTotalSupply.toNumber();
const expectedProgress = (totalVotes / totalSupplyValue) * 100; // Here it should be 60%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
const element = screen.getByTestId('token-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP majority', () => {
const yesVotesLP = 800;
const noVotesLP = 200;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: `${yesVotesLP}`,
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: `${noVotesLP}`,
},
},
})
);
const element = screen.getByTestId('lp-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP participation', () => {
const yesVotesLP = 400;
const noVotesLP = 600;
const totalVotesLP = yesVotesLP + noVotesLP;
const totalLPSupply = 1000;
const expectedProgress = (totalVotesLP / totalLPSupply) * 100; // 100%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: `${yesVotesLP}`,
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: `${noVotesLP}`,
},
},
})
);
const element = screen.getByTestId('lp-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
});
@@ -1,378 +0,0 @@
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}
interface VoteProgressProps {
percentageFor: BigNumber;
colourfulBg?: boolean;
testId?: string;
children?: ReactNode;
}
const VoteProgress = ({
percentageFor,
colourfulBg,
testId,
children,
}: VoteProgressProps) => {
const containerClasses = classNames(
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
);
const progressClasses = classNames(
'absolute h-full top-0 left-0',
colourfulBg ? 'bg-vega-green' : 'bg-white'
);
const textClasses = classNames(
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
);
return (
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
></div>
<div className={textClasses}>{children}</div>
</div>
);
};
interface StatusProps {
reached: boolean;
threshold: BigNumber;
text: string;
testId?: string;
}
const Status = ({ reached, threshold, text, testId }: StatusProps) => {
const { t } = useTranslation();
return (
<div data-testid={testId}>
{reached ? (
<div className="flex items-center gap-2">
<Icon name="tick" size={4} />
<span>
{threshold.toString()}% {text} {t('met')}
</span>
</div>
) : (
<div className="flex items-center gap-2">
<Icon name="cross" size={4} />
<span>
{threshold.toString()}% {text} {t('not met')}
</span>
</div>
)}
</div>
);
};
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
const {
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
yesEquityLikeShareWeight,
noEquityLikeShareWeight,
totalEquityLikeShareWeight,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
majorityMet,
majorityLPMet,
willPassByTokenVote,
willPassByLPVote,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const defaultDP = 2;
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
const participationThresholdProgress = BigNumber.min(
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
new BigNumber(100)
);
const lpParticipationThresholdProgress =
requiredParticipationLP &&
BigNumber.min(
totalLPTokensPercentage
.dividedBy(requiredParticipationLP)
.multipliedBy(100),
new BigNumber(100)
);
const willPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLiquidityVote');
const sectionWrapperClasses = classNames('grid sm:grid-cols-2 gap-6');
const headingClasses = classNames('mb-2 text-vega-dark-400');
const progressDetailsClasses = classNames(
'flex justify-between flex-wrap mt-2 text-sm'
);
return (
<div className="mb-6">
{isProposalOpen && (
<div
data-testid="vote-status"
className="flex items-center gap-1 mb-2 text-bold"
>
<span>
{willPass ? (
<Icon name="tick" size={5} className="text-vega-green" />
) : (
<Icon name="cross" size={5} className="text-vega-pink" />
)}
</span>
<span>{t('currentlySetTo')} </span>
{willPass ? (
<span>
<span className="text-vega-green">{t('pass')}</span>
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
</span>
) : (
<span className="text-vega-pink">{t('fail')}</span>
)}
</div>
)}
{isUpdateMarket && (
<div className="mb-4">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section data-testid="lp-majority-breakdown">
<VoteProgress
percentageFor={yesLPPercentage}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={formatNumber(
yesEquityLikeShareWeight,
defaultDP
)}
>
<button>
{yesEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<Tooltip
description={formatNumber(
noEquityLikeShareWeight,
defaultDP
)}
>
<button>
{noEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
</section>
<section data-testid="lp-participation-breakdown">
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<button>
{totalEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
</span>
</div>
</div>
</section>
</div>
</div>
)}
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
<div className={sectionWrapperClasses}>
<section data-testid="token-majority-breakdown">
<VoteProgress
percentageFor={yesPercentage}
colourfulBg={true}
testId="token-majority-progress"
>
<Status
reached={majorityMet}
threshold={requiredMajorityPercentage}
text={t('majorityThreshold')}
testId={
majorityMet ? 'token-majority-met' : 'token-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('tokenVotesFor')}:</span>
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
<button>
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
(
<Tooltip
description={<span>{yesPercentage.toFixed(defaultDP)}%</span>}
>
<button>{yesPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('tokenVotesAgainst')}:</span>
<Tooltip description={formatNumber(noTokens, defaultDP)}>
<button>
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
(
<Tooltip
description={<span>{noPercentage.toFixed(defaultDP)}%</span>}
>
<button>{noPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
</section>
<section data-testid="token-participation-breakdown">
<VoteProgress
percentageFor={participationThresholdProgress}
testId="token-participation-progress"
>
<Status
reached={participationMet}
threshold={requiredParticipation}
text={t('participationThreshold')}
testId={
participationMet
? 'token-participation-met'
: 'token-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalTokensVoted')}:</span>
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
<button>
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>({totalTokensPercentage.toFixed(defaultDP)}%)</span>
</div>
</div>
</section>
</div>
</div>
);
};
@@ -1 +1 @@
export { UserVote } from './user-vote';
export { VoteDetails } from './vote-details';
@@ -1,78 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface UserVoteProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
voteDatetime: Date | null;
}
export const UserVote = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog,
voteState,
voteDatetime,
}: UserVoteProps) => {
const { pubKey } = useVegaWallet();
const { t } = useTranslation();
return (
<section data-testid="user-vote">
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
{pubKey ? (
proposal && (
<VoteButtonsContainer
voteState={voteState}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id ?? ''}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
) : (
<div className="pb-2">
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</div>
)}
</section>
);
};
@@ -190,15 +190,14 @@ export const VoteButtons = ({
(voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span className="mx-1 text-white font-bold uppercase">
<span className="text-white font-bold">
{t(`voteState_${voteState}`)}
</span>{' '}
{voteDatetime ? (
<span>on {format(voteDatetime, DATE_FORMAT_LONG)}. </span>
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalVotable ? (
<ButtonLink
className="text-white"
data-testid="change-vote-button"
onClick={() => {
setChangeVote(true);
@@ -0,0 +1,255 @@
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { VoteProgress } from '@vegaprotocol/proposals';
import { formatNumber } from '../../../../lib/format-number';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { useVoteInformation } from '../../hooks';
import { CurrentProposalStatus } from '../current-proposal-status';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface VoteDetailsProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
voteDatetime: Date | null;
}
export const VoteDetails = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
proposalType,
submit,
transaction,
dialog,
voteState,
voteDatetime,
}: VoteDetailsProps) => {
const { pubKey } = useVegaWallet();
const {
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const defaultDecimals = 2;
const daysLeft = t('daysLeft', {
daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)),
});
return (
<>
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<section>
<SubHeading title={t('liquidityVotes')} />
<p data-testid="liquidity-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>
{'. '}
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
</p>
<table className="w-full mb-8">
<thead>
<tr>
<th className="text-vega-green w-[18%] text-left">
{t('for')}
</th>
<th>
<VoteProgress
threshold={requiredMajorityLPPercentage}
progress={yesLPPercentage}
/>
</th>
<th className="text-danger w-[18%] text-right">
{t('against')}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
className="text-left"
data-testid="vote-progress-indicator-percentage-for"
>
{yesLPPercentage.toFixed(defaultDecimals)}%
</td>
<td className="text-center text-white">
{t('majorityRequired')}{' '}
{requiredMajorityLPPercentage.toFixed(defaultDecimals)}%
</td>
<td
className="text-right"
data-testid="vote-progress-indicator-percentage-against"
>
{noLPPercentage.toFixed(defaultDecimals)}%
</td>
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationLPMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalLPTokensPercentage, defaultDecimals)}%
<span className="ml-4">
{requiredParticipationLP && (
<>
({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '}
{t('governanceRequired')})
</>
)}
</span>
</p>
</section>
)}
<section data-testid="votes-table">
<SubHeading title={t('tokenVotes')} />
<p data-testid="token-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>
{'. '}
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
</p>
<table className="w-full mb-4">
<thead>
<tr>
<th className="text-vega-green w-[18%] text-left">{t('for')}</th>
<th>
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</th>
<th className="text-danger w-[18%] text-right">{t('against')}</th>
</tr>
</thead>
<tbody>
<tr>
<td
className="text-left"
data-testid="vote-progress-indicator-percentage-for"
>
{yesPercentage.toFixed(defaultDecimals)}%
</td>
<td className="text-center text-white">
{t('majorityRequired')}{' '}
{requiredMajorityPercentage.toFixed(defaultDecimals)}%
</td>
<td
className="text-right"
data-testid="vote-progress-indicator-percentage-against"
>
{noPercentage.toFixed(defaultDecimals)}%
</td>
</tr>
<tr>
<td data-testid="vote-progress-indicator-tokens-for">
{' '}
{formatNumber(yesTokens, defaultDecimals)}{' '}
</td>
<td></td>
<td
data-testid="vote-progress-indicator-tokens-against"
className="text-right"
>
{formatNumber(noTokens, defaultDecimals)}
</td>
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalTokensVoted, defaultDecimals)}{' '}
{formatNumber(totalTokensPercentage, defaultDecimals)}%
<span className="ml-4">
({formatNumber(requiredParticipation, defaultDecimals)}%{' '}
{t('governanceRequired')})
</span>
</p>
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<p>{t('votingThresholdInfo')}</p>
)}
<section className="mt-10">
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
{pubKey ? (
proposal && (
<VoteButtonsContainer
voteState={voteState}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id ?? ''}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
) : (
<RoundedWrapper paddingBottom={true}>
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</RoundedWrapper>
)}
</section>
</section>
</>
);
};
@@ -24,9 +24,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.getByTestId('deal-ticket-fee-margin-required').within(() => {
cy.get('button').click();
});
});
describe('limit order', () => {
+4 -4
View File
@@ -16,12 +16,12 @@ NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
# TAG name of the current app version - TODO: bump to the latest upon release
NX_APP_VERSION=v0.21.0-core-0.72.14
NX_APP_VERSION=v0.20.21-core-0.71.6
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
+3 -3
View File
@@ -19,9 +19,9 @@ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-ma
NX_APP_VERSION=v0.20.19-core-0.71.6
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
+1 -1
View File
@@ -19,6 +19,6 @@ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fa
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
+3 -3
View File
@@ -19,9 +19,9 @@ NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fa
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
@@ -1,49 +1,27 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
LocalStoragePersistTabs as Tabs,
Tab,
TradingAnchorButton,
} from '@vegaprotocol/ui-toolkit';
import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
export const MarketsPage = () => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border rounded-sm border-default">
<div className="h-full my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
</Tab>
<Tab
id="proposed-markets"
name={t('Proposed markets')}
menu={
<TradingAnchorButton size="extra-small" href={externalLink}>
{t('Propose a new market')}
</TradingAnchorButton>
}
>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Proposed />
</Tab>
<Tab id="closed-markets" name={t('Closed markets')}>
+19 -1
View File
@@ -1,6 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { ProposalsList } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Proposed = () => {
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
const tokenLink = useLinks(DApp.Token);
const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL);
return (
<>
<div className="h-[400px]">
<ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />
</div>
<ExternalLink className="py-4 px-[11px] text-sm" href={externalLink}>
{t('Propose a new market')}
</ExternalLink>
</>
);
};
@@ -4,7 +4,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
@@ -12,13 +12,12 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
@@ -151,7 +150,6 @@ const AccountHistoryManager = ({
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
@@ -176,114 +174,111 @@ const AccountHistoryManager = ({
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
const accountTypeMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<DropdownMenuItem
key={type}
onClick={() => setAccountType(type as Schema.AccountType)}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [accountType]);
const assetsMenu = useMemo(() => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAssetId(a.id)}>
{a.symbol}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}, [asset, assets, setAssetId]);
const marketsMenu = useMemo(() => {
return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN &&
markets?.length ? (
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{market && (
<DropdownMenuItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</DropdownMenuItem>
)}
{markets?.map((m) => (
<DropdownMenuItem key={m.id} onClick={() => resolveMarket(m)}>
{m.tradableInstrument.instrument.code}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : null;
}, [markets, market, accountType, resolveMarket]);
useEffect(() => {
if (
accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN ||
market?.tradableInstrument.instrument.product.settlementAsset.id !==
asset?.id
) {
setMarket(null);
}
}, [accountType, asset?.id, market]);
return (
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (
a.id !==
market?.tradableInstrument.instrument.product
.settlementAsset.id
) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0">
<>
{accountTypeMenu}
{assetsMenu}
{marketsMenu}
</>
</div>
<div className="justify-items-end">
<div className="pt-1 justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
@@ -292,19 +287,16 @@ const AccountHistoryManager = ({
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="flex-1">
<div className="h-5/6 px-4">
{asset && (
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
)}
</div>
</div>
@@ -0,0 +1 @@
export * from './vega-wallet-container';
@@ -0,0 +1,27 @@
import { render, screen } from '@testing-library/react';
import { VegaWalletContainer } from './vega-wallet-container';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { PartialDeep } from 'type-fest';
const generateJsx = (context: PartialDeep<VegaWalletContextShape>) => {
return (
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
<VegaWalletContainer>
<div data-testid="child" />
</VegaWalletContainer>
</VegaWalletContext.Provider>
);
};
describe('VegaWalletContainer', () => {
it('doesnt render children if not connected', () => {
render(generateJsx({ pubKey: null }));
expect(screen.queryByTestId('child')).not.toBeInTheDocument();
});
it('renders children if connected', () => {
render(generateJsx({ pubKey: '0x123' }));
expect(screen.getByTestId('child')).toBeInTheDocument();
});
});
@@ -0,0 +1,35 @@
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/i18n';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
interface VegaWalletContainerProps {
children: ReactNode;
}
export const VegaWalletContainer = ({ children }: VegaWalletContainerProps) => {
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { pubKey } = useVegaWallet();
if (!pubKey) {
return (
<Splash>
<div className="text-center">
<p className="mb-4" data-testid="connect-vega-wallet-text">
{t('Connect your Vega wallet')}
</p>
<Button
onClick={openVegaWalletDialog}
data-testid="vega-wallet-connect"
>
{t('Connect')}
</Button>
</div>
</Splash>
);
}
return <>{children}</>;
};
@@ -16,32 +16,29 @@ export const TelemetryApproval = ({
setTelemetryValue,
}: Props) => {
return (
<div className="flex flex-col">
<div className="flex flex-col py-3">
<div className="mr-4" role="form">
<p className="mb-4">
<div className="mt-2">
{t(
'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.'
)}
</p>
<div className="flex items-start mb-2 gap-3">
<VegaIcon name={VegaIconNames.EYE_OFF} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Anonymous')}</h6>
<p className="text-muted">
{t('Your identity is always anonymous on Vega')}
</p>
</div>
<div className="flex items-center mt-2">
<VegaIcon name={VegaIconNames.EYE_OFF} size={24} />
<div className="flex flex-col gap-1 ml-6">
<div className="font-semibold">{t('Anonymous')}</div>
<div>{t('Your identity is always anonymous on Vega')}</div>
</div>
</div>
<div className="flex items-start mb-4 gap-3">
<VegaIcon name={VegaIconNames.COG} size={18} />
<div className="flex flex-col gap-1">
<h6 className="font-semibold">{t('Optional')}</h6>
<p className="text-muted">
{t('You can opt out any time via settings')}
</p>
<div className="flex items-center mt-2">
<VegaIcon name={VegaIconNames.COG} size={24} />
<div className="flex flex-col gap-1 ml-6">
<div className="font-semibold">{t('Optional')}</div>
<div>{t('You can opt out any time via settings')}</div>
</div>
</div>
<div className="flex flex-col items-center justify-around gap-2">
<div className="flex flex-col justify-around items-center mt-10 gap-4 w-full px-4">
<TradingButton
onClick={() => setTelemetryValue('false')}
size="small"
@@ -92,7 +92,7 @@ export const WelcomeDialog = () => {
intent: Intent.Primary,
content: (
<>
<h3 className="mb-1 text-sm uppercase">
<h3 className="text-sm uppercase mb-1">
{t('Improve vega console')}
</h3>
<TelemetryApproval
+6 -21
View File
@@ -4,37 +4,22 @@ export default function Document() {
return (
<>
<Head>
{/*
meta tags
- next advised against using _document for this, so they exist in our
- single page index.page.tsx
*/}
{/* icons */}
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<link
rel="apple-touch-icon"
content="https://static.vega.xyz/favicon.ico"
/>
{/* fonts */}
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
/>
{/* styles */}
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
{/* scripts */}
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<Html>
+28 -1
View File
@@ -10,16 +10,23 @@ export default function Index() {
return (
<>
<Head>
<link rel="stylesheet" href="https://static.vega.xyz/fonts.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Vega Protocol - Console" />
<meta name="description" content="Vega Protocol - VEGA Console" />
<meta name="og:type" content="website" />
<meta name="og:url" content="https://console.vega.xyz/" />
<meta name="og:title" content="Vega Protocol - Console" />
<meta name="og:site_name" content="Vega Protocol - Console" />
<meta name="og:image" content="https://static.vega.xyz/favicon.ico" />
<meta
name="twitter:card"
content="https://static.vega.xyz/favicon.ico"
@@ -32,6 +39,26 @@ export default function Index() {
/>
<meta name="twitter:image:alt" content="VEGA logo" />
<meta name="twitter:site" content="@vegaprotocol" />
<meta name="description" content="Vega Protocol - Console" />
<link
rel="apple-touch-icon"
content="https://static.vega.xyz/favicon.ico"
/>
<link
rel="preload"
href="https://static.vega.xyz/AlphaLyrae-Medium.woff2"
as="font"
type="font/woff2"
/>
{/* eslint-disable-next-line @next/next/no-css-tags */}
<link rel="stylesheet" href="/preloader.css" media="all" />
<link
rel="icon"
type="image/x-icon"
href="https://static.vega.xyz/favicon.ico"
/>
<script src="/theme-setter.js" type="text/javascript" async />
</Head>
<ClientRouter />
</>
@@ -2,8 +2,8 @@ import { ETHERSCAN_ADDRESS, useEtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import {
ActionsDropdown,
TradingDropdownCopyItem,
TradingDropdownItem,
DropdownMenuCopyItem,
DropdownMenuItem,
Link,
VegaIcon,
VegaIconNames,
@@ -30,49 +30,49 @@ export const AccountsActionsDropdown = ({
return (
<ActionsDropdown>
<TradingDropdownItem
<DropdownMenuItem
key={'deposit'}
data-testid="deposit"
onClick={onClickDeposit}
>
<VegaIcon name={VegaIconNames.DEPOSIT} size={16} />
{t('Deposit')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'withdraw'}
data-testid="withdraw"
onClick={onClickWithdraw}
>
<VegaIcon name={VegaIconNames.WITHDRAW} size={16} />
{t('Withdraw')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'transfer'}
data-testid="transfer"
onClick={onClickTransfer}
>
<VegaIcon name={VegaIconNames.TRANSFER} size={16} />
{t('Transfer')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
key={'breakdown'}
data-testid="breakdown"
onClick={onClickBreakdown}
>
<VegaIcon name={VegaIconNames.BREAKDOWN} size={16} />
{t('View usage breakdown')}
</TradingDropdownItem>
<TradingDropdownItem
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
openAssetDialog(assetId, e.target as HTMLElement);
}}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View asset details')}
</TradingDropdownItem>
<TradingDropdownCopyItem value={assetId} text={t('Copy asset ID')} />
</DropdownMenuItem>
<DropdownMenuCopyItem value={assetId} text={t('Copy asset ID')} />
{assetContractAddress && (
<TradingDropdownItem>
<DropdownMenuItem>
<Link
href={etherscanLink(
ETHERSCAN_ADDRESS.replace(':hash', assetContractAddress)
@@ -84,7 +84,7 @@ export const AccountsActionsDropdown = ({
{t('View on Etherscan')}
</span>
</Link>
</TradingDropdownItem>
</DropdownMenuItem>
)}
</ActionsDropdown>
);
+11 -15
View File
@@ -11,13 +11,9 @@ import type {
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS } from '@vegaprotocol/datagrid';
import {
Intent,
TradingButton,
VegaIcon,
VegaIconNames,
TooltipCellComponent,
} from '@vegaprotocol/ui-toolkit';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type {
IGetRowsParams,
@@ -190,7 +186,7 @@ export const AccountTable = ({
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="inline-block ml-2 w-14 text-muted">
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
@@ -252,26 +248,26 @@ export const AccountTable = ({
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 105 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 105 : COL_DEFS.actions.maxWidth,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.balance === '0') {
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<TradingButton
size="extra-small"
intent={Intent.Primary}
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</TradingButton>
</Button>
</CenteredGridCellWrapper>
);
}
+8 -8
View File
@@ -8,6 +8,7 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -15,7 +16,6 @@ import {
TradingSelect,
Tooltip,
TradingCheckbox,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
@@ -276,9 +276,9 @@ export const TransferForm = ({
decimals={asset?.decimals}
/>
)}
<TradingButton type="submit" fill={true}>
<Button type="submit" variant="primary" fill={true}>
{t('Confirm transfer')}
</TradingButton>
</Button>
</form>
);
};
@@ -309,8 +309,8 @@ export const TransferFee = ({
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
return (
<div className="flex flex-col mb-4 text-xs gap-2">
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to %s`,
@@ -324,7 +324,7 @@ export const TransferFee = ({
{formatNumber(fee, decimals)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount to be transferred (without the fee)`
@@ -337,7 +337,7 @@ export const TransferFee = ({
{formatNumber(amount, decimals)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-1">
<div className="flex justify-between gap-1 items-center flex-wrap">
<Tooltip
description={t(
`The total amount taken from your account. The amount to be transferred plus the fee.`
@@ -384,7 +384,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
</button>
+3 -4
View File
@@ -2,10 +2,9 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
Dialog,
Icon,
Splash,
SyntaxHighlighter,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { create } from 'zustand';
import { AssetDetailsTable } from './asset-details-table';
@@ -83,7 +82,7 @@ export const AssetDetailsDialog = ({
return (
<Dialog
title={title}
icon={<VegaIcon name={VegaIconNames.INFO} />}
icon={<Icon name="info-sign"></Icon>}
open={open}
onChange={(isOpen) => onChange(isOpen)}
onCloseAutoFocus={(e) => {
@@ -98,7 +97,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="my-4 text-xs">
<p className="text-xs my-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+6 -3
View File
@@ -3,8 +3,11 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types';
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { CopyWithTooltip, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import {
CopyWithTooltip,
Icon,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -115,7 +118,7 @@ export const rows: Rows = [
</EtherscanLink>{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<VegaIcon size={14} name={VegaIconNames.COPY} />
<Icon size={3} name="duplicate" />
</button>
</CopyWithTooltip>
</>
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
describe('CandlesMenu', () => {
it('should render with the correct default studies', async () => {
it('should render with volume study showing by default', async () => {
render(<CandlesMenu />);
await userEvent.click(
@@ -13,21 +13,5 @@ describe('CandlesMenu', () => {
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
});
it('should render with the correct default overlays', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByRole('button', {
name: 'Overlays',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Moving average')).toHaveAttribute(
'data-state',
'checked'
);
});
});
@@ -15,8 +15,8 @@ interface StoredSettings {
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
overlays: [],
studies: [Study.VOLUME],
};
export const useCandlesChartSettingsStore = create<
@@ -12,8 +12,6 @@ import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as Accordion from '@radix-ui/react-accordion';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
@@ -24,7 +22,6 @@ import {
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit';
const emptyValue = '-';
@@ -247,72 +244,55 @@ export const DealTicketMarginDetails = ({
return (
<>
<Accordion.Root type="single" collapsible>
<Accordion.Item value="margin">
<KeyValue
id="margin-required"
label={
<Accordion.Trigger className={TOOLTIP_TRIGGER_CLASS_NAME}>
{t('Margin required')}
</Accordion.Trigger>
}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<Accordion.Content>
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
<KeyValue
label={t('Margin required')}
value={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
)}
formattedValue={formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
symbol={assetSymbol}
/>
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={TOTAL_MARGIN_AVAILABLE(
formatValue(generalAccountBalance, assetDecimals, quantum),
formatValue(marginAccountBalance, assetDecimals, quantum),
formatValue(currentMargins?.maintenanceLevel, assetDecimals, quantum),
assetSymbol
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
{projectedMargin}
<KeyValue
label={t('Liquidation price estimate')}
@@ -3,8 +3,7 @@ import classnames from 'classnames';
import type { ReactNode } from 'react';
export interface KeyValuePros {
id?: string;
label: ReactNode;
label: string;
value?: string | null | undefined;
symbol: string;
indent?: boolean | undefined;
@@ -14,7 +13,6 @@ export interface KeyValuePros {
}
export const KeyValue = ({
id,
label,
value,
labelDescription,
@@ -33,11 +31,9 @@ export const KeyValue = ({
);
return (
<div
data-testid={`deal-ticket-fee-${
!id && typeof label === 'string'
? label.toLocaleLowerCase().replace(/\s/g, '-')
: id
}`}
data-testid={
'deal-ticket-fee-' + label.toLocaleLowerCase().replace(/\s/g, '-')
}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
+3 -1
View File
@@ -305,7 +305,9 @@ describe('Deposit form', () => {
target: { value: '8' },
});
fireEvent.click(screen.getByRole('button', { name: 'Deposit' }));
fireEvent.click(
screen.getByText('Deposit', { selector: '[type="submit"]' })
);
await waitFor(() => {
expect(props.submitDeposit).toHaveBeenCalledWith({
+10 -9
View File
@@ -13,6 +13,7 @@ import {
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -22,7 +23,6 @@ import {
ButtonLink,
TradingSelect,
truncateMiddle,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -186,14 +186,14 @@ export const DepositForm = ({
);
}
return (
<TradingButton
<Button
onClick={openDialog}
intent={Intent.Primary}
variant="primary"
type="button"
data-testid="connect-eth-wallet-btn"
>
{t('Connect')}
</TradingButton>
</Button>
);
}}
/>
@@ -435,14 +435,15 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
/>
</div>
)}
<TradingButton
<Button
type="submit"
data-testid="deposit-submit"
variant={isActive ? 'primary' : 'default'}
fill
disabled={!isActive || invalidChain}
disabled={invalidChain}
>
{t('Deposit')}
</TradingButton>
</Button>
</>
);
};
@@ -454,7 +455,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
/>
);
};
@@ -512,7 +513,7 @@ export const AddressField = ({
setIsInput((curr) => !curr);
onChange();
}}
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
data-testid="enter-pubkey-manually"
>
{isInput ? t('Select from wallet') : t('Enter manually')}
+4 -3
View File
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react';
import { z } from 'zod';
import {
TradingButton,
Button,
Loader,
TradingFormGroup,
TradingInput,
@@ -157,14 +157,15 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
<Loader size="small" />
</div>
)}
<TradingButton
<Button
variant="primary"
fill
disabled={disabled}
type="submit"
data-testid="ledger-download-button"
>
{t('Download')}
</TradingButton>
</Button>
</div>
</form>
);
@@ -12,10 +12,9 @@ import {
TradingFormGroup,
TradingInput,
TradingInputError,
Button,
Dialog,
VegaIcon,
VegaIconNames,
TradingButton,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import type { Order } from '../order-data-provider';
@@ -38,7 +37,7 @@ export const OrderEditDialog = ({
order,
onSubmit,
}: OrderEditDialogProps) => {
const headerClassName = 'text-xs font-bold text-black dark:text-white';
const headerClassName = 'text-lg font-bold text-black dark:text-white';
const {
register,
formState: { errors },
@@ -58,13 +57,13 @@ export const OrderEditDialog = ({
open={isOpen}
onChange={onChange}
title={t('Edit order')}
icon={<VegaIcon name={VegaIconNames.EDIT} />}
icon={<Icon name="edit" />}
>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{order.market && (
<div className="md:col-span-2">
<p className={headerClassName}>{t(`Market`)}</p>
<p>{order.market.tradableInstrument.instrument.code}</p>
<p>{t(`${order.market.tradableInstrument.instrument.name}`)}</p>
</div>
)}
{order.type === Schema.OrderType.TYPE_LIMIT && order.market && (
@@ -150,7 +149,9 @@ export const OrderEditDialog = ({
)}
</TradingFormGroup>
</div>
<TradingButton type="submit">{t('Update')}</TradingButton>
<Button variant="primary" size="md" type="submit">
{t('Update')}
</Button>
</form>
</Dialog>
);
@@ -10,7 +10,7 @@ import {
ActionsDropdown,
ButtonLink,
TradingDropdownCopyItem,
TradingDropdownItem,
DropdownMenuItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
@@ -271,20 +271,19 @@ export const OrderListTable = memo<
{
colId: 'amend',
...COL_DEFS.actions,
minWidth: showAllActions ? 80 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 80 : COL_DEFS.actions.minWidth,
minWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: Order }) => {
if (!data) return null;
return (
<div className="flex items-center justify-end gap-2">
<div className="flex gap-2 items-center justify-end">
{isOrderAmendable(data) && !props.isReadOnly && (
<>
{!data.icebergOrder && (
<ButtonLink
data-testid="edit"
onClick={() => onEdit(data)}
title={t('Edit order')}
>
<VegaIcon name={VegaIconNames.EDIT} size={16} />
</ButtonLink>
@@ -292,7 +291,6 @@ export const OrderListTable = memo<
<ButtonLink
data-testid="cancel"
onClick={() => onCancel(data)}
title={t('Cancel order')}
>
<VegaIcon name={VegaIconNames.CROSS} size={16} />
</ButtonLink>
@@ -303,14 +301,14 @@ export const OrderListTable = memo<
value={data.id}
text={t('Copy order ID')}
/>
<TradingDropdownItem
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() => onView(data)}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</TradingDropdownItem>
</DropdownMenuItem>
</ActionsDropdown>
</div>
);
@@ -12,7 +12,7 @@ import {
ButtonLink,
VegaIcon,
VegaIconNames,
TradingDropdownItem,
DropdownMenuItem,
TradingDropdownCopyItem,
Pill,
} from '@vegaprotocol/ui-toolkit';
@@ -246,7 +246,7 @@ export const StopOrdersTable = memo(
if (!data) return null;
return (
<div className="flex items-center justify-end gap-2">
<div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && (
<ButtonLink
@@ -263,7 +263,7 @@ export const StopOrdersTable = memo(
value={data.order.id}
text={t('Copy order ID')}
/>
<TradingDropdownItem
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() =>
@@ -273,7 +273,7 @@ export const StopOrdersTable = memo(
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</TradingDropdownItem>
</DropdownMenuItem>
</ActionsDropdown>
)}
</div>
@@ -61,7 +61,7 @@ describe('Positions', () => {
'Market',
'Size / Notional',
'Entry / Mark',
'Margin / Leverage',
'Margin',
'Liquidation',
'Realised PNL',
'Unrealised PNL',
+4 -5
View File
@@ -292,7 +292,7 @@ export const PositionsTable = ({
},
},
{
headerName: t('Margin / Leverage'),
headerName: t('Margin'),
colId: 'margin',
type: 'rightAligned',
cellClass: 'font-mono text-right',
@@ -456,14 +456,13 @@ export const PositionsTable = ({
...COL_DEFS.actions,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
return (
<div className="flex items-center justify-end gap-2">
<div className="flex gap-2 items-center justify-end">
{data?.openVolume &&
data?.openVolume !== '0' &&
data.partyId === pubKey ? (
<ButtonLink
data-testid="close-position"
onClick={() => data && onClose(data)}
title={t('Close position')}
>
<VegaIcon name={VegaIconNames.CROSS} size={16} />
</ButtonLink>
@@ -549,9 +548,9 @@ const WarningCell = ({
showIcon?: boolean;
}) => {
return (
<div className="flex items-center justify-end">
<div className="flex justify-end items-center">
{showIcon && (
<span className="mr-2 text-black dark:text-white">
<span className="text-black dark:text-white mr-2">
<VegaIcon name={VegaIconNames.EXCLAIMATION_MARK} size={12} />
</span>
)}
@@ -35,13 +35,16 @@ export const ProposalsList = ({
const { columnDefs, defaultColDef } = useColumnDefs();
return (
<AgGrid
columnDefs={columnDefs}
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
<div className="relative h-full">
<AgGrid
className="w-full h-full"
columnDefs={columnDefs}
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
/>
</div>
);
};
@@ -7,7 +7,7 @@ export type { IconName } from '@blueprintjs/icons';
export interface IconProps {
name: IconName;
className?: string;
size?: 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16;
size?: 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16;
ariaLabel?: string;
}
@@ -23,7 +23,6 @@ export const Icon = ({ size = 4, name, className, ariaLabel }: IconProps) => {
'w-2 h-2': size === 2,
'w-3 h-3': size === 3,
'w-4 h-4': size === 4,
'w-5 h-5': size === 5,
'w-6 h-6': size === 6,
'w-8 h-8': size === 8,
'w-10 h-10': size === 10,
@@ -1,6 +1,11 @@
export const IconInfo = ({ size = 14 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 14 14">
<svg
width={size}
height={size}
viewBox="0 0 14 14"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M7 0C3.13 0 0 3.13 0 7C0 10.87 3.13 14 7 14C10.87 14 14 10.87 14 7C14 3.13 10.87 0 7 0ZM7.75 10.75H6.25V5.75H7.75V10.75ZM7.75 4.75H6.25V3.25H7.75V4.75Z" />
</svg>
);
@@ -10,7 +10,7 @@ import { useCallback } from 'react';
import { useLayoutEffect } from 'react';
import { useRef } from 'react';
import { Intent } from '../../utils/intent';
import { Icon, VegaIcon, VegaIconNames } from '../icon';
import { Icon } from '../icon';
import { Loader } from '../loader';
import { t } from '@vegaprotocol/i18n';
@@ -317,14 +317,18 @@ export const Toast = ({
}
)}
>
<div className="relative flex">
<div className="flex relative">
<button
type="button"
data-testid="toast-close"
onClick={closeToast}
className="absolute top-0 right-0 z-20 flex items-center p-2"
className="absolute p-[8px] top-[3px] right-[3px] z-20"
>
<VegaIcon name={VegaIconNames.CROSS} size={12} />
<Icon
name="cross"
size={3}
className="!block dark:text-white !w-[11px] !h-[11px]"
/>
</button>
<div
data-testid="toast-accent"
@@ -344,7 +348,7 @@ export const Toast = ({
// red
'bg-vega-red-500 text-vega-red-600': intent === Intent.Danger,
},
'w-8 p-2',
'w-8 p-[9px]',
'flex justify-center'
)}
>
@@ -3,7 +3,7 @@ import { usePrevious } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import type { Ref } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { TradingButton } from '../trading-button';
import { Button } from '../button';
import { Toast } from './toast';
import type { Toasts } from './use-toasts';
import { ToastPosition, useToasts, useToastsConfiguration } from './use-toasts';
@@ -87,27 +87,26 @@ export const ToastsContainer = ({
</li>
);
})}
<div
<Button
title={t('Dismiss all toasts')}
size="sm"
fill={true}
className={classNames(
'absolute w-full top-[-38px] right-0 z-20',
'absolute top-[-38px] right-0 z-20',
'transition-opacity',
'opacity-0 group-hover:opacity-50 hover:!opacity-100',
'text-sm text-black dark:text-white bg-white dark:bg-black hover:!bg-white hover:dark:!bg-black',
{
hidden: validToasts.length === 0,
}
)}
onClick={() => {
closeAll();
}}
variant={'default'}
>
<TradingButton
title={t('Dismiss all toasts')}
size="small"
fill={true}
onClick={() => {
closeAll();
}}
>
{t('Dismiss all')}
</TradingButton>
</div>
{t('Dismiss all')}
</Button>
</ul>
</Portal>
);
@@ -20,9 +20,6 @@ export interface TooltipProps {
sideOffset?: number;
}
export const TOOLTIP_TRIGGER_CLASS_NAME =
'underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed';
// Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({
children,
@@ -35,7 +32,10 @@ export const Tooltip = ({
description ? (
<Provider delayDuration={200} skipDelayDuration={100}>
<Root open={open}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME}>
<Trigger
asChild
className="underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed"
>
{children}
</Trigger>
{description && (
@@ -146,7 +146,7 @@ export const TradingDropdownItemIndicator = forwardRef<
<DropdownMenuPrimitive.ItemIndicator
{...itemIndicatorProps}
ref={forwardedRef}
className="flex-end text-vega-green-600 dark:text-vega-green"
className="flex-end text-vega-green"
>
<VegaIcon name={VegaIconNames.TICK} />
</DropdownMenuPrimitive.ItemIndicator>
@@ -2,7 +2,7 @@ import type { Ref, SelectHTMLAttributes } from 'react';
import { useRef } from 'react';
import { forwardRef } from 'react';
import classNames from 'classnames';
import { VegaIcon, VegaIconNames } from '..';
import { Icon } from '..';
import { defaultSelectElement } from '../../utils/shared';
import * as SelectPrimitive from '@radix-ui/react-select';
@@ -16,7 +16,7 @@ export interface TradingSelectProps
export const TradingSelect = forwardRef<HTMLSelectElement, TradingSelectProps>(
({ className, hasError, ...props }, ref) => (
<div className="relative flex items-center">
<div className="flex items-center relative">
<select
ref={ref}
{...props}
@@ -26,9 +26,10 @@ export const TradingSelect = forwardRef<HTMLSelectElement, TradingSelectProps>(
'appearance-none rounded-md'
)}
/>
<span className="absolute z-10 flex items-center pointer-events-none right-2">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
</span>
<Icon
name="chevron-down"
className="absolute right-4 z-10 pointer-events-none"
/>
</div>
)
);
@@ -51,7 +52,7 @@ export const TradingRichSelect = forwardRef<
return (
<div
ref={containerRef as Ref<HTMLDivElement>}
className="relative flex items-center"
className="flex items-center relative"
>
<SelectPrimitive.Root {...props} defaultOpen={false}>
<SelectPrimitive.Trigger
@@ -65,8 +66,8 @@ export const TradingRichSelect = forwardRef<
ref={forwardedRef}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className={classNames('absolute right-2')}>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
<SelectPrimitive.Icon className={classNames('absolute right-4')}>
<Icon name="chevron-down" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal container={containerRef.current}>
@@ -84,12 +85,12 @@ export const TradingRichSelect = forwardRef<
side={'bottom'}
align={'center'}
>
<SelectPrimitive.ScrollUpButton className="absolute z-20 flex items-center justify-center w-full h-6 py-1 bg-gradient-to-t from-transparent to-neutral-50 dark:to-neutral-900">
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
<SelectPrimitive.ScrollUpButton className="flex items-center justify-center py-1 absolute w-full h-6 z-20 bg-gradient-to-t from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-up" />
</SelectPrimitive.ScrollUpButton>
<SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
<SelectPrimitive.ScrollDownButton className="absolute bottom-0 z-20 flex items-center justify-center w-full h-6 py-1 bg-gradient-to-b from-transparent to-neutral-50 dark:to-neutral-900">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
<SelectPrimitive.ScrollDownButton className="flex items-center justify-center py-1 absolute bottom-0 w-full h-6 z-20 bg-gradient-to-b from-transparent to-neutral-50 dark:to-neutral-900">
<Icon name="chevron-down" />
</SelectPrimitive.ScrollDownButton>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
@@ -122,7 +123,7 @@ export const TradingOption = forwardRef<
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator className="absolute right-4 top-[50%] translate-y-[-50%]">
<VegaIcon name={VegaIconNames.TICK} />
<Icon name="tick" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
));
@@ -598,7 +598,7 @@ const VegaTxCompleteToastsContent = ({ tx }: VegaTxToastContentProps) => {
if (isWithdrawTransaction(tx.body)) {
const completeWithdrawalButton = tx.withdrawal && (
<p className="mt-2">
<p className="mt-1">
<Button
data-testid="toast-complete-withdrawal"
size="xs"
@@ -211,7 +211,7 @@ const MultipleReadyToWithdrawToastContent = ({
count.toString()
)}
</p>
<p className="mt-2">
<p className="mt-1">
<Button
data-testid="toast-view-withdrawals"
size="xs"
@@ -243,7 +243,7 @@ const SingleReadyToWithdrawToastContent = ({
},
});
const completeButton = (
<p className="mt-2">
<p className="mt-1">
<Button
data-testid="toast-complete-withdrawal"
size="xs"
+7 -6
View File
@@ -11,6 +11,7 @@ import {
import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import {
Button,
TradingFormGroup,
TradingInput,
TradingInputError,
@@ -18,7 +19,6 @@ import {
TradingRichSelect,
ExternalLink,
Intent,
TradingButton,
} from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import BigNumber from 'bignumber.js';
@@ -183,7 +183,7 @@ export const WithdrawForm = ({
<>
<div className="mb-4 text-sm">
<p>{t('There are two steps required to make a withdrawal')}</p>
<ol className="pl-4 list-disc">
<ol className="list-disc pl-4">
<li>{t('Step 1 - Release funds from Vega')}</li>
<li>{t('Step 2 - Transfer funds to your Ethereum wallet')}</li>
</ol>
@@ -289,13 +289,14 @@ export const WithdrawForm = ({
</div>
)}
</TradingFormGroup>
<TradingButton
<Button
data-testid="submit-withdrawal"
type="submit"
variant="primary"
fill={true}
>
{t('Release funds')}
</TradingButton>
Release funds
</Button>
</form>
</>
);
@@ -308,7 +309,7 @@ const UseButton = (props: UseButtonProps) => {
<button
{...props}
type="button"
className="absolute top-0 right-0 ml-auto text-sm underline"
className="ml-auto text-sm absolute top-0 right-0 underline"
/>
);
};