Compare commits

..
Author SHA1 Message Date
Matthew Russell 0ffce2f8c9 chore: update tests to use single onboarding key 2023-09-14 10:42:10 -07:00
Matthew Russell 35e9affc83 fix: tests 2023-09-13 19:22:18 -07:00
Matthew Russell fe02d924d5 chore: re add pubkey message 2023-09-13 19:16:45 -07:00
Matthew Russell c531763eae chore: combine onboarding into single store 2023-09-13 19:12:48 -07:00
Matthew Russell cae5b45a9f fix: use correct eye off icon 2023-09-13 17:10:11 -07:00
Matthew Russell f257d7aa78 chore: fix react hook warnings 2023-09-13 16:44:06 -07:00
Matthew Russell 73cc101751 fix: make home page redirect also use top traded, fix page reload when click explore 2023-09-13 16:32:53 -07:00
Matthew Russell 34c19d9f84 fix: use proper element for a link 2023-09-13 15:42:45 -07:00
Matthew Russell 2a1adfa004 fix: dont navigate on dismiss of welcome dialog 2023-09-13 15:42:11 -07:00
Matthew Russell 4b9e63e1ee fix: only show telemetry toast after onboarding is complete 2023-09-13 15:37:09 -07:00
Matthew Russell c0bdf9e862 chore: separate telemetry from onboarding 2023-09-13 15:17:13 -07:00
m.rayandMatthew Russell af6719cc9d chore(trading): add governance link to top level nav (#4765)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-09-13 14:25:31 -07:00
Sam KeenandJoe c433a4ee06 feat(governance): more mobile view improvements (#4751)
Co-authored-by: Joe <joe@vega.xyz>
2023-09-13 19:01:33 +01:00
Matthew Russell e3a00ada05 chore(trading,governance,explorer): update pr template to ensure issues get linked 2023-09-13 08:30:06 -07:00
Joe Tsang aa38e0c001 test(governance): add assert for numerical sort (#4742) 2023-09-13 13:02:47 +01:00
Joe Tsang 86da521da5 test(governance): flaky test fixes (#4760) 2023-09-13 11:53:22 +01:00
Matthew Russell c6df34fe95 chore(trading): bump app version and enable snaps 2023-09-12 13:40:08 -07:00
Matthew Russell c7d0803164 chore(trading): make closed market table rows the correct height 2023-09-12 12:47:47 -07:00
m.ray 250a654544 feat(trading): margin required progressive disclosure (#4755) 2023-09-12 12:06:47 -07:00
Matthew Russell 7f5e8ebb15 chore(trading): replace successor cols with market code cell (#4722) 2023-09-12 11:52:03 -07:00
Art ccbe34e172 chore(trading): make deal ticket open on market change (#4749) 2023-09-12 11:26:54 -07:00
Maciek 27e8ce88bf chore(trading): adjust onboarding experience (#4754) 2023-09-12 11:10:17 -07:00
ArtandMatthew Russell 41804d9869 chore(trading): market list tweaks (#4756)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-09-12 10:41:45 -07:00
Maciek 293288286b chore(trading): adjust gap between candles (#4753) 2023-09-12 09:59:13 -07:00
Matthew Russell a77765b1e9 feat(trading): store study sizes, reduce candles gap (#4708) 2023-09-12 09:56:43 -07:00
89 changed files with 1452 additions and 1896 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Related issues 🔗
Issue: #[Issue number here]
Closes #[Issue number here]
# Description
+2 -1
View File
@@ -1,2 +1,3 @@
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
* @vegaprotocol/frontend
* @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core
@@ -19,7 +19,22 @@ export const upgradeProposalsData = {
},
{
node: {
upgradeBlockHeight: '1955065',
upgradeBlockHeight: '10001',
vegaReleaseTag: 'v0.71.0+dev-12156-bca1d57e',
approvers: [
'02a6531716b7a6d82779b7793c3ad2fcb47290ea2ff0912c56f61219bd9675ff',
'121934387281812a2d5e6913e5d57c0f85a8f169e2752347ee2e23b52d46623c',
'65c80e2f5f84e2109eec30810f137ba04cbbecaba8f27706c146cc6c6f90db29',
'bd6339d2428c79ac3bc9011771236d17bac92bcb1806423388d52fb440043aef',
],
status: 'PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED',
__typename: 'ProtocolUpgradeProposal',
},
__typename: 'ProtocolUpgradeProposalEdge',
},
{
node: {
upgradeBlockHeight: '20',
vegaReleaseTag: 'v0.71.0+dev-12156-bca1d57e',
approvers: [
'02a6531716b7a6d82779b7793c3ad2fcb47290ea2ff0912c56f61219bd9675ff',
@@ -262,10 +262,10 @@ context(
'2'
);
waitForBeginningOfEpoch();
cy.getByTestId(stakeValidatorListStakePercentage).should(
'have.text',
'50.02%'
);
cy.getByTestId(
stakeValidatorListStakePercentage,
epochTimeout
).should('have.text', '50.02%');
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
}
@@ -15,6 +15,7 @@ const proposalDocumentationLink = 'proposal-documentation-link';
const connectToVegaWalletButton = 'connect-to-vega-wallet-btn';
const governanceDocsUrl = 'https://vega.xyz/governance';
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
const proposalUpgradeBlockHeight = 'protocol-upgrade-proposal-block-height';
const closedProposals = 'closed-proposals';
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
const protocolUpgradeTime = 'protocol-upgrade-time';
@@ -184,7 +185,7 @@ context(
'have.text',
'Vega release tag: v1'
);
cy.getByTestId('protocol-upgrade-proposal-block-height').should(
cy.getByTestId(proposalUpgradeBlockHeight).should(
'have.text',
'Upgrade block height: 2015942'
);
@@ -199,7 +200,15 @@ context(
});
cy.getByTestId(closedProposalToggle).click();
cy.getByTestId(closedProposals).within(() => {
cy.getByTestId(networkUpgradeProposalListItem).should('have.length', 1);
cy.getByTestId(networkUpgradeProposalListItem).should('have.length', 2);
cy.getByTestId(networkUpgradeProposalListItem)
.first()
.within(() => {
cy.getByTestId(proposalUpgradeBlockHeight).should(
'contain.text',
'10001'
);
});
});
});
@@ -170,21 +170,20 @@ export function clickOnValidatorFromList(
validatorName = null
) {
cy.contains('Loading...', epochTimeout).should('not.exist');
waitForBeginningOfEpoch();
// below is to ensure validator list is shown
cy.get(stakeValidatorListName, { timeout: 10000 }).should('exist');
cy.get(stakeValidatorListPendingStake, txTimeout).should(
'not.contain',
'2,000,000,000,000,000,000.00' // number due to bug #936
);
waitForBeginningOfEpoch();
if (validatorName) {
cy.contains(validatorName).click();
} else {
cy.get(`[row-id="${validatorNumber}"]`)
.should('be.visible')
.first()
.as('validatorOnList');
cy.get('@validatorOnList').click();
.click();
}
}
@@ -196,7 +195,7 @@ export function validateValidatorListTotalStakeAndShare(
cy.contains('Loading...', epochTimeout).should('not.exist');
waitForBeginningOfEpoch();
cy.get(`[row-id="${positionOnList}"]`)
.eq(1)
.first()
.within(() => {
cy.getByTestId(stakeValidatorListTotalStake, epochTimeout).should(
'have.text',
+1 -1
View File
@@ -45,7 +45,7 @@ const HomeProposals = ({
const { t } = useTranslation();
return (
<section className="mb-16" data-testid="home-proposals">
<section className="mb-16 break-all" data-testid="home-proposals">
<Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
@@ -127,7 +127,7 @@ export const Proposal = ({
voteState={voteState}
/>
<div className="my-10">
<div className="my-10 break-all">
<ProposalChangeTable proposal={proposal} />
</div>
@@ -352,7 +352,6 @@ export const ConsensusValidatorsTable = ({
field: ValidatorFields.RANKING_INDEX,
headerName: '#',
width: 60,
pinned: 'left',
},
{
field: ValidatorFields.VALIDATOR,
@@ -362,7 +361,6 @@ export const ConsensusValidatorsTable = ({
if (a === b) return 0;
return a > b ? 1 : -1;
},
pinned: 'left',
width: 260,
},
{
@@ -195,14 +195,12 @@ export const StandbyPendingValidatorsTable = ({
field: ValidatorFields.RANKING_INDEX,
headerName: '#',
width: 60,
pinned: 'left',
},
{
field: ValidatorFields.VALIDATOR,
headerName: t(ValidatorFields.VALIDATOR).toString(),
cellRenderer: ValidatorRenderer,
comparator: ({ name: a }, { name: b }) => Math.sign(a - b),
pinned: 'left',
width: 260,
},
{
@@ -122,38 +122,43 @@ export const ValidatorTable = ({
<div className="my-12" data-testid="validator-table">
<SubHeading title={t('profile')} />
<RoundedWrapper paddingBottom={true}>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<div className="break-all">
<RoundedWrapper paddingBottom={true}>
<KeyValueTable data-testid="validator-table-profile">
<KeyValueTableRow>
<span>{t('id')}</span>
<ValidatorTableCell dataTestId="validator-id">
{node.id}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('ABOUT THIS VALIDATOR')}</span>
<Tooltip description={t('AboutThisValidatorDescription')}>
<a data-testid="validator-description-url" href={node.infoUrl}>
{node.infoUrl}
</a>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
<Tooltip description={t('ValidatorStatusDescription')}>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
<Tooltip description={t('AboutThisValidatorDescription')}>
<a
data-testid="validator-description-url"
href={node.infoUrl}
>
{node.infoUrl}
</a>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('STATUS')}</strong>
</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<Tooltip description={t('ValidatorStatusDescription')}>
<span data-testid="validator-status">
<strong>
{t(statusTranslationKey(node.rankingScore.status))}
</strong>
</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<div className="mb-10">
{t('validatorTableIntro')}{' '}
@@ -167,144 +172,154 @@ export const ValidatorTable = ({
</div>
<SubHeading title={t('ADDRESS')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell dataTestId="validator-server-location">
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span data-testid="validator-eth-address">
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<div className="break-all">
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-address">
<KeyValueTableRow>
<span>{t('VEGA ADDRESS / PUBLIC KEY')}</span>
<ValidatorTableCell dataTestId="validator-public-key">
{node.pubkey}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('SERVER LOCATION')}</span>
<ValidatorTableCell dataTestId="validator-server-location">
{countryData.find((c) => c.code === node.location)?.name ||
t('not available')}
</ValidatorTableCell>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('ETHEREUM ADDRESS')}</span>
<span data-testid="validator-eth-address">
<Link
title={t('View on Etherscan (opens in a new tab)')}
href={`${ETHERSCAN_URL}/address/${node.ethereumAddress}`}
target="_blank"
>
{node.ethereumAddress}
</Link>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<SubHeading title={t('STAKE')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<div className="break-all">
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-stake">
<KeyValueTableRow>
<span>{t('STAKED BY OPERATOR')}</span>
<Tooltip description={t('StakedByOperatorDescription')}>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
<Tooltip description={t('StakedByOperatorDescription')}>
<span data-testid="staked-by-operator">
{formatNumber(toBigNum(node.stakedByOperator, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<Tooltip description={t('StakedByDelegatesDescription')}>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('STAKED BY DELEGATES')}</span>
<Tooltip description={t('StakedByDelegatesDescription')}>
<span data-testid="staked-by-delegates">
{formatNumber(toBigNum(node.stakedByDelegates, decimals))}
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>
<strong>{t('TOTAL STAKE')}</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<span data-testid="total-stake">
<strong>
{formatNumber(toBigNum(node.stakedTotal, decimals))}
</strong>
</span>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PENDING STAKE')}</span>
<Tooltip description={t('PendingStakeDescription')}>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<Tooltip description={t('PendingStakeDescription')}>
<span data-testid="pending-stake">
{formatNumber(toBigNum(node.pendingStake, decimals))}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>{t('STAKE SHARE')}</span>
<Tooltip description={t('StakeShareDescription')}>
<span data-testid="stake-percentage">{stakePercentage}</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<Tooltip description={t('StakeShareDescription')}>
<span data-testid="stake-percentage">{stakePercentage}</span>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<SubHeading title={t('PENALTIES')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<div className="break-all">
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-penalties">
<KeyValueTableRow>
<span>{t('OVERSTAKED PENALTY')}</span>
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{formatNumberPercentage(penalties.overstaked, 2)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<Tooltip description={t('OverstakedPenaltyDescription')}>
<span data-testid="overstaking-penalty">
{formatNumberPercentage(penalties.overstaked, 2)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow>
<span>{t('PERFORMANCE PENALTY')}</span>
<Tooltip description={t('PerformancePenaltyDescription')}>
<span data-testid="performance-penalty">
{formatNumberPercentage(penalties.performance, 2)}
<Tooltip description={t('PerformancePenaltyDescription')}>
<span data-testid="performance-penalty">
{formatNumberPercentage(penalties.performance, 2)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('TOTAL PENALTIES')}</strong>
</span>
<span data-testid="total-penalties">
<strong>{formatNumberPercentage(penalties.overall, 2)}</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<span data-testid="total-penalties">
<strong>
{formatNumberPercentage(penalties.overall, 2)}
</strong>
</span>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
<SubHeading title={t('VOTING POWER')} />
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<div className="break-all">
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable data-testid="validator-table-voting-power">
<KeyValueTableRow>
<span>{t('UNNORMALISED VOTING POWER')}</span>
<Tooltip description={t('UnnormalisedVotingPowerDescription')}>
<span data-testid="unnormalised-voting-power">
{getUnnormalisedVotingPower(rawValidatorScore)}
<Tooltip description={t('UnnormalisedVotingPowerDescription')}>
<span data-testid="unnormalised-voting-power">
{getUnnormalisedVotingPower(rawValidatorScore)}
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
</Tooltip>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<span>
<strong>{t('NORMALISED VOTING POWER')}</strong>
</span>
<Tooltip description={t('NormalisedVotingPowerDescription')}>
<strong data-testid="normalised-voting-power">
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
<Tooltip description={t('NormalisedVotingPowerDescription')}>
<strong data-testid="normalised-voting-power">
{getNormalisedVotingPower(node.rankingScore.votingPower)}
</strong>
</Tooltip>
</KeyValueTableRow>
</KeyValueTable>
</RoundedWrapper>
</div>
</div>
</>
);
@@ -54,7 +54,7 @@ export const TokenDetails = ({
config.token_vesting_contract?.address || ENV.addresses.tokenVestingAddress;
return (
<div className="token-details">
<div className="token-details break-all">
<RoundedWrapper>
<KeyValueTable>
<KeyValueTableRow>
+12 -11
View File
@@ -1,27 +1,28 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
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
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+2 -2
View File
@@ -16,14 +16,14 @@ 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.21.1-core-0.72.14
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1 -1
View File
@@ -22,7 +22,7 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+10 -15
View File
@@ -1,20 +1,16 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { marketsWithDataProvider } from '@vegaprotocol/markets';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
// The home pages only purpose is to redirect to the users last market,
// the top traded if they are new, or fall back to the list of markets.
// Thats why we just render a loader here
export const Home = () => {
const navigate = useNavigate();
// The default market selected in the platform behind the overlay
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
const { data, error, loading } = useDataProvider({
dataProvider: marketsWithDataProvider,
variables: undefined,
});
const update = useGlobalStore((store) => store.update);
const { data } = useTopTradedMarkets();
const marketId = useGlobalStore((store) => store.marketId);
useEffect(() => {
@@ -32,12 +28,11 @@ export const Home = () => {
navigate(Links[Routes.MARKETS]());
}
}
}, [marketId, data, navigate, update]);
}, [marketId, data, navigate]);
return (
<AsyncRenderer data={data} loading={loading} error={error}>
{/* Render a loading and error state but we will redirect if markets are found */}
{null}
</AsyncRenderer>
<Splash>
<Loader />
</Splash>
);
};
+7 -7
View File
@@ -69,15 +69,15 @@ export const MarketPage = () => {
useEffect(() => {
if (data?.id && data.id !== lastMarketId) {
update({ marketId: data.id });
}
}, [update, lastMarketId, data?.id]);
// Make sidebar open on deal ticket by default
useEffect(() => {
if (init && view === null) {
// make sidebar open on market id change
setView({ type: ViewType.Order });
}
}, [init, view, setView]);
// make sidebar open on deal ticket by default
if (view === null) {
setView({ type: ViewType.Order });
}
}, [update, lastMarketId, data?.id, setView, init, view]);
const tradeView = useMemo(() => {
if (largeScreen) {
+21 -208
View File
@@ -1,4 +1,4 @@
import { act, render, screen, within, waitFor } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { Closed } from './closed';
import { MarketStateMapping, PropertyKeyType } from '@vegaprotocol/types';
@@ -10,55 +10,21 @@ import type {
OracleSpecDataConnectionQuery,
MarketsDataQuery,
MarketsQuery,
SuccessorMarketIdsQuery,
} from '@vegaprotocol/markets';
import {
OracleSpecDataConnectionDocument,
MarketsDataDocument,
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { FLAGS } from '@vegaprotocol/environment';
import {
createMarketFragment,
marketsQuery,
marketsDataQuery,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import type { FeatureFlags } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/markets', () => ({
...jest.requireActual('@vegaprotocol/markets'),
useSuccessorMarket: (marketId: string) =>
marketId === 'include-0'
? {
data: {
id: 'successorMarketID',
state: 'STATE_ACTIVE',
tradableInstrument: {
instrument: {
name: 'Successor Market Name',
code: 'SuccessorCode',
},
},
},
}
: { data: undefined },
}));
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
} as FeatureFlags,
};
});
describe('Closed', () => {
let originalNow: typeof Date.now;
@@ -218,10 +184,8 @@ describe('Closed', () => {
const headers = screen.getAllByRole('columnheader');
const expectedHeaders = [
'Market',
'Description',
'Status',
'Settlement date',
'Successor market',
'Best bid',
'Best offer',
'Mark price',
@@ -235,10 +199,8 @@ describe('Closed', () => {
const cells = screen.getAllByRole('gridcell');
const expectedValues = [
market.tradableInstrument.instrument.code,
market.tradableInstrument.instrument.name,
MarketStateMapping[market.state],
'3 days ago',
'-',
/* eslint-disable @typescript-eslint/no-non-null-assertion */
addDecimalsFormatNumber(marketsData.bestBidPrice, market.decimalPlaces),
addDecimalsFormatNumber(
@@ -340,43 +302,26 @@ describe('Closed', () => {
.getAllByRole('gridcell')
.filter((cell) => cell.getAttribute('col-id') === 'code')
.map((cell) => {
const marketId = within(cell)
.getByTestId('market-code')
.getAttribute('data-market-id');
return marketId;
const marketCode = within(cell).getByTestId('stack-cell-primary');
return marketCode.textContent;
});
expect(cells).toEqual(expectedRows.map((m) => m.node.id));
expect(cells).toEqual(
expectedRows.map((m) => m.node.tradableInstrument.instrument.code)
);
});
it('successor marked should be visible', async () => {
const mixedMarkets = [
const marketsWithSuccessorID = [
{
__typename: 'MarketEdge' as const,
node: createMarketFragment({
id: 'include-0',
state: MarketState.STATE_SETTLED,
successorMarketID: 'successor',
}),
},
{
__typename: 'MarketEdge' as const,
node: {
...createMarketFragment({
id: 'successorMarketID',
state: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
...createMarketFragment().tradableInstrument,
instrument: {
...createMarketFragment().tradableInstrument.instrument,
id: 'successorAssset',
name: 'Successor Market Name',
code: 'SuccessorCode',
},
},
},
},
];
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
const mockWithSuccessors: MockedResponse<MarketsQuery> = {
request: {
query: MarketsDocument,
},
@@ -384,42 +329,17 @@ describe('Closed', () => {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: mixedMarkets,
edges: marketsWithSuccessorID,
},
},
},
};
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
node: {
id: 'include-0',
successorMarketID: 'successorMarketID',
parentMarketID: '',
},
},
],
},
},
},
};
await act(() => {
await act(async () => {
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
mocks={[mockWithSuccessors, marketsDataMock, oracleDataMock]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
@@ -431,122 +351,15 @@ describe('Closed', () => {
);
});
await waitFor(() => {
expect(
screen.getByRole('button', { name: /^SuccessorCode/ })
).toBeInTheDocument();
});
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
).toBeInTheDocument();
screen
.getAllByRole('gridcell', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarket',
})
.forEach((element) => {
expect(element.querySelector('[title="Future"]')?.textContent).toEqual(
'Futr'
);
});
});
const container = within(
document.querySelector('.ag-center-cols-container') as HTMLElement
);
const cell = container.getAllByRole('gridcell', {
name: (_name, element) => element.getAttribute('col-id') === 'code',
})[0];
it('feature flag should hide successors', async () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = false;
const mixedMarkets = [
{
__typename: 'MarketEdge' as const,
node: createMarketFragment({
id: 'include-0',
state: MarketState.STATE_SETTLED,
}),
},
{
__typename: 'MarketEdge' as const,
node: {
...createMarketFragment({
id: 'successorMarketID',
state: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
...createMarketFragment().tradableInstrument,
instrument: {
...createMarketFragment().tradableInstrument.instrument,
id: 'successorAssset',
name: 'Successor Market Name',
code: 'SuccessorCode',
},
},
},
},
];
const mixedMarketsMock: MockedResponse<MarketsQuery> = {
request: {
query: MarketsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: mixedMarkets,
},
},
},
};
const successorMarketsMock: MockedResponse<SuccessorMarketIdsQuery> = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
__typename: 'MarketConnection',
edges: [
{
node: {
id: 'include-0',
successorMarketID: 'successorMarketID',
parentMarketID: '',
},
},
],
},
},
},
};
render(
<MemoryRouter>
<MockedProvider
mocks={[
mixedMarketsMock,
marketsDataMock,
oracleDataMock,
successorMarketsMock,
]}
>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Closed />
</VegaWalletContext.Provider>
</MockedProvider>
</MemoryRouter>
expect(within(cell).getByTestId('stack-cell-secondary')).toHaveTextContent(
'PRNT'
);
await waitFor(() => {
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'settlementDate',
})
).toBeInTheDocument();
});
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('successorMarket');
});
});
});
+24 -34
View File
@@ -4,13 +4,10 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
COL_DEFS,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import type { ProductType } from '@vegaprotocol/types';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
import {
addDecimalsFormatNumber,
@@ -20,17 +17,13 @@ import type {
DataSourceFilterFragment,
MarketMaybeWithData,
} from '@vegaprotocol/markets';
import {
MarketActionsDropdown,
closedMarketsWithDataProvider,
} from '@vegaprotocol/markets';
import { closedMarketsWithDataProvider } from '@vegaprotocol/markets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { ColDef } from 'ag-grid-community';
import { FLAGS } from '@vegaprotocol/environment';
import { SettlementDateCell } from './settlement-date-cell';
import { SettlementPriceCell } from './settlement-price-cell';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { SuccessorMarketRenderer } from './successor-market-cell';
import { MarketActionsDropdown } from './market-table-actions';
import { MarketCodeCell } from './market-code-cell';
type SettlementAsset =
MarketMaybeWithData['tradableInstrument']['instrument']['product']['settlementAsset'];
@@ -51,7 +44,9 @@ interface Row {
setlementDataSourceFilter: DataSourceFilterFragment | undefined;
tradingTerminationOracleId: string;
settlementAsset: SettlementAsset;
productType: string;
productType: ProductType | undefined;
successorMarketID: string | null | undefined;
parentMarketID: string | null | undefined;
}
export const Closed = () => {
@@ -95,16 +90,19 @@ export const Closed = () => {
tradingTerminationOracleId:
instrument.product.dataSourceSpecForTradingTermination.id,
settlementAsset: instrument.product.settlementAsset,
productType: instrument.product.__typename || '',
productType: instrument.product.__typename,
successorMarketID: market.successorMarketID,
parentMarketID: market.parentMarketID,
};
return row;
});
return (
<div className="h-full relative">
<ClosedMarketsDataGrid rowData={rowData} error={error} />
</div>
);
return <ClosedMarketsDataGrid rowData={rowData} error={error} />;
};
const components = {
MarketCodeCell,
};
const ClosedMarketsDataGrid = ({
@@ -117,15 +115,11 @@ const ClosedMarketsDataGrid = ({
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
const colDefs = useMemo(() => {
const cols: ColDef[] = compact([
return [
{
headerName: t('Market'),
field: 'code',
cellRenderer: 'MarketNameCell',
},
{
headerName: t('Description'),
field: 'name',
cellRenderer: 'MarketCodeCell',
},
{
headerName: t('Status'),
@@ -176,12 +170,6 @@ const ClosedMarketsDataGrid = ({
},
},
},
FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Successor market'),
field: 'id',
colId: 'successorMarket',
cellRenderer: 'SuccessorMarketRenderer',
},
{
headerName: t('Best bid'),
field: 'bestBidPrice',
@@ -263,12 +251,13 @@ const ClosedMarketsDataGrid = ({
<MarketActionsDropdown
marketId={data.id}
assetId={data.settlementAsset.id}
successorMarketID={data.successorMarketID}
parentMarketID={data.parentMarketID}
/>
);
},
},
]);
return cols;
];
}, [openAssetDialog]);
return (
@@ -276,8 +265,9 @@ const ClosedMarketsDataGrid = ({
rowData={rowData}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
components={{ SuccessorMarketRenderer, MarketNameCell }}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
components={components}
rowHeight={45}
/>
);
};
@@ -0,0 +1,82 @@
import { render, screen } from '@testing-library/react';
import { ProductTypeShortName } from '@vegaprotocol/types';
import type { MarketCodeCellProps } from './market-code-cell';
import { MarketCodeCell } from './market-code-cell';
describe('MarketCodeCell', () => {
const renderComponent = (props: MarketCodeCellProps) => {
return render(<MarketCodeCell {...props} />);
};
it('renders SCCR if the market is a successor', () => {
const productType = 'Future';
const code = 'code';
const props = {
value: 'code',
data: {
productType,
parentMarketID: 'foo',
successorMarketID: undefined,
},
} as const;
renderComponent(props);
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
ProductTypeShortName[productType]
);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
'SCCR'
);
expect(screen.getByTestId('stack-cell-secondary')).not.toHaveTextContent(
'PRNT'
);
});
it('renders PRNT if the market is a parent', () => {
const productType = 'Future';
const code = 'code';
const props = {
value: 'code',
data: {
productType,
parentMarketID: undefined,
successorMarketID: 'foo',
},
} as const;
renderComponent(props);
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
ProductTypeShortName[productType]
);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
'PRNT'
);
expect(screen.getByTestId('stack-cell-secondary')).not.toHaveTextContent(
'SCCR'
);
});
it('renders both SCCR and PRNT if the market is both a parent and a successor', () => {
const productType = 'Future';
const code = 'code';
const props = {
value: 'code',
data: {
productType,
parentMarketID: 'foo',
successorMarketID: 'bar',
},
} as const;
renderComponent(props);
expect(screen.getByTestId('stack-cell-primary')).toHaveTextContent(code);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
ProductTypeShortName[productType]
);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
'PRNT'
);
expect(screen.getByTestId('stack-cell-secondary')).toHaveTextContent(
'SCCR'
);
});
});
@@ -0,0 +1,51 @@
import compact from 'lodash/compact';
import type { ProductType } from '@vegaprotocol/types';
import { ProductTypeMapping, ProductTypeShortName } from '@vegaprotocol/types';
import { StackedCell } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
export interface MarketCodeCellProps {
value: string | undefined; // market code
data: {
productType: ProductType | undefined;
parentMarketID: string | null | undefined;
successorMarketID: string | null | undefined;
};
}
export const MarketCodeCell = ({ value, data }: MarketCodeCellProps) => {
if (!value || !data || !data.productType) return null;
const infoSpanClasses =
'mr-1 pr-1 uppercase border-r last:pr-0 last:mr-0 last:border-r-0 border-vega-clight-200 dark:border-vega-cdark-200';
const info = compact([
<span
className={infoSpanClasses}
key="productType"
title={ProductTypeMapping[data.productType]}
>
{ProductTypeShortName[data.productType]}
</span>,
data.parentMarketID && (
<span
className={infoSpanClasses}
key="successor"
title={t('Successor of a market')}
>
{t('SCCR')}
</span>
),
data.successorMarketID && (
<span
className={infoSpanClasses}
key="parent"
title={t('Parent of a market')}
>
{t('PRNT')}
</span>
),
]);
return <StackedCell primary={value} secondary={info} />;
};
@@ -0,0 +1,35 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, PriceFlashCell } from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
const components = {
PriceFlashCell,
};
type Props = TypedDataAgGrid<MarketMaybeWithData>;
export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs();
return (
<AgGrid
getRowId={getRowId}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
components={components}
rowHeight={45}
{...props}
/>
);
};
export default MarketListTable;
@@ -9,14 +9,21 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { DApp, EXPLORER_MARKET, useLinks } from '@vegaprotocol/environment';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
export const MarketActionsDropdown = ({
marketId,
assetId,
successorMarketID,
parentMarketID,
}: {
marketId: string;
assetId: string;
successorMarketID: string | null | undefined;
parentMarketID: string | null | undefined;
}) => {
const navigate = useNavigate();
const open = useAssetDetailsDialogStore((store) => store.open);
const linkCreator = useLinks(DApp.Explorer);
@@ -42,6 +49,26 @@ export const MarketActionsDropdown = ({
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View settlement asset details')}
</TradingDropdownItem>
{parentMarketID && (
<TradingDropdownItem
onClick={() => {
navigate(Links[Routes.MARKET](parentMarketID));
}}
>
<VegaIcon name={VegaIconNames.EYE} size={16} />
{t('View parent market')}
</TradingDropdownItem>
)}
{successorMarketID && (
<TradingDropdownItem
onClick={() => {
navigate(Links[Routes.MARKET](successorMarketID));
}}
>
<VegaIcon name={VegaIconNames.EYE} size={16} />
{t('View successor market')}
</TradingDropdownItem>
)}
</ActionsDropdown>
);
};
@@ -6,7 +6,7 @@ import {
Tab,
TradingAnchorButton,
} from '@vegaprotocol/ui-toolkit';
import { Markets } from './markets';
import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
@@ -33,7 +33,7 @@ export const MarketsPage = () => {
<div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets">
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
<OpenMarkets />
</Tab>
<Tab
id="proposed-markets"
@@ -1,13 +0,0 @@
import { MarketsContainer } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { SuccessorMarketRenderer } from './successor-market-cell';
export const Markets = () => {
const handleOnSelect = useMarketClickHandler();
return (
<MarketsContainer
onSelect={handleOnSelect}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
);
};
@@ -0,0 +1,63 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { marketListProvider } from '@vegaprotocol/markets';
import { useEffect } from 'react';
import { t } from '@vegaprotocol/i18n';
import type { CellClickedEvent } from 'ag-grid-community';
import MarketListTable from './market-list-table';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { Interval } from '@vegaprotocol/types';
import { useYesterday } from '@vegaprotocol/react-helpers';
const POLLING_TIME = 2000;
export const OpenMarkets = () => {
const handleOnSelect = useMarketClickHandler();
const yesterday = useYesterday();
const { data, error, reload } = useDataProvider({
dataProvider: marketListProvider,
variables: {
since: new Date(yesterday).toISOString(),
interval: Interval.INTERVAL_I1H,
},
});
useEffect(() => {
const interval = setInterval(() => {
reload();
}, POLLING_TIME);
return () => {
clearInterval(interval);
};
}, [reload]);
return (
<MarketListTable
rowData={data}
onCellClicked={({
data,
column,
event,
}: CellClickedEvent<MarketMaybeWithData>) => {
if (!data) return;
// prevent navigating to the market page if any of the below cells are clicked
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
const colId = column.getColId();
if (
[
'tradableInstrument.instrument.product.settlementAsset.symbol',
'market-actions',
].includes(colId)
) {
return;
}
// @ts-ignore metaKey exists
handleOnSelect(data.id, event ? event.metaKey : false);
}}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
/>
);
};
@@ -0,0 +1,46 @@
import { useEnvironment } from '@vegaprotocol/environment';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import type { Market } from '@vegaprotocol/markets';
import {
getMatchingOracleProvider,
getVerifiedStatusIcon,
useOracleProofs,
} from '@vegaprotocol/markets';
export const OracleStatus = ({
dataSourceSpecForSettlementData,
dataSourceSpecForTradingTermination,
}: Pick<
Market['tradableInstrument']['instrument']['product'],
'dataSourceSpecForSettlementData' | 'dataSourceSpecForTradingTermination'
>) => {
const { ORACLE_PROOFS_URL } = useEnvironment();
const { data: providers } = useOracleProofs(ORACLE_PROOFS_URL);
if (providers) {
const settlementDataProvider = getMatchingOracleProvider(
dataSourceSpecForSettlementData.data,
providers
);
const tradingTerminationDataProvider = getMatchingOracleProvider(
dataSourceSpecForTradingTermination.data,
providers
);
let maliciousOracleProvider = null;
if (settlementDataProvider?.oracle.status !== 'GOOD') {
maliciousOracleProvider = settlementDataProvider;
} else if (tradingTerminationDataProvider?.oracle.status !== 'GOOD') {
maliciousOracleProvider = tradingTerminationDataProvider;
}
if (!maliciousOracleProvider) return null;
const { icon } = getVerifiedStatusIcon(maliciousOracleProvider);
return <Icon size={3} name={icon as IconName} className="ml-1" />;
}
return null;
};
@@ -0,0 +1,15 @@
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const ParentMarketCell = ({
value,
}: {
value: string; // parentMarketId
}) => {
const { data, loading } = useMarketsMapProvider();
if (loading) return null;
if (!data || !data[value]) return <span>-</span>;
return <div>{data[value].tradableInstrument.instrument.code}</div>;
};
@@ -1,6 +1,10 @@
import { ProposalsList } from '@vegaprotocol/proposals';
import { SuccessorMarketRenderer } from './successor-market-cell';
import { ParentMarketCell } from './parent-market-cell';
const cellRenderers = {
ParentMarketCell,
};
export const Proposed = () => {
return <ProposalsList SuccessorMarketRenderer={SuccessorMarketRenderer} />;
return <ProposalsList cellRenderers={cellRenderers} />;
};
@@ -1,190 +0,0 @@
import { render, screen, act, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import userEvent from '@testing-library/user-event';
import type { Market } from '@vegaprotocol/markets';
import { SuccessorMarketRenderer } from './successor-market-cell';
import {
MarketsDocument,
SuccessorMarketIdsDocument,
} from '@vegaprotocol/markets';
import { createMarketFragment } from '@vegaprotocol/mock';
const mockSuccessorsQuery = [
{
id: 'market1',
parentMarketID: 'parentMarket1',
successorMarketID: 'successorMarket1',
},
{ id: 'market2', parentMarketID: 'parentMarket2' },
{ id: 'market3', successorMarketID: 'successorMarket3' },
];
const parentMarket1 = {
id: 'parentMarket1',
tradableInstrument: {
instrument: { code: 'code parent 1', id: '1' },
},
} as unknown as Market;
const successorMarket1 = {
id: 'successorMarket1',
tradableInstrument: {
instrument: { code: 'code successor 1', id: '2' },
},
} as unknown as Market;
const parentMarket2 = {
id: 'parentMarket2',
tradableInstrument: {
instrument: { code: 'code parent 2', id: '3' },
},
} as unknown as Market;
const successorMarket3 = {
id: 'successorMarket3',
tradableInstrument: {
instrument: { code: 'code successor 3', id: '4' },
},
} as unknown as Market;
const mockMarkets = [
parentMarket1,
successorMarket1,
parentMarket2,
successorMarket3,
];
const mockClickHandler = jest.fn();
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
useMarketClickHandler: jest.fn().mockImplementation(() => mockClickHandler),
}));
const marketMock = {
request: {
query: MarketsDocument,
variables: undefined,
},
result: {
data: {
marketsConnection: {
edges: mockMarkets.map((item) => ({
node: {
...createMarketFragment(item),
},
})),
},
},
},
};
const successorMock = {
request: {
query: SuccessorMarketIdsDocument,
},
result: {
data: {
marketsConnection: {
edges: mockSuccessorsQuery.map((item) => ({
node: {
...item,
},
})),
},
},
},
};
const mocks = [marketMock, successorMock];
describe('SuccessorMarketRenderer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should properly rendered successor market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket1', false);
});
});
it('should properly rendered parent market', async () => {
const successorValue = 'market1';
render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 1')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket1', false);
});
});
it('should properly rendered only parent market', async () => {
const successorValue = 'market2';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code parent 2')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('parentMarket2', false);
});
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
expect(screen.getByText('-')).toBeInTheDocument();
});
it('should properly rendered only successor market', async () => {
const successorValue = 'market3';
const { rerender } = render(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} />
</MockedProvider>
);
await waitFor(() => {
expect(screen.getByTestId('market-code')).toBeInTheDocument();
});
expect(screen.getByText('code successor 3')).toBeInTheDocument();
expect(screen.getByText('Futr')).toBeInTheDocument();
await userEvent.click(screen.getByRole('button'));
await waitFor(() => {
expect(mockClickHandler).toHaveBeenCalledWith('successorMarket3', false);
});
await act(() => {
rerender(
<MockedProvider mocks={[...mocks]}>
<SuccessorMarketRenderer value={successorValue} parent />
</MockedProvider>
);
});
expect(screen.getByText('-')).toBeInTheDocument();
});
});
@@ -1,41 +0,0 @@
import React from 'react';
import { MarketNameCell } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketProvider, useSuccessorMarketIds } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const SuccessorMarketRenderer = ({
value,
parent,
}: {
value: string;
parent?: boolean;
}) => {
const successors = useSuccessorMarketIds(value);
const onMarketClick = useMarketClickHandler();
const lookupValue = successors
? parent
? successors.parentMarketID
: successors.successorMarketID
: '';
const { data } = useDataProvider({
dataProvider: marketProvider,
variables: {
marketId: lookupValue || '',
},
skip: !lookupValue,
});
return data ? (
<MarketNameCell
value={data.tradableInstrument.instrument.code}
data={data}
onMarketClick={onMarketClick}
productType={data.tradableInstrument.instrument?.product.__typename}
/>
) : (
'-'
);
};
@@ -0,0 +1,226 @@
import { useMemo } from 'react';
import type { ColDef, ValueFormatterParams } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type {
MarketMaybeWithData,
MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions';
import { calcCandleVolume } from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell';
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = () => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
() => [
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
flex: 2,
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.code'
>) => (
<MarketCodeCell
value={value}
data={{
productType:
data?.tradableInstrument.instrument.product.__typename,
successorMarketID: data?.successorMarketID,
parentMarketID: data?.parentMarketID,
}}
/>
),
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
flex: 2,
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return '-';
const { trigger, marketTradingMode } = data.data;
const withTriggerInfo =
marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED;
if (withTriggerInfo) {
return (
<Tooltip
description={`${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`}
>
<span>
{Schema.MarketTradingModeMapping[marketTradingMode]}
</span>
</Tooltip>
);
}
return Schema.MarketTradingModeMapping[marketTradingMode];
},
filter: SetFilter,
filterParams: {
set: Schema.MarketTradingModeMapping,
},
},
{
headerName: t('Status'),
field: 'state',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
},
filter: SetFilter,
filterParams: {
set: Schema.MarketStateMapping,
},
},
{
headerName: t('Mark price'),
field: 'data.markPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(data.data.markPrice, data.decimalPlaces),
},
{
headerName: t('24h volume'),
type: 'rightAligned',
field: 'data.candles',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithDataAndCandles>) => {
if (!data) return 0;
const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0';
return Number(vol);
},
valueFormatter: ({
data,
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0';
const volume =
data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00';
return volume;
},
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
headerName: t('Spread'),
field: 'data.bestBidPrice',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: 'PriceFlashCell',
valueGetter: ({ data }: VegaValueGetterParams<MarketMaybeWithData>) => {
if (!data || !data.data?.bestOfferPrice || !data.data?.bestBidPrice) {
return undefined;
}
const offer = toBigNum(data.data.bestOfferPrice, data.decimalPlaces);
const bid = toBigNum(data.data.bestBidPrice, data.decimalPlaces);
const spread = offer.minus(bid).toNumber();
// The calculation above can result in '-0' being rendered after formatting
// so return Math.abs to remove it and just render '0'
if (spread === 0) {
return Math.abs(spread);
}
return spread;
},
valueFormatter: ({
value,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) => {
if (!value) return '-';
return value.toString();
},
},
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketActionsDropdown
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
successorMarketID={data.successorMarketID}
parentMarketID={data.parentMarketID}
/>
);
},
},
],
[openAssetDetailsDialog]
);
};
-1
View File
@@ -1,2 +1 @@
export const THROTTLE_UPDATE_TIME = 500;
export const ONBOARDING_VIEWED_KEY = 'vega_onboarding_viewed';
@@ -10,7 +10,7 @@ import type { SortType } from './sort-dropdown';
import { SortTypeMapping } from './sort-dropdown';
import { Sort } from './sort-dropdown';
import { subDays } from 'date-fns';
import { isMarketActive } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils';
jest.mock('@vegaprotocol/markets');
const mockUseMarketList = useMarketList as jest.Mock;
@@ -1,15 +1,13 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import {
isMarketActive,
useMarketSelectorList,
} from './use-market-selector-list';
import { Product } from '../../components/market-selector/product-selector';
import { useMarketSelectorList } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from '../../components/market-selector';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/markets', () => ({
@@ -1,18 +1,11 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from '../../components/market-selector/market-selector';
import { Sort } from './sort-dropdown';
import { Product } from './product-selector';
// Used for sort order and filter
const MARKET_TEMPLATE = [
MarketState.STATE_ACTIVE,
MarketState.STATE_SUSPENDED,
MarketState.STATE_PENDING,
];
import { isMarketActive } from '../../lib/utils';
export const useMarketSelectorList = ({
product,
@@ -87,7 +80,3 @@ export const useMarketSelectorList = ({
return { markets, data, loading, error, reload };
};
export const isMarketActive = (state: MarketState) => {
return MARKET_TEMPLATE.includes(state);
};
@@ -57,6 +57,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = screen.getAllByRole('link');
@@ -88,6 +89,7 @@ describe('Navbar', () => {
['/markets/all', 'Markets'],
[`/markets/${marketId}`, 'Trading'],
['/portfolio', 'Portfolio'],
[expect.stringContaining('governance'), 'Governance'],
];
const links = menu.getAllByRole('link');
links.forEach((link, i) => {
+20 -9
View File
@@ -1,6 +1,12 @@
import type { ButtonHTMLAttributes, LiHTMLAttributes, ReactNode } from 'react';
import { useState } from 'react';
import { useEnvironment, DocsLinks, Networks } from '@vegaprotocol/environment';
import {
useEnvironment,
DocsLinks,
Networks,
DApp,
useLinks,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useGlobalStore } from '../../stores';
import { VegaWalletConnectButton } from '../vega-wallet-connect-button';
@@ -59,14 +65,14 @@ export const Navbar = ({
<VLogo className="w-4" />
</NavLink>
{/* Left section */}
<div className="lg:hidden flex items-center">{children}</div>
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */}
<div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} />
</div>
{/* Right section */}
<div className="ml-auto flex justify-end items-center gap-2">
<div className="flex items-center justify-end ml-auto gap-2">
<ProtocolUpgradeCountdown />
<NavbarMobileButton
onClick={() => {
@@ -100,7 +106,7 @@ export const Navbar = ({
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
>
<D.Overlay
className="lg:hidden fixed inset-0 dark:bg-black/80 bg-black/50 z-20"
className="fixed inset-0 z-20 lg:hidden dark:bg-black/80 bg-black/50"
data-testid="navbar-menu-overlay"
/>
<D.Content
@@ -111,7 +117,7 @@ export const Navbar = ({
)}
data-testid="navbar-menu-content"
>
<div className="flex justify-end items-center h-10 p-1">
<div className="flex items-center justify-end h-10 p-1">
<NavbarMobileButton onClick={() => setMenu(null)}>
<span className="sr-only">{t('Close menu')}</span>
<VegaIcon name={VegaIconNames.CROSS} size={24} />
@@ -179,6 +185,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')}
</NavbarLink>
</NavbarItem>
<NavbarItem>
<NavbarLinkExternal to={useLinks(DApp.Token)()}>
{t('Governance')}
</NavbarLinkExternal>
</NavbarItem>
<NavbarItem>
<NavbarTrigger>{t('Resources')}</NavbarTrigger>
<NavbarContent data-testid="navbar-content-resources">
@@ -336,7 +347,7 @@ const NavbarLinkExternal = ({
<NavLink
to={to}
className={classNames(
'flex lg:inline-flex gap-2 justify-between items-center relative',
'flex gap-2 lg:h-full items-center',
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
)}
@@ -344,7 +355,7 @@ const NavbarLinkExternal = ({
target="_blank"
>
<span>{children}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={14} />
</NavLink>
</N.Link>
);
@@ -364,8 +375,8 @@ const BurgerIcon = () => (
const NavbarListDivider = () => {
return (
<div className="py-2 px-6 lg:px-0" role="separator">
<div className="h-px lg:h-full w-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
<div className="px-6 py-2 lg:px-0" role="separator">
<div className="w-full h-px lg:h-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
</div>
);
};
@@ -0,0 +1 @@
export { Telemetry } from './telemetry';
@@ -0,0 +1,69 @@
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { Intent, useToasts } from '@vegaprotocol/ui-toolkit';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useCallback, useEffect } from 'react';
import { TelemetryApproval } from './telemetry-approval';
import { t } from '@vegaprotocol/i18n';
import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step';
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
export const Telemetry = () => {
const onboardingDissmissed = useOnboardingStore((store) => store.dismissed);
const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] =
useTelemetryApproval();
const [setToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.remove,
]);
const onApprovalClose = useCallback(() => {
closeTelemetry();
removeToast(TELEMETRY_APPROVAL_TOAST_ID);
}, [closeTelemetry, removeToast]);
const setTelemetryApprovalAndClose = useCallback(
(value: string) => {
setTelemetryValue(value);
onApprovalClose();
},
[onApprovalClose, setTelemetryValue]
);
useEffect(() => {
if (isTelemetryNeeded && onboardingDissmissed) {
const toast: Toast = {
id: TELEMETRY_APPROVAL_TOAST_ID,
intent: Intent.Primary,
content: (
<>
<h3 className="mb-1 text-sm uppercase">
{t('Improve vega console')}
</h3>
<TelemetryApproval
telemetryValue={telemetryValue}
setTelemetryValue={setTelemetryApprovalAndClose}
/>
</>
),
onClose: onApprovalClose,
};
if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) {
setToast(toast);
}
return;
}
}, [
telemetryValue,
isTelemetryNeeded,
onboardingDissmissed,
setToast,
hasToast,
onApprovalClose,
setTelemetryApprovalAndClose,
]);
return null;
};
@@ -2,7 +2,8 @@ import { MemoryRouter } from 'react-router-dom';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { GetStarted } from './get-started';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import { useOnboardingStore } from './use-get-onboarding-step';
let mockStep = 1;
jest.mock('./use-get-onboarding-step', () => ({
@@ -44,13 +45,15 @@ describe('GetStarted', () => {
globalThis.window.vega = undefined as unknown as Vega;
});
it('renders nothing if connected', () => {
it('renders nothing if dismissed', () => {
useOnboardingStore.setState({ dismissed: true });
mockStep = 0;
const { container } = renderComponent({ pubKey: 'my-pubkey' });
expect(container).toBeEmptyDOMElement();
});
it('steps should be ticked', () => {
useOnboardingStore.setState({ dismissed: false });
const navigatorGetter: jest.SpyInstance = jest.spyOn(
window.navigator,
'userAgent',
@@ -87,6 +90,8 @@ describe('GetStarted', () => {
screen.getByRole('button', { name: 'Ready to trade' })
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Ready to trade' }));
mockStep = 5;
rerender(
<MemoryRouter>
@@ -9,17 +9,15 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useNavigate } from 'react-router-dom';
import {
OnboardingStep,
useGetOnboardingStep,
useOnboardingStore,
} from './use-get-onboarding-step';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import { useSidebar, ViewType } from '../sidebar';
import * as constants from '../constants';
import { useOnboardingStore } from './welcome-dialog';
interface Props {
lead?: string;
@@ -27,11 +25,8 @@ interface Props {
const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
const navigate = useNavigate();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const dismiss = useOnboardingStore((store) => store.dismiss);
const setDialogOpen = useOnboardingStore((store) => store.setDialogOpen);
const marketId = useGlobalStore((store) => store.marketId);
const link = marketId ? Links[Routes.MARKET](marketId) : Links[Routes.HOME]();
const openVegaWalletDialog = useVegaWalletDialogStore(
@@ -49,14 +44,14 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Deposit });
dismiss();
setDialogOpen(false);
};
} else if (step === OnboardingStep.ONBOARDING_ORDER_STEP) {
} else if (step >= OnboardingStep.ONBOARDING_ORDER_STEP) {
buttonText = t('Ready to trade');
onClickHandle = () => {
navigate(link);
setView({ type: ViewType.Order });
setOnboardingViewed('true');
dismiss();
};
}
@@ -75,17 +70,12 @@ const GetStartedButton = ({ step }: { step: OnboardingStep }) => {
export const GetStarted = ({ lead }: Props) => {
const { pubKey } = useVegaWallet();
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const currentStep = useGetOnboardingStep();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const getStartedNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP;
const CANONICAL_URL = VEGA_NETWORKS[VEGA_ENV] || 'https://console.vega.xyz';
const currentStep = useGetOnboardingStep();
const dismissed = useOnboardingStore((store) => store.dismissed);
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
@@ -94,7 +84,7 @@ export const GetStarted = ({ lead }: Props) => {
{ 'mt-8': !lead }
);
if (getStartedNeeded) {
if (!dismissed) {
return (
<div className={wrapperClasses} data-testid="get-started-banner">
{lead && <h2>{lead}</h2>}
@@ -1,3 +1,5 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { depositsProvider } from '@vegaprotocol/deposits';
import { useDataProvider } from '@vegaprotocol/data-provider';
@@ -7,6 +9,29 @@ import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { useGlobalStore } from '../../stores';
const ONBOARDING_STORAGE_KEY = 'vega_onboarding';
export const useOnboardingStore = create<{
dialogOpen: boolean;
dismissed: boolean;
dismiss: () => void;
setDialogOpen: (isOpen: boolean) => void;
}>()(
persist(
(set) => ({
dialogOpen: true,
dismissed: false,
dismiss: () => set({ dismissed: true }),
setDialogOpen: (isOpen) => set({ dialogOpen: isOpen }),
}),
{
name: ONBOARDING_STORAGE_KEY,
partialize: (state) => ({
dismissed: state.dismissed,
}),
}
)
);
export enum OnboardingStep {
ONBOARDING_UNKNOWN_STEP,
ONBOARDING_WALLET_STEP,
@@ -1,22 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import { GetStarted } from './get-started';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { useNavigate } from 'react-router-dom';
import { TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
import { useOnboardingStore } from './welcome-dialog';
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
import { useOnboardingStore } from './use-get-onboarding-step';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const setOnboardingDialog = useOnboardingStore(
(store) => store.setDialogOpen
);
const { data } = useTopTradedMarkets();
const marketId = data && data[0]?.id;
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.MARKETS]();
const dismiss = useOnboardingStore((store) => store.dismiss);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
dismiss();
};
const lead =
VEGA_ENV === Networks.MAINNET
? t('Start trading on the worlds most advanced decentralised exchange.')
@@ -25,7 +27,7 @@ export const WelcomeDialogContent = () => {
);
return (
<div className="flex flex-col sm:flex-row gap-8">
<div className="sm:w-1/2 flex flex-col justify-between pt-3">
<div className="flex flex-col justify-between pt-3 sm:w-1/2">
<ul className="ml-0">
<ListItemContent
icon={<NonCustodialIcon />}
@@ -47,15 +49,16 @@ export const WelcomeDialogContent = () => {
)}
/>
</ul>
<TradingButton
onClick={browseMarkets}
<TradingAnchorButton
href={link}
onClick={() => setOnboardingDialog(false)}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
</TradingButton>
{t('Explore')}
</TradingAnchorButton>
</div>
<div className="sm:w-1/2 flex grow">
<div className="flex sm:w-1/2 grow">
<GetStarted lead={lead} />
</div>
</div>
@@ -72,10 +75,10 @@ const ListItemContent = ({
text: string;
}) => {
return (
<li className="my-4 flex gap-3">
<div className="shrink-0 pt-1">{icon}</div>
<li className="flex my-4 gap-3">
<div className="pt-1 shrink-0">{icon}</div>
<div>
<h3 className="text-lg leading-snug mb-2">{title}</h3>
<h3 className="mb-2 text-lg leading-snug">{title}</h3>
<p className="text-sm text-secondary">{text}</p>
</div>
</li>
@@ -1,133 +1,32 @@
import { useNavigate } from 'react-router-dom';
import type { Toast } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent, useToasts } from '@vegaprotocol/ui-toolkit';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
import {
useGetOnboardingStep,
OnboardingStep,
} from './use-get-onboarding-step';
import * as constants from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
import { useCallback } from 'react';
import { useOnboardingStore } from './use-get-onboarding-step';
const ONBOARDING_STORAGE_KEY = 'vega_onboarding_dismiss_store';
export const useOnboardingStore = create<{
dismissed: boolean;
dismiss: () => void;
}>()(
persist(
(set) => ({
dismissed: false,
dismiss: () => set(() => ({ dismissed: true })),
}),
{
name: ONBOARDING_STORAGE_KEY,
}
)
);
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const navigate = useNavigate();
const [telemetryValue, setTelemetryValue, isTelemetryNeeded, closeTelemetry] =
useTelemetryApproval();
const [onBoardingViewed] = useLocalStorage(constants.ONBOARDING_VIEWED_KEY);
const dismiss = useOnboardingStore((store) => store.dismiss);
const dismissed = useOnboardingStore((store) => store.dismissed);
const currentStep = useGetOnboardingStep();
const isTelemetryPopupNeeded =
isTelemetryNeeded &&
(onBoardingViewed === 'true' ||
currentStep > OnboardingStep.ONBOARDING_ORDER_STEP);
const dialogOpen = useOnboardingStore((store) => store.dialogOpen);
const dismiss = useOnboardingStore((store) => store.dismiss);
const isOnboardingDialogNeeded =
onBoardingViewed !== 'true' &&
currentStep &&
currentStep < OnboardingStep.ONBOARDING_COMPLETE_STEP &&
!dismissed;
const marketId = useGlobalStore((store) => store.marketId);
const onClose = () => {
if (isTelemetryPopupNeeded) {
closeTelemetry();
} else {
const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
dismiss();
}
};
const [setToast, hasToast, removeToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.remove,
]);
const onApprovalClose = useCallback(() => {
closeTelemetry();
removeToast(TELEMETRY_APPROVAL_TOAST_ID);
}, [removeToast, closeTelemetry]);
const setTelemetryApprovalAndClose = useCallback(
(value: string) => {
setTelemetryValue(value);
onApprovalClose();
},
[setTelemetryValue, onApprovalClose]
);
if (isTelemetryPopupNeeded) {
const toast: Toast = {
id: TELEMETRY_APPROVAL_TOAST_ID,
intent: Intent.Primary,
content: (
<>
<h3 className="mb-1 text-sm uppercase">
{t('Improve vega console')}
</h3>
<TelemetryApproval
telemetryValue={telemetryValue}
setTelemetryValue={setTelemetryApprovalAndClose}
/>
</>
),
onClose: onApprovalClose,
};
if (!hasToast(TELEMETRY_APPROVAL_TOAST_ID)) {
setToast(toast);
}
return;
}
const title = (
<span className="font-alpha calt" data-testid="welcome-title">
{t('Console')}{' '}
<span className="text-vega-clight-100 dark:text-vega-cdark-100">
{VEGA_ENV}
</span>
</span>
);
return isOnboardingDialogNeeded ? (
return (
<Dialog
open
title={title}
open={dismissed ? false : dialogOpen}
title={
<span className="font-alpha calt" data-testid="welcome-title">
{t('Console')}{' '}
<span className="text-vega-clight-100 dark:text-vega-cdark-100">
{VEGA_ENV}
</span>
</span>
}
size="medium"
onChange={onClose}
onChange={() => dismiss()}
intent={Intent.None}
dataTestId="welcome-dialog"
>
<WelcomeDialogContent />
</Dialog>
) : null;
);
};
@@ -1,24 +1,18 @@
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useCallback } from 'react';
import { Links, Routes } from '../../pages/client-router';
export const useMarketClickHandler = (replace = false) => {
const navigate = useNavigate();
const { marketId } = useParams();
const { pathname } = useLocation();
const isMarketPage = pathname.match(/^\/markets\/(.+)/);
return useCallback(
(selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.MARKET](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else if (selectedId !== marketId || !isMarketPage) {
navigate(link, { replace });
}
},
[navigate, marketId, replace, isMarketPage]
);
return (selectedId: string, metaKey?: boolean) => {
const link = Links[Routes.MARKET](selectedId);
if (metaKey) {
window.open(`/#${link}`, '_blank');
} else {
navigate(link, { replace });
}
};
};
export const useMarketLiquidityClickHandler = () => {
@@ -0,0 +1,13 @@
import orderBy from 'lodash/orderBy';
import { calcTradedFactor, useMarketList } from '@vegaprotocol/markets';
import { isMarketActive } from '../utils';
export const useTopTradedMarkets = () => {
const { data, loading, error } = useMarketList();
const activeMarkets = data?.filter((m) => isMarketActive(m.state));
const marketsByTopTraded = data
? orderBy(activeMarkets, (m) => calcTradedFactor(m), 'desc')
: undefined;
return { data: marketsByTopTraded, loading, error };
};
+12
View File
@@ -0,0 +1,12 @@
import { MarketState } from '@vegaprotocol/types';
// Used for sort order and filter
const MARKET_TEMPLATE = [
MarketState.STATE_ACTIVE,
MarketState.STATE_SUSPENDED,
MarketState.STATE_PENDING,
];
export const isMarketActive = (state: MarketState) => {
return MARKET_TEMPLATE.includes(state);
};
+2
View File
@@ -49,6 +49,7 @@ import {
import { ViewingBanner } from '../components/viewing-banner';
import { NavHeader } from '../components/navbar/nav-header';
import { Routes as AppRoutes } from './client-router';
import { Telemetry } from '../components/telemetry';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@@ -125,6 +126,7 @@ function AppBody({ Component }: AppProps) {
<InitializeHandlers />
<MaybeConnectEagerly />
<PartyData />
<Telemetry />
</div>
);
}
+3 -1
View File
@@ -82,7 +82,9 @@ html [data-theme='light'] {
--pennant-color-volume-sell: theme(colors.market.red.DEFAULT);
--pennant-candlestick-inner-padding: 0.1;
/* reduce space between candles */
--pennant-candlestick-inner-padding: 0.175;
--pennant-candlestick-stroke-width: 0.5;
}
html [data-theme='light'] {
+25 -6
View File
@@ -3,6 +3,7 @@ import { CandlestickChart } from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react';
import debounce from 'lodash/debounce';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
@@ -22,8 +23,25 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const {
interval,
chartType,
overlays,
studies,
studySizes,
setStudies,
setStudySizes,
setOverlays,
} = useCandlesChartSettings();
const handlePaneChange = useMemo(
() =>
debounce((sizes: number[]) => {
// first number is main pain, which is greedy so we don't store it
setStudySizes(sizes.filter((_, i) => i !== 0));
}, 300),
[setStudySizes]
);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey);
@@ -45,15 +63,16 @@ export const CandlesChartContainer = ({
initialNumCandlesToDisplay: Math.floor(
width * CANDLES_TO_WIDTH_FACTOR
),
studySize: 150, // default size
studySizes,
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
merge({
overlays: options.overlays,
studies: options.studies,
});
setStudies(options.studies);
setOverlays(options.overlays);
}}
onPaneChanged={handlePaneChange}
/>
</div>
)}
@@ -5,36 +5,45 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
type StudySizes = { [S in Study]?: number };
interface StoredSettings {
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
studySizes: StudySizes;
}
export const STUDY_SIZE = 100;
const STUDY_ORDER: Study[] = [
Study.FORCE_INDEX,
Study.RELATIVE_STRENGTH_INDEX,
Study.ELDAR_RAY,
Study.MACD,
Study.VOLUME,
];
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [Overlay.MOVING_AVERAGE],
studies: [Study.MACD, Study.VOLUME],
studySizes: {},
};
export const useCandlesChartSettingsStore = create<
StoredSettings & {
merge: (settings: Partial<StoredSettings>) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
setOverlays: (overlays?: Overlay[]) => void;
setStudies: (studies?: Study[]) => void;
setStudySizes: (sizes: number[]) => void;
}
>()(
persist(
immer((set) => ({
...DEFAULT_CHART_SETTINGS,
merge: (settings: Partial<StoredSettings>) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type) =>
set((state) => {
state.type = type;
@@ -43,14 +52,35 @@ export const useCandlesChartSettingsStore = create<
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays) =>
setOverlays: (overlays) => {
if (!overlays) return;
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies) =>
});
},
setStudies: (studies) => {
if (!studies) return;
// Make sure studies are always returned in the same order
studies.sort((a, b) => {
return STUDY_ORDER.indexOf(a) - STUDY_ORDER.indexOf(b);
});
set((state) => {
state.studies = studies;
}),
});
},
setStudySizes: (sizes) => {
set((state) => {
// for every study find the corresonding size and update
// the size record for that study
state.studies.forEach((s, i) => {
const size = sizes[i];
state.studySizes[s] = size;
});
});
},
})),
{
name: 'vega_candles_chart_store',
@@ -85,15 +115,22 @@ export const useCandlesChartSettings = () => {
[Study.VOLUME]
);
// find the study size
const studySizes = studies.map((s) => {
const size = settings.studySizes[s] || STUDY_SIZE;
return size;
});
return {
interval,
chartType,
overlays,
studies,
studySizes,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
merge: settings.merge,
setStudySizes: settings.setStudySizes,
};
};
@@ -61,10 +61,15 @@ export function addVegaWalletConnect() {
});
}
const onboardingViewedState = { state: { dismissed: true }, version: 0 };
export function addSetVegaWallet() {
Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem(
'vega_onboarding',
JSON.stringify(onboardingViewedState)
);
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
win.localStorage.setItem(
@@ -82,7 +87,10 @@ export function addSetVegaWallet() {
export function addSetOnBoardingViewed() {
Cypress.Commands.add('setOnBoardingViewed', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem(
'vega_onboarding',
JSON.stringify(onboardingViewedState)
);
win.localStorage.setItem('vega_telemetry_approval', 'false');
win.localStorage.setItem('vega_telemetry_viewed', 'true');
});
+1
View File
@@ -12,6 +12,7 @@ export * from './lib/cells/centered-grid-cell';
export * from './lib/cells/market-name-cell';
export * from './lib/cells/order-type-cell';
export * from './lib/cells/size';
export * from './lib/cells/stacked-cell';
export * from './lib/filters/date-range-filter';
export * from './lib/filters/set-filter';
@@ -59,7 +59,7 @@ export const MarketNameCell = ({
(data as { market: Market })?.market?.tradableInstrument.instrument.product
.__typename;
if (!value) return;
if (!value) return null;
const content = (
<>
<span data-testid="market-code" data-market-id={id}>
@@ -72,7 +72,7 @@ export const MarketNameCell = ({
<button
onClick={handleOnClick}
tabIndex={0}
className="block text-left text-ellipsis overflow-hidden whitespace-nowrap w-full"
className="block w-full overflow-hidden text-left text-ellipsis whitespace-nowrap"
>
{content}
</button>
@@ -11,8 +11,7 @@ import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
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 * as AccordionPrimitive from '@radix-ui/react-accordion';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
@@ -24,7 +23,13 @@ import {
} from '../../constants';
import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value';
import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit';
import {
Accordion,
AccordionChevron,
AccordionPanel,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const emptyValue = '-';
@@ -246,31 +251,56 @@ export const DealTicketMarginDetails = ({
const quoteName = market.tradableInstrument.instrument.product.quoteName;
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>
<div className="flex flex-col gap-2 w-full">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full pt-2',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center gap-2 justify-between w-full"
>
<div className="flex items-center gap-1">
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
noUnderline
>
<div className="font-mono text-right">
{formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col gap-2 w-full">
<KeyValue
label={t('Total margin available')}
indent
@@ -310,12 +340,12 @@ export const DealTicketMarginDetails = ({
quantum
)}
/>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue
label={t('Liquidation price estimate')}
label={t('Liquidation')}
value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
@@ -329,6 +359,6 @@ export const DealTicketMarginDetails = ({
onClose={onAccountBreakdownDialogClose}
/>
)}
</>
</div>
);
};
@@ -538,7 +538,7 @@ const NotionalAndFees = ({
market.positionDecimalPlaces
);
return (
<div className="mb-4">
<div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
@@ -464,7 +464,7 @@ export const DealTicket = ({
)}
/>
)}
<div className="mb-4">
<div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue
label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)}
@@ -25,11 +25,11 @@ export const KeyValue = ({
}: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? (
<button onClick={onClick} className="text-muted">
<button onClick={onClick} className="font-mono">
{displayValue}
</button>
) : (
<div className="text-muted">{displayValue}</div>
<div className="font-mono">{displayValue}</div>
);
return (
<div
@@ -40,14 +40,14 @@ export const KeyValue = ({
}`}
key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap',
'text-xs flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent }
)}
>
<Tooltip description={labelDescription}>
<div>{label}</div>
<div className="text-muted">{label}</div>
</Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`} noUnderline>
{valueElement}
</Tooltip>
</div>
+2 -2
View File
@@ -8,7 +8,7 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
[settlementAsset]
);
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
'Estimated total margin that will cover open position, active orders and this order.'
'Estimated total margin that will cover open positions, active orders and this order.'
);
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
);
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is a approximation to the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
);
export const EST_SLIPPAGE = t(
+4 -2
View File
@@ -7,12 +7,12 @@ export type DataSourceFilterFragment = { __typename?: 'Filter', key: { __typenam
export type DataSourceSpecFragment = { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } };
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
export type MarketFieldsFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } };
export type MarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
export type MarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, parentMarketID?: string | null, successorMarketID?: string | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number, quantum: string }, dataSourceSpecForTradingTermination: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecForSettlementData: { __typename?: 'DataSourceSpec', id: string, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null } }> | null } } | { __typename?: 'DataSourceDefinitionInternal' } } }, dataSourceSpecBinding: { __typename?: 'DataSourceSpecToFutureBinding', settlementDataProperty: string, tradingTerminationProperty: string } } } }, marketTimestamps: { __typename?: 'MarketTimestamps', open: any, close: any } } }> } | null };
export const DataSourceFilterFragmentDoc = gql`
fragment DataSourceFilter on Filter {
@@ -55,6 +55,8 @@ export const MarketFieldsFragmentDoc = gql`
positionDecimalPlaces
state
tradingMode
parentMarketID
successorMarketID
fees {
factors {
makerFee
-1
View File
@@ -2,7 +2,6 @@ export * from './fees-breakdown';
export * from './last-24h-price-change';
export * from './last-24h-volume';
export * from './market-info';
export * from './markets-container';
export * from './oracle-banner';
export * from './oracle-basic-profile';
export * from './oracle-full-profile';
@@ -1,2 +0,0 @@
export * from './markets-container';
export * from './market-table-actions';
@@ -1,69 +0,0 @@
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import {
AgGridLazy as AgGrid,
PriceFlashCell,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '../../markets-provider';
import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs';
export const getRowId = ({ data }: { data: { id: string } }) => data.id;
interface MarketNameCellProps {
value?: string;
data?: MarketMaybeWithData;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
const MarketName = (props: MarketNameCellProps) => (
<>
<MarketNameCell {...props} />
{props.data ? (
<OracleStatus
dataSourceSpecForSettlementData={
props.data.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData
}
dataSourceSpecForTradingTermination={
props.data.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination
}
/>
) : null}
</>
);
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
};
export const MarketListTable = ({
onMarketClick,
SuccessorMarketRenderer,
...props
}: Props) => {
const columnDefs = useColumnDefs({ onMarketClick });
const components = {
PriceFlashCell,
MarketName,
...(SuccessorMarketRenderer ? { SuccessorMarketRenderer } : null),
};
return (
<AgGrid
getRowId={getRowId}
defaultColDef={defaultColDef}
columnDefs={columnDefs}
suppressCellFocus
components={components}
{...props}
/>
);
};
export default MarketListTable;
@@ -1,179 +0,0 @@
import { render, screen, act, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as DataProviders from '@vegaprotocol/data-provider';
import { MockedProvider } from '@apollo/react-testing';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketsContainer } from './markets-container';
import { FLAGS } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
},
};
});
const SuccessorMarketRenderer = ({ value }: { value: string }) => {
return '-';
};
const market = {
id: 'id-1',
tradableInstrument: {
instrument: {
product: { settlementAsset: { id: 'assetId-1' } },
},
},
decimalPlaces: 1,
positionDecimalPlaces: 1,
state: 'STATE_ACTIVE',
tradingMode: 'TRADING_MODE_OPENING_AUCTION',
data: {
bestBidPrice: 100,
},
} as unknown as MarketMaybeWithData;
describe('MarketsContainer', () => {
const spyOnSelect = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [market],
};
});
});
it('context menu should stay open', async () => {
let rerenderRef: (ui: React.ReactElement) => void;
await act(async () => {
const { rerender } = render(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
</MockedProvider>
);
rerenderRef = rerender;
});
// make sure ag grid is finished initializaing
const rowContainer = await screen.findByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-center-cols-container'),
});
expect(within(rowContainer).getAllByRole('row')).toHaveLength(1);
expect(
screen.getByRole('rowgroup', {
name: (_name, element) =>
element.classList.contains('ag-pinned-right-cols-container'),
})
).toBeInTheDocument();
// open the dropdown
await userEvent.click(
screen.getByRole('button', {
name: (_name, element) =>
(element.parentNode as Element)?.getAttribute('id') ===
'cell-market-actions-9',
})
);
await checkDropdown();
// reset the mock and rerender so the component
// updates with new data
jest
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.spyOn<typeof DataProviders, any>(DataProviders, 'useDataProvider')
.mockImplementation(() => {
return {
error: null,
reload: jest.fn(),
data: [{ ...market, state: 'STATE_PENDING' }],
};
});
// @ts-ignore we await the act above so rerenderRef is definitely defined
rerenderRef(
<MockedProvider mocks={[]}>
<MarketsContainer onSelect={spyOnSelect} />
</MockedProvider>
);
// make sure dropdown is still open
await checkDropdown();
async function checkDropdown() {
const dropdownContent = await screen.findByTestId(
'market-actions-content'
);
expect(dropdownContent).toBeInTheDocument();
expect(
within(dropdownContent).getByRole('menuitem', {
name: 'Copy Market ID',
})
).toBeInTheDocument();
}
});
it('SuccessorMarketRenderer should be rendered', async () => {
const successorMarketName = 'Successor Market Name';
const spySuccessorMarketRenderer = jest
.fn()
.mockReturnValue(successorMarketName);
render(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={spySuccessorMarketRenderer}
/>
</MockedProvider>
);
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'successorMarketID',
})
).toBeInTheDocument();
expect(
screen.getByRole('presentation', {
name: (_name, element) =>
element.getAttribute('id') === 'cell-successorMarketID-14',
})
).toHaveTextContent(successorMarketName);
});
it('feature flag should hide successorMarketID column', async () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = false;
const spySuccessorMarketRenderer = jest.fn();
render(
<MockedProvider>
<MarketsContainer
onSelect={spyOnSelect}
SuccessorMarketRenderer={spySuccessorMarketRenderer}
/>
</MockedProvider>
);
expect(spySuccessorMarketRenderer).not.toHaveBeenCalled();
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('successorMarketID');
});
});
});
@@ -1,66 +0,0 @@
import type { MouseEvent } from 'react';
import React, { useEffect } from 'react';
import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import { MarketListTable } from './market-list-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketsWithDataProvider as dataProvider } from '../../markets-provider';
import type { MarketMaybeWithData } from '../../markets-provider';
const POLLING_TIME = 2000;
interface MarketsContainerProps {
onSelect: (marketId: string, metaKey?: boolean) => void;
SuccessorMarketRenderer?: React.FC<{ value: string }>;
}
export const MarketsContainer = ({
onSelect,
SuccessorMarketRenderer,
}: MarketsContainerProps) => {
const { data, error, reload } = useDataProvider({
dataProvider,
variables: undefined,
});
useEffect(() => {
const interval = setInterval(() => {
reload();
}, POLLING_TIME);
return () => {
clearInterval(interval);
};
}, [reload]);
return (
<div className="h-full relative">
<MarketListTable
rowData={data}
onCellClicked={(cellEvent: CellClickedEvent) => {
const { data, column, event } = cellEvent;
// prevent navigating to the market page if any of the below cells are clicked
// event.preventDefault or event.stopPropagation dont seem to apply for aggird
const colId = column.getColId();
if (
[
'id',
'tradableInstrument.instrument.code',
'tradableInstrument.instrument.product.settlementAsset',
'tradableInstrument.instrument.product.settlementAsset.symbol',
'market-actions',
].includes(colId)
) {
return;
}
onSelect(
(data as MarketMaybeWithData).id,
(event as unknown as MouseEvent)?.metaKey ||
(event as unknown as MouseEvent)?.ctrlKey
);
}}
onMarketClick={onSelect}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
SuccessorMarketRenderer={SuccessorMarketRenderer}
/>
</div>
);
};
@@ -1,44 +0,0 @@
import { useMemo } from 'react';
import { useEnvironment } from '@vegaprotocol/environment';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { getMatchingOracleProvider, useOracleProofs } from '../../hooks';
import type { Market } from '../../markets-provider';
import { getVerifiedStatusIcon } from '../oracle-basic-profile';
export const OracleStatus = ({
dataSourceSpecForSettlementData,
dataSourceSpecForTradingTermination,
}: Pick<
Market['tradableInstrument']['instrument']['product'],
'dataSourceSpecForSettlementData' | 'dataSourceSpecForTradingTermination'
>) => {
const { ORACLE_PROOFS_URL } = useEnvironment();
const { data: providers } = useOracleProofs(ORACLE_PROOFS_URL);
return useMemo(() => {
if (providers) {
const settlementDataProvider = getMatchingOracleProvider(
dataSourceSpecForSettlementData.data,
providers
);
const tradingTerminationDataProvider = getMatchingOracleProvider(
dataSourceSpecForTradingTermination.data,
providers
);
let maliciousOracleProvider = null;
if (settlementDataProvider?.oracle.status !== 'GOOD') {
maliciousOracleProvider = settlementDataProvider;
} else if (tradingTerminationDataProvider?.oracle.status !== 'GOOD') {
maliciousOracleProvider = tradingTerminationDataProvider;
}
if (!maliciousOracleProvider) return null;
const { icon } = getVerifiedStatusIcon(maliciousOracleProvider);
return <Icon size={3} name={icon as IconName} className="ml-1" />;
}
return null;
}, [
providers,
dataSourceSpecForSettlementData,
dataSourceSpecForTradingTermination,
]);
};
@@ -1,29 +0,0 @@
import * as React from 'react';
/*
import { useMarketOverview } from '../../../../hooks/use-market-overview'
import { colorByMarketMovement } from '../../../../lib/vega-colours'
import { Sparkline } from '../../components/sparkline'
import { VEGA_TABLE_CLASSES } from '../../components/vega-table'
*/
export interface SummaryCellProps {
value: string; // marketId
}
export const SummaryCellView = ({ value }: SummaryCellProps) => {
// const { sparkline, change, bullish } = useMarketOverview(value)
// const color = colorByMarketMovement(bullish)
return (
<>
{/* <Sparkline data={sparkline} style={{ marginRight: 4 }} />*/}
<span>{'change'}</span>
</>
);
};
SummaryCellView.displayName = 'SummaryCellView';
export const SummaryCell = React.memo(SummaryCellView);
SummaryCell.displayName = 'SummaryCell';
@@ -1,205 +0,0 @@
import { useMemo } from 'react';
import type { ColDef } from 'ag-grid-community';
import compact from 'lodash/compact';
import { t } from '@vegaprotocol/i18n';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { FLAGS } from '@vegaprotocol/environment';
import type { MarketMaybeWithData } from '../../markets-provider';
import { MarketActionsDropdown } from './market-table-actions';
interface Props {
onMarketClick: (marketId: string, metaKey?: boolean) => void;
}
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = ({ onMarketClick }: Props) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
() =>
compact([
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
cellRenderer: 'MarketName',
cellRendererParams: { onMarketClick },
},
{
headerName: t('Description'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Trading mode'),
field: 'tradingMode',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return '-';
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[marketTradingMode];
},
filter: SetFilter,
filterParams: {
set: Schema.MarketTradingModeMapping,
},
},
{
headerName: t('Status'),
field: 'state',
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'state'>) => {
return data?.state ? Schema.MarketStateMapping[data.state] : '-';
},
filter: SetFilter,
filterParams: {
set: Schema.MarketStateMapping,
},
},
FLAGS.SUCCESSOR_MARKETS && {
headerName: t('Successor market'),
field: 'successorMarketID',
cellRenderer: 'SuccessorMarketRenderer',
},
{
headerName: t('Best bid'),
field: 'data.bestBidPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestBidPrice === undefined
? undefined
: toBigNum(
data?.data?.bestBidPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestBidPrice'
>) =>
data?.data?.bestBidPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestBidPrice,
data.decimalPlaces
),
},
{
headerName: t('Best offer'),
field: 'data.bestOfferPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.bestOfferPrice === undefined
? undefined
: toBigNum(
data?.data?.bestOfferPrice,
data.decimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
MarketMaybeWithData,
'data.bestOfferPrice'
>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.bestOfferPrice,
data.decimalPlaces
),
},
{
headerName: t('Mark price'),
field: 'data.markPrice',
type: 'rightAligned',
cellRenderer: 'PriceFlashCell',
filter: 'agNumberColumnFilter',
valueGetter: ({
data,
}: VegaValueGetterParams<MarketMaybeWithData>) => {
return data?.data?.markPrice === undefined
? undefined
: toBigNum(data?.data?.markPrice, data.decimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data.markPrice'>) =>
data?.data?.bestOfferPrice === undefined
? '-'
: addDecimalsFormatNumber(
data.data.markPrice,
data.decimalPlaces
),
},
{
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketMaybeWithData,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
colId: 'market-actions',
field: 'id',
...COL_DEFS.actions,
cellRenderer: ({
data,
}: VegaICellRendererParams<MarketMaybeWithData>) => {
if (!data) return null;
return (
<MarketActionsDropdown
marketId={data.id}
assetId={
data.tradableInstrument.instrument.product.settlementAsset.id
}
/>
);
},
},
]),
[onMarketClick, openAssetDetailsDialog]
);
};
@@ -43,6 +43,7 @@ export const getVerifiedStatusIcon = (provider: Provider) => {
const lastVerified = provider.oracle.last_verified
? new Date(provider.oracle.last_verified)
: new Date(provider.oracle.first_verified);
return {
...getIconIntent(),
message: t(
@@ -112,13 +113,13 @@ export const OracleBasicProfile = ({
<Icon size={3} name={icon as IconName} />
</span>
</span>
<p className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2">
<p className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300">
{message}
</p>
{oracleMarkets && (
<p
data-testid="signed-proofs"
className="text-sm dark:text-vega-light-300 text-vega-dark-300 mb-2"
className="mb-2 text-sm dark:text-vega-light-300 text-vega-dark-300"
>
{t('Involved in %s %s', [
oracleMarkets.length.toString(),
@@ -130,9 +131,9 @@ export const OracleBasicProfile = ({
<div className="flex flex-row gap-3">
{links.map((link) => (
<ExternalLink key={link.url} href={link.url} data-testid={link.url}>
<span className="flex gap-1 items-center">
<span className="flex items-center gap-1">
<VegaIcon name={getLinkIcon(link.type)} />
<span className="capitalize underline">{link.type}</span>
<span className="underline capitalize">{link.type}</span>
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</span>
</ExternalLink>
-33
View File
@@ -28,8 +28,6 @@ import {
} from './market-utils';
import { MarketsDocument } from './__generated__/markets';
import type { Candle } from './market-candles-provider';
import type { SuccessorMarketIdsQuery } from './__generated__/SuccessorMarket';
import { SuccessorMarketIdsDocument } from './__generated__';
export type Market = MarketFieldsFragment;
@@ -240,34 +238,3 @@ export const useMarketList = () => {
reload,
};
};
export type MarketSuccessors = {
__typename?: 'Market';
id: string;
successorMarketID?: string | null;
parentMarketID?: string | null;
};
const getMarketSuccessorData = (
responseData: SuccessorMarketIdsQuery | null
): MarketSuccessors[] | null =>
responseData?.marketsConnection?.edges.map((edge) => edge.node) || null;
export const marketSuccessorProvider = makeDataProvider<
SuccessorMarketIdsQuery,
MarketSuccessors[],
never,
never
>({
query: SuccessorMarketIdsDocument,
getData: getMarketSuccessorData,
fetchPolicy: 'no-cache',
});
export const useSuccessorMarketIds = (marketId: string) => {
const { data } = useDataProvider({
dataProvider: marketSuccessorProvider,
variables: undefined,
skip: !marketId,
});
return data?.find((item) => item.id === marketId) ?? null;
};
+2
View File
@@ -36,6 +36,8 @@ fragment MarketFields on Market {
positionDecimalPlaces
state
tradingMode
parentMarketID
successorMarketID
fees {
factors {
makerFee
+2
View File
@@ -42,6 +42,8 @@ export const createMarketFragment = (
close: null,
open: null,
},
successorMarketID: null,
parentMarketID: null,
fees: {
__typename: 'Fees',
factors: {
+1 -1
View File
@@ -15,6 +15,7 @@ import {
MarketNameCell,
ProgressBarCell,
MarketProductPill,
StackedCell,
} from '@vegaprotocol/datagrid';
import {
ButtonLink,
@@ -40,7 +41,6 @@ import {
import { DocsLinks } from '@vegaprotocol/environment';
import { PositionActionsDropdown } from './position-actions-dropdown';
import { LiquidationPrice } from './liquidation-price';
import { StackedCell } from './stacked-cell';
interface Props extends TypedDataAgGrid<Position> {
onClose?: (data: Position) => void;
@@ -1,10 +1,4 @@
import {
render,
screen,
act,
waitFor,
getAllByRole,
} from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import merge from 'lodash/merge';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
@@ -14,25 +8,13 @@ import { createProposalListFieldsFragment } from '../../lib/proposals-data-provi
import type { ProposalsListQuery } from '../../lib';
import { ProposalsListDocument } from '../../lib';
import type { PartialDeep } from 'type-fest';
import { FLAGS } from '@vegaprotocol/environment';
jest.mock('@vegaprotocol/environment', () => {
const actual = jest.requireActual('@vegaprotocol/environment');
return {
...actual,
FLAGS: {
...actual.FLAGS,
SUCCESSOR_MARKETS: true,
},
};
});
const successorMarketName = 'Successor Market Name';
const spySuccessorMarketRenderer = jest
.fn()
.mockReturnValue(successorMarketName);
const parentMarketName = 'Parent Market Name';
const ParentMarketCell = () => <span>{parentMarketName}</span>;
describe('ProposalsList', () => {
const rowContainerSelector = '.ag-center-cols-container';
const createProposalsMock = (override?: PartialDeep<ProposalsListQuery>) => {
const defaultProposalEdges = [
{
@@ -81,75 +63,55 @@ describe('ProposalsList', () => {
return mock;
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should be properly rendered', async () => {
const mock = createProposalsMock();
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
const container = document.querySelector('.ag-center-cols-container');
render(
<MockedProvider mocks={[mock]}>
<ProposalsList cellRenderers={{ ParentMarketCell }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(3);
});
it('some of states should be filtered out', async () => {
const proposalNode = createProposalListFieldsFragment({
id: 'id-1',
state: Types.ProposalState.STATE_ENACTED,
expect(document.querySelector(rowContainerSelector)).toBeInTheDocument();
});
const mock = createProposalsMock({
proposalsConnection: {
edges: [
{
__typename: 'ProposalEdge',
node: {
...proposalNode,
terms: {
...proposalNode.terms,
change: {
...proposalNode.terms.change,
},
},
},
},
],
},
} as PartialDeep<ProposalsListQuery>);
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
});
const expectedHeaders = [
'Market',
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'', // actions col
];
expect(spySuccessorMarketRenderer).toHaveBeenCalled();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(expectedHeaders.length);
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'parentMarket',
})
).toBeInTheDocument();
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual(expectedHeaders);
const container = within(
document.querySelector(rowContainerSelector) as HTMLElement
);
expect(container.getAllByRole('row')).toHaveLength(
// @ts-ignore data is mocked
mock?.result?.data.proposalsConnection.edges.length
);
expect(
screen.getAllByRole('gridcell', {
name: (name, element) =>
element.getAttribute('col-id') === 'parentMarket',
container.getAllByRole('gridcell', {
name: (_, element) =>
element.getAttribute('col-id') ===
'terms.change.successorConfiguration.parentMarketId',
})[0]
).toHaveTextContent(successorMarketName);
).toHaveTextContent(parentMarketName);
});
it('empty response should causes no data message display', async () => {
@@ -169,58 +131,11 @@ describe('ProposalsList', () => {
},
},
};
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
expect(await screen.findByText('No markets')).toBeInTheDocument();
expect(
screen.getByRole('columnheader', {
name: (_name, element) =>
element.getAttribute('col-id') === 'parentMarket',
})
).toBeInTheDocument();
});
it('feature flag should hide parent marketcolumn', async () => {
const mockedFlags = jest.mocked(FLAGS);
mockedFlags.SUCCESSOR_MARKETS = false;
const mock: MockedResponse<ProposalsListQuery> = {
request: {
query: ProposalsListDocument,
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
},
result: {
data: {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: [],
},
},
},
};
await act(() => {
render(
<MockedProvider mocks={[mock]}>
<ProposalsList SuccessorMarketRenderer={spySuccessorMarketRenderer} />
</MockedProvider>
);
});
await waitFor(() => {
expect(
screen.getByRole('columnheader', {
name: (_name, element) => element.getAttribute('col-id') === 'market',
})
).toBeInTheDocument();
});
screen.getAllByRole('columnheader').forEach((element) => {
expect(element.getAttribute('col-id')).not.toEqual('parentMarket');
});
render(
<MockedProvider mocks={[mock]}>
<ProposalsList cellRenderers={{ ParentMarketCell }} />
</MockedProvider>
);
expect(await screen.findByText('No proposed markets')).toBeInTheDocument();
});
});
@@ -1,11 +1,11 @@
import React from 'react';
import type { FC } from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import { MarketNameProposalCell, useColumnDefs } from './use-column-defs';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useColumnDefs } from './use-column-defs';
export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
data.filter((proposal) =>
@@ -16,13 +16,19 @@ export const getNewMarketProposals = (data: ProposalListFieldsFragment[]) =>
].includes(proposal.state)
);
const defaultColDef = {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
interface ProposalListProps {
SuccessorMarketRenderer: React.FC<{ value: string }>;
cellRenderers: {
[name: string]: FC<{ value: string; data: ProposalListFieldsFragment }>;
};
}
export const ProposalsList = ({
SuccessorMarketRenderer,
}: ProposalListProps) => {
export const ProposalsList = ({ cellRenderers }: ProposalListProps) => {
const { data } = useProposalsListQuery({
variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
@@ -32,7 +38,7 @@ export const ProposalsList = ({
const filteredData = getNewMarketProposals(
removePaginationWrapper(data?.proposalsConnection?.edges)
);
const { columnDefs, defaultColDef } = useColumnDefs();
const columnDefs = useColumnDefs();
return (
<AgGrid
@@ -40,8 +46,9 @@ export const ProposalsList = ({
rowData={filteredData}
defaultColDef={defaultColDef}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={t('No markets')}
components={{ SuccessorMarketRenderer, MarketNameProposalCell }}
overlayNoRowsTemplate={t('No proposed markets')}
components={cellRenderers}
rowHeight={45}
/>
);
};
@@ -6,9 +6,9 @@ import {
COL_DEFS,
DateRangeFilter,
SetFilter,
StackedCell,
} from '@vegaprotocol/datagrid';
import compact from 'lodash/compact';
import { useEnvironment, FLAGS } from '@vegaprotocol/environment';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -19,50 +19,15 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { ExternalLink, Pill } from '@vegaprotocol/ui-toolkit';
import {
ProposalProductTypeMapping,
ProposalProductTypeShortName,
ProductTypeMapping,
ProductTypeShortName,
ProposalStateMapping,
} from '@vegaprotocol/types';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { VoteProgress } from '../voting-progress';
import { ProposalActionsDropdown } from '../proposal-actions-dropdown';
export const MarketNameProposalCell = ({
value,
data,
}: VegaICellRendererParams<
ProposalListFieldsFragment,
'terms.change.instrument.code'
>) => {
const { VEGA_TOKEN_URL } = useEnvironment();
const { change } = data?.terms || {};
if (change?.__typename === 'NewMarket' && VEGA_TOKEN_URL) {
const type = change.instrument.futureProduct?.__typename;
const content = (
<>
<span data-testid="market-code">{value as string}</span>
{type && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProposalProductTypeMapping[type]}
>
{ProposalProductTypeShortName[type]}
</Pill>
)}
</>
);
if (data?.id) {
const link = `${VEGA_TOKEN_URL}/proposals/${data.id}`;
return <ExternalLink href={link}>{content}</ExternalLink>;
}
return content;
}
return null;
};
export const useColumnDefs = () => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
@@ -80,17 +45,36 @@ export const useColumnDefs = () => {
headerName: t('Market'),
field: 'terms.change.instrument.code',
cellStyle: { lineHeight: '14px' },
cellRenderer: 'MarketNameProposalCell',
},
{
colId: 'description',
headerName: t('Description'),
field: 'terms.change.instrument.name',
cellRenderer: ({
value,
data,
}: {
value: string;
data: ProposalListFieldsFragment;
}) => {
if (!value || !data) return '-';
// TODO: update when we switch to ProductConfiguration
const productType = 'Future';
return (
<StackedCell
primary={value}
secondary={
<span
title={ProductTypeMapping[productType]}
className="uppercase"
>
{ProductTypeShortName[productType]}
</span>
}
/>
);
},
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'terms.change.instrument.futureProduct.settlementAsset.name',
field: 'terms.change.instrument.futureProduct.settlementAsset.symbol',
},
{
colId: 'state',
@@ -105,12 +89,10 @@ export const useColumnDefs = () => {
set: ProposalStateMapping,
},
},
FLAGS.SUCCESSOR_MARKETS && {
{
headerName: t('Parent market'),
field: 'id',
colId: 'parentMarket',
cellRenderer: 'SuccessorMarketRenderer',
cellRendererParams: { parent: true },
field: 'terms.change.successorConfiguration.parentMarketId',
cellRenderer: 'ParentMarketCell',
},
{
colId: 'voting',
@@ -169,25 +151,12 @@ export const useColumnDefs = () => {
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (!data?.id) return null;
return <ProposalActionsDropdown id={data.id} />;
},
},
]);
}, [requiredMajorityPercentage]);
const defaultColDef: ColDef = useMemo(() => {
return {
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
};
}, []);
return useMemo(
() => ({
columnDefs,
defaultColDef,
}),
[columnDefs, defaultColDef]
);
return columnDefs;
};
@@ -114,6 +114,9 @@ fragment NewMarketFields on NewMarket {
lpPriceRange
# linearSlippageFactor
# quadraticSlippageFactor
successorConfiguration {
parentMarketId
}
}
fragment UpdateMarketFields on UpdateMarket {
File diff suppressed because one or more lines are too long
@@ -109,6 +109,58 @@ export const marketUpdateProposal: ProposalListFieldsFragment = {
export const createProposalListFieldsFragment = (
override?: PartialDeep<ProposalListFieldsFragment>
): ProposalListFieldsFragment => {
const newMarket = {
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: undefined,
successorConfiguration: {
__typename: 'SuccessorConfiguration',
parentMarketId: 'xyz',
},
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
decimals: 1,
quantum: '1',
__typename: 'Asset',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
} as const;
const defaultProposal: ProposalListFieldsFragment = {
id: 'e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829',
reference: 'injected_at_runtime',
@@ -147,54 +199,7 @@ export const createProposalListFieldsFragment = (
terms: {
closingDatetime: '2022-11-15T12:44:34Z',
enactmentDatetime: '2022-11-15T12:44:54Z',
change: {
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: [],
instrument: {
code: 'ETHUSD',
name: 'ETHUSD',
futureProduct: {
settlementAsset: {
id: 'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
symbol: 'tDAI',
decimals: 1,
quantum: '1',
__typename: 'Asset',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
},
},
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
change: newMarket,
__typename: 'ProposalTerms',
},
__typename: 'Proposal',
@@ -1,5 +1,6 @@
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import classNames from 'classnames';
import type { VegaIconProps } from '../icon';
import { VegaIcon, VegaIconNames } from '../icon';
export interface AccordionItemProps {
@@ -24,6 +25,28 @@ export const Accordion = ({ panels, children }: AccordionProps) => {
);
};
export const AccordionPanel = ({
trigger,
children,
itemId,
}: {
trigger: React.ReactNode;
children: React.ReactNode;
itemId: string;
}) => {
return (
<AccordionPrimitive.Item value={itemId}>
<AccordionPrimitive.Header>{trigger}</AccordionPrimitive.Header>
<AccordionPrimitive.Content
className="py-3 text-sm"
data-testid="accordion-content"
>
{children}
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
);
};
export const AccordionItem = ({
title,
content,
@@ -31,7 +54,7 @@ export const AccordionItem = ({
}: AccordionPanelProps) => {
const triggerClassNames = classNames(
'w-full py-2',
'flex items-center justify-between border-b border-vega-light-200 dark:border-vega-dark-200 text-sm',
'flex items-center justify-between gap-2 border-b border-vega-light-200 dark:border-vega-dark-200 text-sm',
'group'
);
return (
@@ -41,7 +64,9 @@ export const AccordionItem = ({
data-testid="accordion-toggle"
className={triggerClassNames}
>
<span data-testid="accordion-title">{title}</span>
<span data-testid="accordion-title" className="flex-1 text-left">
{title}
</span>
<AccordionChevron aria-hidden />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
@@ -55,15 +80,17 @@ export const AccordionItem = ({
);
};
export const AccordionChevron = () => {
export const AccordionChevron = ({
size = 16,
}: Pick<VegaIconProps, 'size'>) => {
return (
<span
className={classNames(
'transform transition ease-in-out duration-300',
'flex transform transition ease-in-out duration-300',
'group-data-[state=open]:rotate-180'
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden size={size} />
</span>
);
};
@@ -1,7 +1,8 @@
export const IconEyeOff = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M16 7.97v-.02-.01-.02-.02a.672.672 0 00-.17-.36c-.49-.63-1.07-1.2-1.65-1.72l-3.16 2.26a2.978 2.978 0 01-2.98 2.9c-.31 0-.6-.06-.88-.15L5.09 12.3c.44.19.9.36 1.37.47.97.23 1.94.24 2.92.05.88-.17 1.74-.54 2.53-.98 1.25-.7 2.39-1.67 3.38-2.75.18-.2.37-.41.53-.62.09-.1.15-.22.17-.36v-.02-.02-.01-.02-.03c.01-.02.01-.03.01-.04zm-.43-4.17c.25-.18.43-.46.43-.8 0-.55-.45-1-1-1-.22 0-.41.08-.57.2l-.01-.01-2.67 1.91c-.69-.38-1.41-.69-2.17-.87a6.8 6.8 0 00-2.91-.05c-.88.18-1.74.54-2.53.99-1.25.7-2.39 1.67-3.38 2.75-.18.2-.37.41-.53.62-.23.29-.23.63-.01.92.51.66 1.11 1.25 1.73 1.79.18.16.38.29.56.44l-2.09 1.5.01.01c-.25.18-.43.46-.43.8 0 .55.45 1 1 1 .22 0 .41-.08.57-.2l.01.01 14-10-.01-.01zm-10.41 5a3.03 3.03 0 01-.11-.8 2.99 2.99 0 012.99-2.98c.62 0 1.19.21 1.66.53L5.16 8.8z" />
<path d="M13.5 13.9989C13.4343 13.999 13.3693 13.9861 13.3086 13.961C13.248 13.9358 13.1929 13.8989 13.1466 13.8524L2.14658 2.85237C2.05677 2.75784 2.00744 2.63197 2.00911 2.50159C2.01077 2.37121 2.06331 2.24664 2.15551 2.15443C2.24771 2.06223 2.37228 2.0097 2.50266 2.00803C2.63304 2.00636 2.75892 2.05569 2.85345 2.1455L13.8534 13.1455C13.9233 13.2154 13.9709 13.3045 13.9902 13.4015C14.0095 13.4984 13.9996 13.5989 13.9617 13.6902C13.9239 13.7816 13.8599 13.8597 13.7777 13.9146C13.6955 13.9695 13.5989 13.9989 13.5 13.9989ZM7.98939 11.9989C6.69282 11.9989 5.44251 11.6152 4.27314 10.8583C3.20845 10.1708 2.25001 9.18612 1.50126 8.01456V8.01206C2.12439 7.11925 2.80689 6.36425 3.54001 5.7555C3.54664 5.74995 3.55205 5.74309 3.5559 5.73535C3.55975 5.72761 3.56194 5.71916 3.56235 5.71052C3.56277 5.70189 3.56138 5.69326 3.55829 5.68519C3.5552 5.67712 3.55046 5.66977 3.54439 5.66362L2.92189 5.04206C2.91083 5.03091 2.89597 5.02433 2.88028 5.02363C2.86458 5.02294 2.8492 5.02818 2.8372 5.03831C2.05845 5.69456 1.33564 6.49956 0.67845 7.44206C0.565383 7.60434 0.503113 7.79658 0.499551 7.99433C0.49599 8.19209 0.551299 8.38644 0.65845 8.55269C1.48376 9.84425 2.54595 10.9321 3.7297 11.698C5.06251 12.5614 6.49689 12.9989 7.98939 12.9989C8.795 12.9964 9.59491 12.8637 10.3581 12.6058C10.3682 12.6024 10.3772 12.5965 10.3844 12.5886C10.3915 12.5807 10.3965 12.5711 10.3989 12.5608C10.4013 12.5504 10.4011 12.5396 10.3981 12.5294C10.3952 12.5192 10.3897 12.5099 10.3822 12.5024L9.70782 11.828C9.6923 11.8129 9.6731 11.802 9.65212 11.7965C9.63113 11.7911 9.60909 11.7911 9.58814 11.7967C9.06587 11.9312 8.52869 11.9992 7.98939 11.9989ZM15.3388 7.45519C14.5119 6.17644 13.4391 5.09019 12.2366 4.31362C10.9063 3.45362 9.43751 2.99894 7.98939 2.99894C7.19232 3.00035 6.40116 3.13589 5.64908 3.39987C5.63905 3.40336 5.63009 3.40934 5.62302 3.41725C5.61595 3.42516 5.61101 3.43474 5.60866 3.44509C5.60632 3.45544 5.60664 3.46621 5.60961 3.4764C5.61258 3.48658 5.6181 3.49585 5.62564 3.50331L6.29908 4.17675C6.31475 4.19216 6.33422 4.20316 6.3555 4.20865C6.37679 4.21414 6.39915 4.21391 6.42032 4.208C6.93188 4.06962 7.45945 3.99932 7.98939 3.99894C9.26095 3.99894 10.5075 4.38737 11.6941 5.15519C12.7788 5.85519 13.7485 6.83894 14.4991 7.99894C14.4996 7.99965 14.4999 8.00053 14.4999 8.00144C14.4999 8.00234 14.4996 8.00322 14.4991 8.00394C13.9542 8.86173 13.2781 9.62867 12.4953 10.2767C12.4886 10.2823 12.4831 10.2891 12.4792 10.2969C12.4753 10.3046 12.4731 10.3131 12.4727 10.3218C12.4722 10.3305 12.4736 10.3392 12.4767 10.3473C12.4798 10.3554 12.4845 10.3628 12.4906 10.3689L13.1125 10.9905C13.1235 11.0016 13.1383 11.0082 13.1539 11.0089C13.1695 11.0097 13.1849 11.0046 13.1969 10.9946C14.0325 10.291 14.7558 9.46395 15.3419 8.54206C15.4455 8.37961 15.5002 8.19083 15.4997 7.99817C15.4991 7.8055 15.4433 7.61704 15.3388 7.45519Z" />
<path d="M8.00002 4.99777C7.77531 4.99765 7.5513 5.02281 7.33221 5.07277C7.32114 5.07507 7.31091 5.08033 7.30259 5.08798C7.29428 5.09563 7.28819 5.1054 7.28498 5.11624C7.28178 5.12707 7.28157 5.13858 7.28438 5.14952C7.2872 5.16047 7.29293 5.17045 7.30096 5.1784L10.8194 8.6959C10.8273 8.70393 10.8373 8.70966 10.8483 8.71247C10.8592 8.71529 10.8707 8.71508 10.8816 8.71187C10.8924 8.70867 10.9022 8.70258 10.9098 8.69427C10.9175 8.68595 10.9227 8.67571 10.925 8.66465C11.0252 8.22529 11.0251 7.769 10.9247 7.32968C10.8244 6.89036 10.6263 6.47929 10.3453 6.127C10.0643 5.77472 9.70756 5.49026 9.30153 5.29477C8.89551 5.09928 8.45066 4.99776 8.00002 4.99777ZM5.18065 7.29965C5.1727 7.29161 5.16272 7.28588 5.15177 7.28307C5.14083 7.28026 5.12932 7.28047 5.11849 7.28367C5.10765 7.28688 5.09788 7.29296 5.09023 7.30128C5.08257 7.3096 5.07732 7.31983 5.07502 7.3309C4.96169 7.82601 4.97592 8.34178 5.11638 8.82989C5.25684 9.318 5.51892 9.76245 5.87807 10.1216C6.23722 10.4807 6.68167 10.7428 7.16978 10.8833C7.65789 11.0237 8.17366 11.038 8.66877 10.9246C8.67984 10.9223 8.69007 10.9171 8.69839 10.9094C8.70671 10.9018 8.71279 10.892 8.716 10.8812C8.7192 10.8703 8.71941 10.8588 8.7166 10.8479C8.71379 10.8369 8.70806 10.827 8.70002 10.819L5.18065 7.29965Z" />
</svg>
);
};
@@ -8,6 +8,7 @@ import {
Portal,
} from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community';
import classNames from 'classnames';
const tooltipContentClasses =
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs text-black dark:text-white break-word';
@@ -18,10 +19,14 @@ export interface TooltipProps {
align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number;
noUnderline?: boolean;
}
export const TOOLTIP_TRIGGER_CLASS_NAME =
'underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed';
export const TOOLTIP_TRIGGER_CLASS_NAME = (noUnderline?: boolean) =>
classNames(
{ 'underline underline-offset-2': !noUnderline },
'decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed'
);
// Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({
@@ -31,11 +36,12 @@ export const Tooltip = ({
sideOffset,
align = 'start',
side = 'bottom',
noUnderline,
}: TooltipProps) =>
description ? (
<Provider delayDuration={200} skipDelayDuration={100}>
<Root open={open}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME(noUnderline)}>
{children}
</Trigger>
{description && (
@@ -6,6 +6,7 @@ import type {
ReactNode,
} from 'react';
import { Intent } from '../../utils/intent';
import { Link } from 'react-router-dom';
type TradingButtonProps = {
size?: 'large' | 'medium' | 'small' | 'extra-small';
@@ -119,30 +120,22 @@ export const TradingButton = forwardRef<
)
);
export const TradingAnchorButton = forwardRef<
HTMLAnchorElement,
AnchorHTMLAttributes<HTMLAnchorElement> & TradingButtonProps
>(
(
{
size = 'medium',
intent = Intent.None,
icon,
href,
children,
className,
subLabel,
...props
},
ref
) => (
<a
ref={ref}
href={href}
className={getClassName({ size, subLabel, intent }, className)}
{...props}
>
<Content icon={icon} subLabel={subLabel} children={children} />
</a>
)
export const TradingAnchorButton = ({
size = 'medium',
intent = Intent.None,
icon,
href,
children,
className,
subLabel,
...props
}: AnchorHTMLAttributes<HTMLAnchorElement> &
TradingButtonProps & { href: string }) => (
<Link
to={href}
className={getClassName({ size, subLabel, intent }, className)}
{...props}
>
<Content icon={icon} subLabel={subLabel} children={children} />
</Link>
);
+1 -1
View File
@@ -70,7 +70,7 @@
"jsondiffpatch": "^0.4.1",
"lodash": "^4.17.21",
"next": "13.3.0",
"pennant": "1.11.1",
"pennant": "1.12.0",
"react": "18.2.0",
"react-copy-to-clipboard": "^5.0.4",
"react-dom": "18.2.0",
+4 -4
View File
@@ -20253,10 +20253,10 @@ pend@~1.2.0:
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
pennant@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.11.1.tgz#f47bceaade01db215eeba666cd755840d16fe9ed"
integrity sha512-U26OxjxETWLJAvCFj20oH0y5gzyBfJ5oD4O3dYg1aWnH4giUYDPWuUDFnzlMJ+yXolyYECccXm6S1BkoVcVzDg==
pennant@1.12.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.12.0.tgz#e12707d5f1aac554d81bad060637e608335e0b50"
integrity sha512-xosg5erRf+Ke9iORdqyv+SOGcD3uJX1dgf990q1DvHuzz36w2txCZsfnvcXhRO++HYVKS9sU/YLPRLJgolFGtA==
dependencies:
"@babel/runtime" "^7.13.10"
"@d3fc/d3fc-technical-indicator" "^8.0.1"