feat(577): view new market proposals (#2078)

* feat: market proposal selector

* feat: market proposal selector

* feat: market proposal selector

* feat: market proposal selector

* feat: market proposal selector - fix linters

* feat: market proposal selector - add some int tests

* feat: market proposal selector - add some unit tests

* feat: market proposal selector - improve union type extracting

* feat: market proposal selector - fix failing on develop e2e tests

* feat: market proposal selector - fix failing on develop e2e tests

* feat: market proposal selector - fix failing on develop e2e tests

* feat: market proposal selector - fix failing on develop e2e tests

* feat: market proposal selector - fix failing on develop e2e tests
This commit is contained in:
macqbat 2022-11-16 15:36:03 +01:00 committed by GitHub
parent d1b45a65a0
commit 52e1757d33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1211 additions and 26 deletions

View File

@ -8,7 +8,7 @@ export const generateChainId = (
const defaultResult = {
statistics: {
__typename: 'Statistics',
chainId: 'test-chain-id',
chainId: Cypress.env('VEGA_ENV').toLowerCase() || 'test-chain-id',
},
};

View File

@ -8,7 +8,7 @@ export const generateStatistics = (
const defaultResult = {
statistics: {
__typename: 'Statistics',
chainId: 'test-chain-id',
chainId: Cypress.env('VEGA_ENV').toLowerCase() || 'test-chain-id',
blockHeight: '11',
},
};

View File

@ -2,13 +2,13 @@ import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { VoteProgress } from '@vegaprotocol/governance';
import { formatNumber } from '../../../../lib/format-number';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { useVoteInformation } from '../../hooks';
import { useUserVote } from './use-user-vote';
import { CurrentProposalStatus } from '../current-proposal-status';
import { VoteButtonsContainer } from './vote-buttons';
import { VoteProgress } from './vote-progress';
import { ProposalType } from '../proposal/proposal';
import type { Proposal_proposal } from '../../proposal/__generated__/Proposal';

View File

@ -60,6 +60,37 @@ describe('markets table', { tags: '@smoke' }, () => {
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('proposed markets tab should be rendered properly', () => {
cy.getByTestId('view-market-list-link')
.should('have.attr', 'href', '#/markets')
.click();
cy.get('[data-testid="Active markets"]').should(
'have.attr',
'data-state',
'active'
);
cy.get('[data-testid="Proposed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
cy.get('[data-testid="Proposed markets"]').click();
cy.get('[data-testid="Proposed markets"]').should(
'have.attr',
'data-state',
'active'
);
cy.getByTestId('tab-proposed-markets').should('be.visible');
cy.get('.ag-body-viewport .ag-center-cols-container .ag-row').should(
'have.length',
10
);
cy.getByTestId('external-link').should('have.length', 11);
cy.getByTestId('external-link')
.eq(10)
.should('have.text', 'Propose a new market');
});
});
function openMarketDropDown() {

View File

@ -34,6 +34,14 @@ export const generateNetworkParameters = (
}),
},
},
{
node: {
key: 'governance.proposal.market.requiredMajority',
value: '0.66',
__typename: 'NetworkParameter',
},
__typename: 'NetworkParameterEdge',
},
],
},
};

View File

@ -0,0 +1,625 @@
import type { ProposalsListQuery } from '@vegaprotocol/governance';
import { Schema } from '@vegaprotocol/types';
const marketProposals: ProposalsListQuery = {
proposalsConnection: {
edges: [
{
node: {
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_OPEN,
datetime: '2022-11-15T12:38:55.901696Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '20000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-15T12:44:34Z',
enactmentDatetime: '2022-11-15T12:44:54Z',
change: {
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '1cd1deb532b97fbeb9262fe94499ecec5835e60ae564b7c5af530c90a13c29cb',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_REJECTED,
datetime: '2022-11-15T12:38:08.810603Z',
votes: {
yes: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-15T12:39:41Z',
enactmentDatetime: '2022-11-15T12:39:51Z',
change: {
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'e503cadb437861037cddfd7263d25b69102098a97573db23f8e5fc320cea1ce9',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_PASSED,
datetime: '2022-11-14T16:18:57.437675Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '10000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-14T16:24:24Z',
enactmentDatetime: '2022-11-14T16:24:34Z',
change: {
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
symbol: 'mUSDT-II',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'e38415b453d862c246743fa979b877d97e2c9cbe160b6174e02c5589864817a0',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_REJECTED,
datetime: '2022-11-14T16:17:09.605382Z',
votes: {
yes: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:32:22Z',
enactmentDatetime: '2022-11-11T16:32:32Z',
change: {
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
symbol: 'mUSDT-II',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'e3119d341022a401cc68ba3a7ead5c431028d0060b3a49fc115025d7784c646f',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
datetime: '2022-11-14T09:35:54.040219Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '40000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-14T09:40:57Z',
enactmentDatetime: '2022-11-14T09:41:17Z',
change: {
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '07549b2cf93abbf6fb8ad976af3f24876e5946df85a3b6f0a6338672c7453bfa',
reference: 'injected_at_runtime',
state: Schema.ProposalState.STATE_ENACTED,
datetime: '2022-11-11T16:29:23.46877Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:32:22Z',
enactmentDatetime: '2022-11-11T16:32:32Z',
change: {
instrument: {
code: 'LINKUSD',
name: 'LINKUSD',
futureProduct: {
settlementAsset: {
id: 'eb30d55e90e1f9e5c4727d6fa2a5a8cd36ab9ae9738eb8f3faf53e2bee4861ee',
name: 'mUSDT-II',
symbol: 'mUSDT-II',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'de48ad0adc668a80a074e790d118b6a64fa4909fc60ebbc2c335be3b675fec93',
reference: 'zMSg0drbaFnoM1kF8YdsbH28poqdsJr33rYwyETv',
state: Schema.ProposalState.STATE_OPEN,
datetime: '2022-11-11T16:27:05.914266Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'ETHDAI.MF21',
name: 'ETHDAI Monthly (Dec 2022)',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'c80c1cc89dcc5a302e443b7fcd215cdadbb259f6b180db6253a5572ad6406253',
reference: 'NlotxzM5Jg3LfEN1vYCxxQ5wcmUwfWAqQQtad3Se',
state: Schema.ProposalState.STATE_PASSED,
datetime: '2022-11-11T16:27:05.914266Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '20000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'AAPL.MF21',
name: 'Apple Monthly (Dec 2022)',
futureProduct: {
settlementAsset: {
id: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
name: 'tUSDC TEST',
symbol: 'tUSDC',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'ad2e531441c2e8a43e85423db399a4acc8f9a8a2376304a4c377d0da8eb31e80',
reference: 'rVcSDJYfk3Ni6yi9ZwVcYXFaSA6miUZucHdzdGCv',
state: Schema.ProposalState.STATE_OPEN,
datetime: '2022-11-11T16:27:05.220442Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '35000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'BTCUSD.MF21',
name: 'BTCUSD Monthly (Dec 2022)',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'acff1b95980c3355e65c7162c35917388702525c0475ba753f03d0efc9c6d6a3',
reference: 'EMRU5vhlhKUFBx9D2sr4KWSS4PZhNhWEbRr8SMf7',
state: Schema.ProposalState.STATE_PASSED,
datetime: '2022-11-11T16:27:06.008336Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'TSLA.QM21',
name: 'Tesla Quarterly (Feb 2023)',
futureProduct: {
settlementAsset: {
id: '177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
name: 'tEURO TEST',
symbol: 'tEURO',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '90e71c52b2f40db78efc24abe4217382993868cd24e45b3dd17147be4afaf884',
reference: '1HpOzeaQhW3OgLlMS4uFIzrOJcK0zaGMN2ccWpF7',
state: Schema.ProposalState.STATE_OPEN,
datetime: '2022-11-11T16:27:05.861888Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '22000000000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'AAVEDAI.MF21',
name: 'AAVEDAI Monthly (Dec 2022)',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '8b4aaea9cf7cbbeee90aa6ccea21bd5b9ec15c1872d6b0b3a58e31b741dd948a',
reference: '9xlda7c2xrBTDVMNvsDdTSeVJqgCgvN3Uea7WPEN',
state: Schema.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
datetime: '2022-11-11T16:27:06.076146Z',
votes: {
yes: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '12345600000000000000000000',
totalNumber: '0',
totalWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'ETHBTC.QM21',
name: 'ETHBTC Quarterly (Feb 2023)',
futureProduct: {
settlementAsset: {
id: 'cee709223217281d7893b650850ae8ee8a18b7539b5658f9b4cc24de95dd18ad',
name: 'tBTC TEST',
symbol: 'tBTC',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '325dfa07e1be5192376616241d23b4d71740fe712e298130bfd35d27738f1ce4',
reference: 'WrmmmaQE4j70M0Wauh85oNpKOA1Lp8omIcn07DLS',
state: Schema.ProposalState.STATE_OPEN,
datetime: '2022-11-11T16:27:05.914266Z',
votes: {
yes: {
totalTokens: '10000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '50000000000000000000000000',
totalNumber: '1',
totalWeight: '1',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
terms: {
closingDatetime: '2022-11-11T16:28:25Z',
enactmentDatetime: '2022-11-11T16:30:35Z',
change: {
instrument: {
code: 'UNIDAI.MF21',
name: 'UNIDAI Monthly (Dec 2022)',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
__typename: 'Asset',
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
],
__typename: 'ProposalsConnection',
},
};
export const generateMarketProposals = () => {
return { ...marketProposals };
};

View File

@ -27,6 +27,7 @@ import {
generatePartyBalance,
generatePartyMarketData,
} from './mocks/generate-fees';
import { generateMarketProposals } from './mocks/generate-proposals';
const mockTradingPage = (
req: CyHttpMessages.IncomingHttpRequest,
@ -106,6 +107,7 @@ const mockTradingPage = (
aliasQuery(req, 'PartyBalance', generatePartyBalance());
aliasQuery(req, 'MarketPositions', generatePositions());
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
aliasQuery(req, 'ProposalsList', generateMarketProposals());
};
declare global {

View File

@ -1,3 +1,3 @@
import { Markets } from './markets';
import { MarketsPage } from './markets-page';
export default Markets;
export default MarketsPage;

View File

@ -0,0 +1,25 @@
import React, { useEffect } from 'react';
import { t, titlefy } from '@vegaprotocol/react-helpers';
import { Tabs, Tab } from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
export const MarketsPage = () => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<Tabs>
<Tab id="active-markets" name={t('Active markets')}>
<Markets />
</Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}>
<Proposed />
</Tab>
</Tabs>
);
};

View File

@ -1,25 +1,18 @@
import { useCallback } from 'react';
import { MarketsContainer } from '@vegaprotocol/market-list';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/react-helpers';
import { useGlobalStore } from '../../stores';
import { useNavigate } from 'react-router-dom';
export const Markets = () => {
const navigate = useNavigate();
const { update } = useGlobalStore((store) => ({ update: store.update }));
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy(['Markets']));
}, [updateTitle]);
return (
<MarketsContainer
onSelect={(marketId) => {
update({ marketId });
navigate(`/markets/${marketId}`);
}}
/>
const handleOnSelect = useCallback(
(marketId: string) => {
update({ marketId });
navigate(`/markets/${marketId}`);
},
[update, navigate]
);
return <MarketsContainer onSelect={handleOnSelect} />;
};

View File

@ -0,0 +1,23 @@
import { t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment';
import { ProposalsList } from '@vegaprotocol/governance';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
NEW_PROPOSAL_LINK,
TOKEN_DEFAULT_DOMAIN,
} from '../../components/constants';
export const Proposed = () => {
const { VEGA_TOKEN_URL } = useEnvironment();
const externalLink = `${
VEGA_TOKEN_URL || TOKEN_DEFAULT_DOMAIN
}${NEW_PROPOSAL_LINK}`;
return (
<>
<ProposalsList />
<ExternalLink className="py-4 px-[11px]" href={externalLink}>
{t('Propose a new market')}
</ExternalLink>
</>
);
};

View File

@ -1 +1,3 @@
export const DEBOUNCE_UPDATE_TIME = 500;
export const TOKEN_DEFAULT_DOMAIN = 'https://token.fairground.wtf';
export const NEW_PROPOSAL_LINK = '/governance/propose';

View File

@ -1,3 +1,6 @@
export * from './proposals-hooks';
export * from './proposals-queries';
export * from './voting-hooks';
export * from './proposals-data-provider';
export * from './proposals-list';
export * from './voting-progress';

View File

@ -0,0 +1,51 @@
fragment NewMarketFields on NewMarket {
instrument {
code
name
futureProduct {
settlementAsset {
id
name
symbol
}
}
}
}
fragment ProposalListFields on Proposal {
id
reference
state
datetime
votes {
yes {
totalTokens
totalNumber
totalWeight
}
no {
totalTokens
totalNumber
totalWeight
}
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
...NewMarketFields
}
}
}
}
query ProposalsList($proposalType: ProposalType, $inState: ProposalState) {
proposalsConnection(proposalType: $proposalType, inState: $inState) {
edges {
node {
...ProposalListFields
}
}
}
}

View File

@ -0,0 +1,101 @@
import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NewMarketFieldsFragment = { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } };
export type ProposalListFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } };
export type ProposalsListQueryVariables = Types.Exact<{
proposalType?: Types.InputMaybe<Types.ProposalType>;
inState?: Types.InputMaybe<Types.ProposalState>;
}>;
export type ProposalsListQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: string, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalWeight: string } }, terms: { __typename?: 'ProposalTerms', closingDatetime: string, enactmentDatetime?: string | null, change: { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', code: string, name: string, futureProduct?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', id: string, name: string, symbol: string } } | null } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateNetworkParameter' } } } } | null> | null } | null };
export const NewMarketFieldsFragmentDoc = gql`
fragment NewMarketFields on NewMarket {
instrument {
code
name
futureProduct {
settlementAsset {
id
name
symbol
}
}
}
}
`;
export const ProposalListFieldsFragmentDoc = gql`
fragment ProposalListFields on Proposal {
id
reference
state
datetime
votes {
yes {
totalTokens
totalNumber
totalWeight
}
no {
totalTokens
totalNumber
totalWeight
}
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
...NewMarketFields
}
}
}
}
${NewMarketFieldsFragmentDoc}`;
export const ProposalsListDocument = gql`
query ProposalsList($proposalType: ProposalType, $inState: ProposalState) {
proposalsConnection(proposalType: $proposalType, inState: $inState) {
edges {
node {
...ProposalListFields
}
}
}
}
${ProposalListFieldsFragmentDoc}`;
/**
* __useProposalsListQuery__
*
* To run a query within a React component, call `useProposalsListQuery` and pass it any options that fit your needs.
* When your component renders, `useProposalsListQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useProposalsListQuery({
* variables: {
* proposalType: // value for 'proposalType'
* inState: // value for 'inState'
* },
* });
*/
export function useProposalsListQuery(baseOptions?: Apollo.QueryHookOptions<ProposalsListQuery, ProposalsListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProposalsListQuery, ProposalsListQueryVariables>(ProposalsListDocument, options);
}
export function useProposalsListLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProposalsListQuery, ProposalsListQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProposalsListQuery, ProposalsListQueryVariables>(ProposalsListDocument, options);
}
export type ProposalsListQueryHookResult = ReturnType<typeof useProposalsListQuery>;
export type ProposalsListLazyQueryHookResult = ReturnType<typeof useProposalsListLazyQuery>;
export type ProposalsListQueryResult = Apollo.QueryResult<ProposalsListQuery, ProposalsListQueryVariables>;

View File

@ -0,0 +1,2 @@
export * from './proposals-data-provider';
export * from './__generated___/Proposals';

View File

@ -0,0 +1,18 @@
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import type {
ProposalsListQuery,
ProposalListFieldsFragment,
} from './__generated___/Proposals';
import { ProposalsListDocument } from './__generated___/Proposals';
const getData = (responseData: ProposalsListQuery) =>
responseData.proposalsConnection?.edges
?.filter((edge) => Boolean(edge?.node))
.map((edge) => edge?.node as ProposalListFieldsFragment) || null;
export const proposalsListDataProvider = makeDataProvider<
ProposalsListQuery,
ProposalListFieldsFragment[],
never,
never
>({ query: ProposalsListDocument, getData });

View File

@ -0,0 +1 @@
export * from './proposals-list';

View File

@ -0,0 +1,96 @@
import {
render,
screen,
act,
waitFor,
getAllByRole,
} from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { ProposalsList } from './proposals-list';
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
import { Schema as Types } from '@vegaprotocol/types';
const votesMock = {
yes: {
totalTokens: '5000',
},
no: {
totalTokens: '2000',
},
};
let marketsProposalMock: ProposalListFieldsFragment[] | null = [
{
id: 'id-1',
state: Types.ProposalState.STATE_OPEN,
votes: { ...votesMock },
},
{
id: 'id-2',
state: Types.ProposalState.STATE_PASSED,
votes: { ...votesMock },
},
{
id: 'id-3',
state: Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
votes: { ...votesMock },
},
] as ProposalListFieldsFragment[];
const useDataProvider = () => {
return {
data: marketsProposalMock,
loading: false,
error: false,
};
};
jest.mock('@vegaprotocol/react-helpers', () => ({
...jest.requireActual('@vegaprotocol/react-helpers'),
useDataProvider: jest.fn(() => useDataProvider()),
}));
describe('ProposalsList', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should be properly rendered', async () => {
await act(() => {
render(<ProposalsList />, { wrapper: MockedProvider });
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(3);
});
it('some of states should be filtered out', async () => {
marketsProposalMock = [
{
...(marketsProposalMock as ProposalListFieldsFragment[])[0],
state: Types.ProposalState.STATE_ENACTED,
},
...(marketsProposalMock as ProposalListFieldsFragment[]).slice(1),
];
await act(() => {
render(<ProposalsList />, { wrapper: MockedProvider });
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
});
it('empty response should causes no data message display', async () => {
marketsProposalMock = null;
await act(() => {
render(<ProposalsList />, { wrapper: MockedProvider });
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(screen.getByText('No Rows To Show')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,51 @@
import {
AgGridDynamic as AgGrid,
AsyncRenderer,
} from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { Schema as Types } from '@vegaprotocol/types';
import { proposalsListDataProvider } from '../proposals-data-provider';
import { useCallback, useMemo, useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useColumnDefs } from './use-column-defs';
import type { ProposalListFieldsFragment } from '../proposals-data-provider/__generated___/Proposals';
export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
data.filter((proposal) =>
[
Types.ProposalState.STATE_OPEN,
Types.ProposalState.STATE_PASSED,
Types.ProposalState.STATE_WAITING_FOR_NODE_VOTE,
].includes(proposal.state)
);
export const ProposalsList = () => {
const gridRef = useRef<AgGridReact | null>(null);
const handleOnGridReady = useCallback(() => {
gridRef.current?.api?.sizeColumnsToFit();
}, [gridRef]);
const variables = useMemo(() => {
return {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
};
}, []);
const { data, loading, error } = useDataProvider({
dataProvider: proposalsListDataProvider,
variables,
});
const filteredData = getNewMarketProposals(data || []);
const { columnDefs, defaultColDef } = useColumnDefs();
return (
<AsyncRenderer loading={loading} error={error} data={filteredData}>
<AgGrid
ref={gridRef}
domLayout="autoHeight"
className="min-w-full"
columnDefs={columnDefs}
rowData={filteredData}
defaultColDef={defaultColDef}
onGridReady={handleOnGridReady}
/>
</AsyncRenderer>
);
};

View File

@ -0,0 +1,152 @@
import { useMemo } from 'react';
import BigNumber from 'bignumber.js';
import type { ColDef } from 'ag-grid-community';
import { useEnvironment } from '@vegaprotocol/environment';
import {
t,
NetworkParams,
useNetworkParams,
getDateTimeFormat,
} from '@vegaprotocol/react-helpers';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ProposalStateMapping } from '@vegaprotocol/types';
import type {
ProposalListFieldsFragment,
NewMarketFieldsFragment,
} from '../proposals-data-provider/__generated___/Proposals';
import { VoteProgress } from '../voting-progress';
const instrumentGuard = (
change?: ProposalListFieldsFragment['terms']['change']
): change is NewMarketFieldsFragment => {
return change?.__typename === 'NewMarket';
};
export const useColumnDefs = () => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const cellCss = 'grid h-full items-center';
const columnDefs: ColDef[] = useMemo(() => {
return [
{
colId: 'market',
headerName: t('Market'),
field: 'terms.change.instrument.code',
width: 150,
cellStyle: { lineHeight: '14px' },
cellRenderer: ({
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { change } = data?.terms || {};
if (instrumentGuard(change) && VEGA_TOKEN_URL) {
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/governance/${data.id}`;
return (
<ExternalLink href={link}>
{change.instrument.code}
</ExternalLink>
);
}
return change.instrument.code;
}
return null;
},
},
{
colId: 'description',
headerName: t('Description'),
field: 'terms.change.instrument.name',
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'terms.change.instrument.futureProduct.settlementAsset.name',
},
{
colId: 'state',
headerName: t('State'),
field: 'state',
valueFormatter: ({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) =>
value ? ProposalStateMapping[value] : '-',
},
{
colId: 'voting',
headerName: t('Voting'),
cellClass: 'flex justify-between leading-tight font-mono',
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{
colId: 'closing-date',
headerName: t('Closing date'),
field: 'terms.closingDatetime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-'),
},
{
colId: 'enactment-date',
headerName: t('Enactment date'),
field: 'terms.enactmentDatetime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => (value ? getDateTimeFormat().format(new Date(value)) : '-'),
},
];
}, [VEGA_TOKEN_URL, requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => {
return {
sortable: false,
cellClass: cellCss,
};
}, []);
return useMemo(
() => ({
columnDefs,
defaultColDef,
}),
[columnDefs, defaultColDef]
);
};

View File

@ -0,0 +1 @@
export * from './voting-progress';

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import BigNumber from 'bignumber.js';
import { VoteProgress } from './vote-progress';
import { VoteProgress } from './voting-progress';
it('Renders with data-testid', () => {
render(

View File

@ -1,4 +1,4 @@
import type { BigNumber } from '../../../../lib/bignumber';
import type BigNumber from 'bignumber.js';
export const VoteProgress = ({
progress,
@ -14,7 +14,7 @@ export const VoteProgress = ({
>
<div
data-testid="vote-progress-indicator"
className="absolute -top-1 w-[1px] h-3 bg-white z-1"
className="absolute -top-1 w-[1px] h-3 bg-neutral-300 dark:bg-white z-1"
style={{ left: `${threshold}%` }}
/>
<div className="w-full h-2">
@ -26,7 +26,7 @@ export const VoteProgress = ({
}}
/>
<div
className="absolute left-0 bg-vega-red h-1"
className="absolute right-0 bg-vega-red h-1"
data-testid="vote-progress-bar-against"
style={{
width: `${100 - progress.toNumber()}%`,