Compare commits

...
Author SHA1 Message Date
Matthew Russell bc758e90c8 chore: add specs 2023-08-07 08:40:02 +01:00
Matthew Russell a70a7bcc8e fix(trading): dont show connect wallet message in deal ticket (#4482) 2023-08-04 11:09:43 +01:00
Matthew Russell 4461a6a497 fix(trading): remove hover state from disabled view as button (#4487) 2023-08-04 11:08:48 +01:00
Matthew Russell 1041864ad8 chore(trading): remove unnecessary forwarding of grid refs (#4486) 2023-08-04 11:08:28 +01:00
Matthew Russell 015e0188ec chore: enable iceberg orders 2023-08-03 12:38:51 +01:00
Matthew Russell 228236c152 chore(deal-ticket): type dropdown icon (#4468) 2023-08-03 12:15:21 +01:00
Art cd6e906dc6 feat(trading): view as button in sidebar (#4470) 2023-08-03 12:14:35 +01:00
Maciek 8a47a92dbd fix(trading): add info splashes on deposit and withdrawals list containers (#4479) 2023-08-03 12:01:42 +01:00
Matthew Russell af14c27df9 chore(trading): responsive welcome dialog content and content update (#4475) 2023-08-03 12:01:09 +01:00
Matthew Russell 38edcd88c0 chore(trading): fix alignment of expire (#4477) 2023-08-03 12:00:12 +01:00
m.ray a74cf02030 feat(trading): order details dialog (#4281) 2023-08-03 10:05:12 +00:00
Joe Tsang 6686755763 chore(governance): fix failing tests from nightly run (#4453) 2023-08-03 10:01:00 +01:00
Joe Tsang 79cf8163be chore(governance): add e2e test for successor market (#4471) 2023-08-03 09:54:51 +01:00
Matthew Russell 1688e26f4b feat(trading): add fairground texture background to logo (#4474) 2023-08-03 09:28:01 +01:00
Art 5e101a8eaf feat(trading): deposit page (#4472) 2023-08-02 19:06:49 +02:00
Matthew Russell 65071f57d0 fix(trading,governance): eager connection for browser wallet (#4444) 2023-08-02 17:16:09 +01:00
daro-maj b87c981a90 test(trading): fix for key-to-key test (#4469) 2023-08-02 18:11:31 +02:00
Joe TsangandMatthew Russell b509e5b347 test(explorer): e2e proposal test (#4379)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-02 16:37:55 +01:00
Maciek bb9fce4831 feat(trading): 4322 get started steps (#4440) 2023-08-02 15:34:04 +00:00
Matthew Russell 918825aca8 chore: adjust spacing and text size of long/short type selectors 2023-08-02 15:36:39 +01:00
Matthew Russell 74bf183fad feat(trading): 3114 pane context (#4442) 2023-08-02 15:29:41 +01:00
m.ray 674baa1ee3 fix(trading): iceberg order fills (#4460) 2023-08-02 16:55:49 +03:00
Matthew Russell d18a0c7e15 fix(trading): lp table forward ref breaking resize grid (#4447) 2023-08-02 13:59:05 +01:00
m.ray 76a6005b77 fix(trading): replace all markets with open markets (#4452) 2023-08-02 13:58:26 +01:00
JonRay15andMatthew Russell 79414614c2 chore(trading,governance,explorer): fix issue template (#4459)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-08-02 12:52:51 +01:00
164 changed files with 6195 additions and 2414 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
name: Feature Epic name: Feature Epic
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
title: 'Epic: ' title: 'Epic: '
description: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
labels: feature-epic labels: feature-epic
--- ---
@@ -0,0 +1,46 @@
context('Proposal page', { tags: '@smoke' }, function () {
describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
const proposalTitle = 'Add Lorem Ipsum market';
before('Create market proposal', function () {
cy.visit('/');
cy.createMarket();
});
it('Able to view proposal', function () {
cy.navigate_to('governanceProposals');
cy.getByTestId(proposalHeading).should('be.visible');
// get first proposal in list
cy.get('[row-index="0"]').within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.get('[col-id="eDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contains', 'https://governance.fairground.wtf/proposals/');
cy.contains('View terms').should('exist').click();
});
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
cy.get('.language-json').should('exist');
});
it('Proposal page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.navigate_to('governanceProposals', true);
cy.getByTestId(proposalHeading).should('be.visible');
cy.get('[row-index="0"]').within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
});
});
});
});
@@ -0,0 +1,107 @@
{
"changes": {
"decimalPlaces": "5",
"positionDecimalPlaces": "5",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"lpPriceRange": "10",
"instrument": {
"name": "Token test market",
"code": "Token.24h",
"future": {
"settlementAsset": "816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc",
"quoteName": "fUSDC",
"dataSourceSpecForSettlementData": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "prices.BTC.value",
"type": "TYPE_INTEGER",
"numberDecimalPlaces": "0"
},
"conditions": [
{
"operator": "OPERATOR_GREATER_THAN",
"value": "0"
}
]
}
]
}
}
},
"dataSourceSpecForTradingTermination": {
"external": {
"oracle": {
"signers": [
{
"pubKey": {
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
}
}
],
"filters": [
{
"key": {
"name": "trading.terminated.ETH5",
"type": "TYPE_BOOLEAN"
},
"conditions": [
{
"operator": "OPERATOR_EQUALS",
"value": "true"
}
]
}
]
}
}
},
"dataSourceSpecBinding": {
"settlementDataProperty": "prices.BTC.value",
"tradingTerminationProperty": "trading.terminated.ETH5"
}
}
},
"metadata": ["sector:food", "sector:materials", "source:docs.vega.xyz"],
"priceMonitoringParameters": {
"triggers": [
{
"horizon": "43200",
"probability": "0.9999999",
"auctionExtension": "600"
}
]
},
"liquidityMonitoringParameters": {
"targetStakeParameters": {
"timeWindow": "3600",
"scalingFactor": 10
},
"triggeringRatio": "0.7",
"auctionExtension": "1"
},
"logNormal": {
"tau": 0.0001140771161,
"riskAversionParameter": 0.001,
"params": {
"mu": 0,
"r": 0.016,
"sigma": 0.8
}
},
"successor": {
"parentMarketId": "",
"insurancePoolFraction": "0.75"
}
}
}
@@ -220,7 +220,7 @@ context(
function () { function () {
const proposalTitle = 'Test new market proposal'; const proposalTitle = 'Test new market proposal';
goToMakeNewProposal(governanceProposalType.NEW_MARKET); goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type('Test new market proposal'); cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type('E2E test for proposals'); cy.getByTestId(newProposalDescription).type('E2E test for proposals');
cy.fixture('/proposals/new-market').then((newMarketProposal) => { cy.fixture('/proposals/new-market').then((newMarketProposal) => {
const newMarketPayload = JSON.stringify(newMarketProposal); const newMarketPayload = JSON.stringify(newMarketProposal);
@@ -606,6 +606,94 @@ context(
}); });
}); });
it('able to submit successor market proposal', function () {
const proposalTitle = 'Test successor market proposal';
cy.createMarket();
cy.reload();
waitForSpinner();
cy.getByTestId('closed-proposals').within(() => {
cy.contains('Add Lorem Ipsum market')
.parentsUntil(proposalListItem)
.last()
.within(() => {
cy.getByTestId(viewProposalBtn).click();
});
});
getProposalInformationFromTable('ID').invoke('text').as('parentMarketId');
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
cy.getByTestId(newProposalTitle).type(proposalTitle);
cy.getByTestId(newProposalDescription).type(
'E2E test for successor market'
);
cy.fixture('/proposals/successor-market').then((newMarketProposal) => {
newMarketProposal.changes.successor.parentMarketId =
this.parentMarketId;
const newMarketPayload = JSON.stringify(newMarketProposal);
cy.getByTestId(newProposalTerms).type(newMarketPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
cy.getByTestId(proposalDownloadBtn)
.should('be.visible')
.click()
.then(() => {
cy.wrap(
getDownloadedProposalJsonPath('vega-new-market-proposal-')
).then((filePath) => {
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({ proposalBody: filePath });
});
});
navigateTo(navigation.proposals);
getProposalFromTitle(proposalTitle).within(() => {
// 3003-PMAN-008
cy.getByTestId('proposal-successor-info')
.should('have.text', 'Successor market to: TEST.24h')
.find('a')
.should('have.attr', 'href')
.and('contain', this.parentMarketId);
cy.getByTestId('view-proposal-btn').click();
});
// #3003-PMAN-010
cy.getByTestId(proposalJsonToggle).click();
cy.get('.language-json').within(() => {
cy.get('.hljs-attr').should('contain.text', 'parentMarketId');
cy.get('.hljs-string').should('contain.text', this.parentMarketId);
cy.get('.hljs-attr').should('contain.text', 'insurancePoolFraction');
cy.get('.hljs-string').should('contain.text', '0.75');
});
cy.getByTestId('proposal-market-data').within(() => {
cy.getByTestId('proposal-market-data-toggle').click();
cy.contains('Key details').click();
// 3003-PMAN-009
getMarketProposalDetailsFromTable('Parent Market ID').should(
'have.text',
this.parentMarketId
);
getMarketProposalDetailsFromTable('Insurance Pool Fraction').should(
'have.text',
'0.75'
);
getMarketProposalDetailsFromTable('Trading Mode').should(
'have.text',
'No trading'
);
});
// 3003-PMAN-011
cy.contains('Parent Market ID').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The ID of the market this market succeeds.'
);
cy.contains('Insurance Pool Fraction').realHover();
cy.getByTestId('tooltip-content').should(
'contain.text',
'The fraction of the insurance pool balance that is carried over from the parent market to the successor.'
);
});
after('Disassociate from second wallet key if present', function () { after('Disassociate from second wallet key if present', function () {
cy.reload(); cy.reload();
waitForSpinner(); waitForSpinner();
@@ -96,12 +96,14 @@ context('rewards - flow', { tags: '@slow' }, function () {
.within(() => { .within(() => {
cy.get('h2').first().should('contain.text', 'EPOCH'); cy.get('h2').first().should('contain.text', 'EPOCH');
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega'); cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut) cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut).should(
.should('contain.text', '0.4415') 'contain.text',
.and('contain.text', '(44.15%)'); '0.4415'
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE') );
.should('contain.text', '0.0004') cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
.and('contain.text', '(44.15%)'); 'contain.text',
'0.0004'
);
cy.getByTestId('total').should('have.text', '0.4419'); cy.getByTestId('total').should('have.text', '0.4419');
}); });
}); });
@@ -339,7 +339,7 @@ context(
cy.getByTestId(vegaWalletCurrencyTitle) cy.getByTestId(vegaWalletCurrencyTitle)
.contains(name) .contains(name)
.parent() .parent()
.siblings() .siblings(txTimeout)
.should((elementAmount) => { .should((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text()); const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount); expect(displayedAmount).be.gte(expectedAmount);
@@ -107,7 +107,7 @@ export function dissociateFromSecondWalletKey() {
cy.getByTestId('vega-in-wallet') cy.getByTestId('vega-in-wallet')
.first() .first()
.within(() => { .within(() => {
cy.getByTestId('eth-wallet-associated-balances') cy.getByTestId('eth-wallet-associated-balances', txTimeout)
.last() .last()
.within(() => { .within(() => {
cy.getByTestId('associated-key') cy.getByTestId('associated-key')
@@ -78,13 +78,21 @@ export function stakingPageAssociateTokens(
} }
cy.get(tokenAmountInputBox, epochTimeout).type(amount); cy.get(tokenAmountInputBox, epochTimeout).type(amount);
if (approve) { if (approve) {
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click(); cy.getByTestId('wallet-associate').then((walletAssociateField) => {
cy.contains('Approve $VEGA Tokens for staking on Vega').should( if (
'be.visible' walletAssociateField.find('[data-testid="token-input-approve-button"]')
); .length
cy.contains('Approve $VEGA Tokens for staking on Vega', txTimeout).should( ) {
'not.exist' cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
); cy.contains('Approve $VEGA Tokens for staking on Vega').should(
'be.visible'
);
cy.contains(
'Approve $VEGA Tokens for staking on Vega',
txTimeout
).should('not.exist');
}
});
} }
cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click(); cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click();
@@ -200,7 +200,7 @@ export const ProposalMarketData = ({
title={marketData.tradableInstrument.instrument.code} title={marketData.tradableInstrument.instrument.code}
open={isOpen} open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())} onChange={(isOpen) => (isOpen ? open() : close())}
size="medium" size="large"
dataTestId="market-json-dialog" dataTestId="market-json-dialog"
> >
<CopyWithTooltip text={JSON.stringify(marketData)}> <CopyWithTooltip text={JSON.stringify(marketData)}>
@@ -57,7 +57,7 @@ const ROWS = [
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => { export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
return ( return (
<Dialog size="medium" open={isOpen} onChange={onChange}> <Dialog size="large" open={isOpen} onChange={onChange}>
<h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase"> <h1 className="text-2xl mb-5 pr-2 font-medium font-alpha uppercase">
{t('Health')} {t('Health')}
</h1> </h1>
@@ -2,6 +2,7 @@ describe('charts', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
cy.getByTestId('Depth').click(); cy.getByTestId('Depth').click();
@@ -202,6 +202,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
const specDataConnection = createDataConnection(); const specDataConnection = createDataConnection();
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => { cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery()); aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery()); aliasGQLQuery(req, 'Statistics', statisticsQuery());
@@ -455,6 +456,7 @@ describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click(); cy.get('[data-testid="Closed markets"]').click();
}); });
@@ -4,6 +4,7 @@ const nodeHealthTrigger = 'node-health-trigger';
describe('home', { tags: '@regression' }, () => { describe('home', { tags: '@regression' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/'); cy.visit('/');
+38 -37
View File
@@ -2,7 +2,7 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
const dialogContent = 'dialog-content'; const dialogContent = 'welcome-dialog';
const generateProposal = (code: string): ProposalListFieldsFragment => ({ const generateProposal = (code: string): ProposalListFieldsFragment => ({
__typename: 'Proposal', __typename: 'Proposal',
@@ -140,43 +140,43 @@ describe('home', { tags: '@regression' }, () => {
cy.wait('@MarketsData'); cy.wait('@MarketsData');
}); });
it('redirects to market/all and displays welcome notice', () => { it('close welcome dialog should redirect to market/all', () => {
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`); cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.getByTestId('welcome-notice-title').should( cy.getByTestId('welcome-dialog').should('be.visible');
'contain.text', cy.getByTestId('welcome-title').should('contain.text', 'Console CUSTOM');
'Welcome to Console' cy.getByTestId('browse-markets-button').should('not.be.disabled');
); cy.getByTestId('get-started-banner').should('be.visible');
}); cy.getByTestId('get-started-button').should('not.be.disabled');
}); cy.getByTestId('dialog-close').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
describe('no proposal nor markets found', () => { cy.window().then((window) => {
it('there are welcome text and a link to propose market', () => { expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
cy.mockGQL((req) => { 'true'
const data = { );
marketsConnection: {
__typename: 'MarketConnection',
edges: [],
},
};
aliasGQLQuery(req, 'Markets', data);
aliasGQLQuery(req, 'MarketsData', data);
aliasGQLQuery(req, 'ProposalsList', {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: null,
},
});
}); });
cy.visit('/'); });
cy.wait('@Markets');
cy.wait('@MarketsData'); it('click browse markets button should redirect to market/all', () => {
cy.getByTestId('welcome-notice-title').should( cy.getByTestId('welcome-dialog').should('be.visible');
'contain.text', cy.getByTestId('browse-markets-button').click();
'Welcome to Console' cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
); cy.window().then((window) => {
cy.getByTestId('external-link') expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
.contains('Propose a market') 'true'
.should('exist'); );
});
});
it('click get started button should open connect dialog', () => {
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('get-started-button').click();
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets/all`);
cy.window().then((window) => {
expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
'true'
);
});
cy.getByTestId('wallet-dialog-title').should('contain.text', 'Connect');
}); });
}); });
@@ -185,7 +185,7 @@ describe('home', { tags: '@regression' }, () => {
cy.window().then((window) => { cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1'); window.localStorage.setItem('marketId', 'market-1');
cy.visit('/'); cy.visit('/');
cy.wait('@Markets'); cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-1'); cy.location('hash').should('equal', '#/markets/market-1');
cy.getByTestId(dialogContent).should('not.exist'); cy.getByTestId(dialogContent).should('not.exist');
}); });
@@ -199,6 +199,7 @@ describe('home', { tags: '@regression' }, () => {
}); });
cy.visit('/'); cy.visit('/');
cy.wait('@Markets'); cy.wait('@Markets');
cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-not-existing'); cy.location('hash').should('equal', '#/markets/market-not-existing');
cy.getByTestId(dialogContent).should('not.exist'); cy.getByTestId(dialogContent).should('not.exist');
}); });
@@ -3,12 +3,13 @@ import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
const rowSelector = const rowSelector =
'[data-testid="tab-all-markets"] .ag-center-cols-container .ag-row'; '[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]'; const colInstrumentCode = '[col-id="tradableInstrument.instrument.code"]';
describe('markets all table', { tags: '@smoke' }, () => { describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => { beforeEach(() => {
cy.clearLocalStorage().then(() => { cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -34,7 +35,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'Settlement asset', 'Settlement asset',
'', '',
]; ];
cy.getByTestId('tab-all-markets').within(($headers) => { cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers) cy.wrap($headers)
.get('.ag-header-cell-text') .get('.ag-header-cell-text')
.each(($header, i) => { .each(($header, i) => {
@@ -44,7 +45,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
}); });
it('markets tab should be rendered properly', () => { it('markets tab should be rendered properly', () => {
cy.get('[data-testid="All markets"]').should( cy.get('[data-testid="Open markets"]').should(
'have.attr', 'have.attr',
'data-state', 'data-state',
'active' 'active'
@@ -166,7 +167,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'ETHBTC.QM21', 'ETHBTC.QM21',
'SOLUSD', 'SOLUSD',
]; ];
cy.get('[data-testid="All markets"]').click({ force: true }); cy.get('[data-testid="Open markets"]').click({ force: true });
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all'); cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
cy.contains('AAPL.MF21').should('be.visible'); cy.contains('AAPL.MF21').should('be.visible');
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
@@ -191,7 +192,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
}); });
}); });
describe('no all markets', { tags: '@smoke', testIsolation: true }, () => { describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
const markets: MarketsQuery = {}; const markets: MarketsQuery = {};
@@ -199,11 +200,12 @@ describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'Markets', markets); aliasGQLQuery(req, 'Markets', markets);
}); });
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
}); });
it.skip('can see no markets message', () => { it.skip('can see no markets message', () => {
// 6001-MARK-048 // 6001-MARK-048
cy.getByTestId('tab-all-markets').should('contain.text', 'No markets'); cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
}); });
}); });
@@ -21,6 +21,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
}); });
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(MarketState.STATE_ACTIVE); cy.mockTradingPage(MarketState.STATE_ACTIVE);
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
@@ -36,6 +36,7 @@ const headers = [
describe('liquidity table - trading', { tags: '@smoke' }, () => { describe('liquidity table - trading', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription(); cy.mockSubscription();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
@@ -119,6 +120,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
describe('liquidity table view', { tags: '@smoke' }, () => { describe('liquidity table view', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription(); cy.mockSubscription();
cy.mockTradingPage( cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE, Schema.MarketState.STATE_ACTIVE,
@@ -12,6 +12,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.window().then((window) => { cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1'); window.localStorage.setItem('marketId', 'market-1');
}); });
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
MarketState.STATE_ACTIVE, MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION, MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -30,6 +30,7 @@ describe('Market trading page', () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
); );
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@MarketData'); cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible'); cy.getByTestId(marketSummaryBlock).should('be.visible');
@@ -9,6 +9,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click(); cy.get('[data-testid="Proposed markets"]').click();
}); });
@@ -213,6 +214,7 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'ProposalsList', proposal); aliasGQLQuery(req, 'ProposalsList', proposal);
}); });
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
}); });
it('can see no markets message', () => { it('can see no markets message', () => {
@@ -12,6 +12,7 @@ describe('markets table', { tags: '@smoke' }, () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
); );
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all'); cy.visit('/#/markets/all');
}); });
}); });
@@ -6,6 +6,7 @@ const oracleFullProfile = 'oracle-full-profile';
describe('oracle information', { tags: '@smoke' }, () => { describe('oracle information', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage( cy.mockTradingPage(
MarketState.STATE_ACTIVE, MarketState.STATE_ACTIVE,
undefined, undefined,
@@ -14,6 +14,7 @@ const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => { describe('order book', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
@@ -4,6 +4,7 @@ describe('Settings page', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/'); cy.visit('/');
// Only click if not already active otherwise sidebar will close // Only click if not already active otherwise sidebar will close
@@ -77,6 +77,7 @@ function getButtonSelectorByText(text: string): string {
beforeEach(() => { beforeEach(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -14,6 +14,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.clearAllLocalStorage(); cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -21,9 +22,8 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
it('must show place order button and connect wallet if wallet is not connected', () => { it('must show place order button and connect wallet if wallet is not connected', () => {
// 0003-WTXN-001 // 0003-WTXN-001
cy.getByTestId('connect-vega-wallet'); // Not connected cy.getByTestId('connect-vega-wallet'); // Not connected
cy.getByTestId('order-connect-wallet').should('exist');
cy.getByTestId(placeOrderBtn).should('exist'); cy.getByTestId(placeOrderBtn).should('exist');
cy.getByTestId('deal-ticket-connect-wallet').should('exist'); cy.getByTestId('get-started-button').should('exist');
}); });
it('must be able to select order direction - long/short', function () { it('must be able to select order direction - long/short', function () {
@@ -44,7 +44,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
mockConnectWallet(); mockConnectWallet();
cy.getByTestId(toggleLimit).click(); cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).clear().type('101'); cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click(); cy.getByTestId('get-started-button').click();
cy.getByTestId('dialog-content').should('be.visible'); cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list') cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]') .find('[data-testid="connector-jsonRpc"]')
@@ -15,6 +15,7 @@ describe('time in force validation', { tags: '@smoke' }, () => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.clearAllLocalStorage(); cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0'); cy.visit('/#/markets/market-0');
cy.wait('@Markets'); cy.wait('@Markets');
}); });
@@ -9,6 +9,7 @@ describe('trades', { tags: '@smoke' }, () => {
before(() => { before(() => {
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.intercept('POST', '/graphql', (req) => { cy.intercept('POST', '/graphql', (req) => {
if (req.body.operationName === 'Trades') { if (req.body.operationName === 'Trades') {
req.alias = '@Trades'; req.alias = '@Trades';
@@ -17,6 +17,7 @@ describe(
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
}); });
@@ -122,6 +123,7 @@ describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.visit('/#/portfolio'); cy.visit('/#/portfolio');
cy.mockTradingPage(); cy.mockTradingPage();
cy.mockSubscription(); cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist'); cy.get('[data-testid="pathname-/portfolio"]').should('exist');
}); });
@@ -0,0 +1,191 @@
import { selectAsset } from '../support/helpers';
const amountField = 'input[name="amount"]';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2;
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
@@ -5,206 +5,15 @@ const amountShortName = 'input[name="amount"] + div + span.text-xs';
const assetSelection = 'select-asset'; const assetSelection = 'select-asset';
const assetBalance = 'asset-balance'; const assetBalance = 'asset-balance';
const assetOption = 'rich-select-option'; const assetOption = 'rich-select-option';
const transferText = 'transfer-intro-text';
const errorText = 'input-error-text';
const formFieldError = 'input-error-text';
const includeTransferFeeRadioBtn = 'include-transfer-fee';
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
const manageVegaWallet = 'manage-vega-wallet';
const openTransferButton = 'open-transfer'; const openTransferButton = 'open-transfer';
const submitTransferBtn = '[type="submit"]'; const submitTransferBtn = '[type="submit"]';
const toAddressField = '[name="toAddress"]'; const toAddressField = '[name="toAddress"]';
const totalTransferfee = 'total-transfer-fee';
const transferAmount = 'transfer-amount';
const transferForm = 'transfer-form'; const transferForm = 'transfer-form';
const transferFee = 'transfer-fee';
const walletTransfer = 'wallet-transfer';
const ASSET_EURO = 1;
const ASSET_SEPOLIA_TBTC = 2; const ASSET_SEPOLIA_TBTC = 2;
const collateralTab = 'Collateral'; const collateralTab = 'Collateral';
const toastCloseBtn = 'toast-close'; const toastCloseBtn = 'toast-close';
const toastContent = 'toast-content'; const toastContent = 'toast-content';
describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Assets');
cy.wait('@Accounts');
cy.mockVegaWalletTransaction();
// Only click if not already active otherwise sidebar will close
cy.get('[data-testid="sidebar-content"]').then(($sidebarContent) => {
if ($sidebarContent.find('h2').text() !== 'Transfer') {
cy.get('[data-testid="sidebar"] [data-testid="Transfer"]').click();
}
});
});
it('transfer fees tooltips', () => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
/// Check Include Transfer Fee tooltip
cy.get('label[for="include-transfer-fee"] div').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Transfer Fee tooltip
cy.contains('div', 'Transfer fee').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Amount to be transferred tooltip
cy.contains('div', 'Amount to be transferred').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
//Check Total amount (with fee) tooltip
cy.contains('div', 'Total amount (with fee)').realHover();
cy.get('[data-side="bottom"] div')
.should('be.visible')
.should('not.be.empty');
});
it('transfer fees', () => {
// 1003-TRAN-020
// 1003-TRAN-021
// 1003-TRAN-022
// 1003-TRAN-023
cy.getByTestId(transferForm);
cy.contains('Enter manually').click();
cy.getByTestId(transferForm)
.find(toAddressField)
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
selectAsset(ASSET_SEPOLIA_TBTC);
cy.getByTestId(includeTransferFeeRadioBtn).should('be.disabled');
cy.getByTestId(transferForm)
.find(amountField)
.type('1', { delay: 100, force: true });
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '1.00');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.01');
cy.getByTestId(includeTransferFeeRadioBtn).click();
cy.getByTestId(transferFee)
.should('be.visible')
.should('contain.text', '0.01');
cy.getByTestId(transferAmount)
.should('be.visible')
.should('contain.text', '0.99');
cy.getByTestId(totalTransferfee)
.should('be.visible')
.should('contain.text', '1.00');
});
});
describe(
'transfer form validation',
{ tags: '@regression', testIsolation: true },
() => {
beforeEach(() => {
cy.mockWeb3Provider();
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.getByTestId(manageVegaWallet).click();
cy.getByTestId(walletTransfer).click();
cy.wait('@Accounts');
cy.wait('@Assets');
});
it('transfer Text', () => {
// 1003-TRAN-003
cy.getByTestId(transferText)
.should('exist')
.get(keyID)
.invoke('text')
.should('match', /[\w.]{6}…[\w.]{6}/);
});
it('invalid vega key validation', () => {
//1003-TRAN-013
//1003-TRAN-004
cy.getByTestId(transferForm).should('be.visible');
cy.contains('Enter manually').click();
cy.getByTestId(transferForm).find(toAddressField).type('asd');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
cy.contains('label', 'Vega key').should('be.visible');
cy.contains('label', 'Asset').should('be.visible');
cy.contains('label', 'Amount').should('be.visible');
});
it('empty fields', () => {
// 1003-TRAN-012
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(formFieldError).should('contain.text', 'Required');
cy.getByTestId(formFieldError).should('have.length', 3);
});
it('min amount', () => {
// 1002-WITH-010
// 1003-TRAN-014
selectAsset(ASSET_SEPOLIA_TBTC);
cy.get(amountField).clear().type('0');
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'Value is below minimum'
);
});
it('max amount', () => {
// 1003-TRAN-002
// 1003-TRAN-011
// 1003-TRAN-002
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
cy.get(amountField).clear().type('1001', { delay: 100 });
cy.getByTestId(transferForm).find(submitTransferBtn).click();
cy.getByTestId(errorText).should(
'contain.text',
'You cannot transfer more than your available collateral'
);
});
}
);
describe('withdraw actions', { tags: '@smoke', testIsolation: true }, () => { describe('withdraw actions', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => { beforeEach(() => {
cy.mockWeb3Provider(); cy.mockWeb3Provider();
+1 -1
View File
@@ -18,5 +18,5 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
@@ -0,0 +1,19 @@
import { DepositContainer } from '@vegaprotocol/deposits';
import { GetStarted } from '../../components/welcome-dialog';
import { t } from '@vegaprotocol/i18n';
export const Deposit = () => {
return (
<div className="py-16 px-8 flex w-full justify-center">
<div className="lg:min-w-[700px] min-w-[300px] max-w-[700px]">
<h1 className="text-4xl xl:text-5xl uppercase font-alpha calt">
{t('Deposit')}
</h1>
<div className="mt-10">
<DepositContainer />
<GetStarted />
</div>
</div>
</div>
);
};
@@ -0,0 +1,3 @@
import { Deposit } from './deposit';
export default Deposit;
-1
View File
@@ -23,7 +23,6 @@ export const Home = () => {
replace: true, replace: true,
}); });
} else if (data) { } else if (data) {
update({ shouldDisplayWelcomeDialog: true });
const marketDataId = data[0]?.id; const marketDataId = data[0]?.id;
if (marketDataId) { if (marketDataId) {
navigate(Links[Routes.MARKET](marketDataId), { navigate(Links[Routes.MARKET](marketDataId), {
+31 -44
View File
@@ -10,7 +10,6 @@ import type { Market } from '@vegaprotocol/markets';
import { Filter } from '@vegaprotocol/orders'; import { Filter } from '@vegaprotocol/orders';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler'; import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { import {
ResizableGrid, ResizableGrid,
ResizableGridPanel, ResizableGridPanel,
@@ -58,7 +57,11 @@ const MainGrid = memo(
> >
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-main-left"> <Tabs storageKey="console-trade-grid-main-left">
<Tab id="chart" name={t('Chart')}> <Tab
id="chart"
name={t('Chart')}
menu={<TradingViews.candles.menu />}
>
<TradingViews.candles.component marketId={marketId} /> <TradingViews.candles.component marketId={marketId} />
</Tab> </Tab>
<Tab id="depth" name={t('Depth')}> <Tab id="depth" name={t('Depth')}>
@@ -95,64 +98,48 @@ const MainGrid = memo(
<TradeGridChild> <TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom"> <Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <TradingViews.positions.component
<TradingViews.positions.component onMarketClick={onMarketClick}
onMarketClick={onMarketClick} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="open-orders" name={t('Open')}> <Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Open}
filter={Filter.Open} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="closed-orders" name={t('Closed')}> <Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Closed}
filter={Filter.Closed} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="rejected-orders" name={t('Rejected')}> <Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer> <TradingViews.orders.component
<TradingViews.orders.component marketId={marketId}
marketId={marketId} filter={Filter.Rejected}
filter={Filter.Rejected} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="orders" name={t('All')}> <Tab id="orders" name={t('All')}>
<VegaWalletContainer> <TradingViews.orders.component marketId={marketId} />
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
</Tab> </Tab>
{FLAGS.STOP_ORDERS ? ( {FLAGS.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}> <Tab id="stop-orders" name={t('Stop orders')}>
<VegaWalletContainer> <TradingViews.stopOrders.component />
<TradingViews.stopOrders.component />
</VegaWalletContainer>
</Tab> </Tab>
) : null} ) : null}
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <TradingViews.fills.component
<TradingViews.fills.component marketId={marketId}
marketId={marketId} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} />
/>
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="accounts" name={t('Collateral')}> <Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer> <TradingViews.collateral.component
<TradingViews.collateral.component pinnedAsset={pinnedAsset}
pinnedAsset={pinnedAsset} onMarketClick={onMarketClick}
onMarketClick={onMarketClick} hideButtons
hideButtons />
/>
</VegaWalletContainer>
</Tab> </Tab>
</Tabs> </Tabs>
</TradeGridChild> </TradeGridChild>
@@ -37,6 +37,7 @@ export const TradePanels = ({
const onOrderTypeClick = useMarketLiquidityClickHandler(); const onOrderTypeClick = useMarketLiquidityClickHandler();
const [view, setView] = useState<TradingView>('candles'); const [view, setView] = useState<TradingView>('candles');
const renderView = () => { const renderView = () => {
const Component = memo<{ const Component = memo<{
marketId: string; marketId: string;
@@ -65,8 +66,23 @@ export const TradePanels = ({
); );
}; };
const renderMenu = () => {
const viewCfg = TradingViews[view];
if ('menu' in viewCfg) {
const Menu = viewCfg.menu;
return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
</div>
);
}
return null;
};
return ( return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]"> <div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div> <div>
{FLAGS.SUCCESSOR_MARKETS && ( {FLAGS.SUCCESSOR_MARKETS && (
<> <>
@@ -76,6 +92,7 @@ export const TradePanels = ({
)} )}
<OracleBanner marketId={market?.id || ''} /> <OracleBanner marketId={market?.id || ''} />
</div> </div>
<div>{renderMenu()}</div>
<div className="h-full"> <div className="h-full">
<AutoSizer> <AutoSizer>
{({ width, height }) => ( {({ width, height }) => (
@@ -2,7 +2,10 @@ import type { ComponentProps } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { TradesContainer } from '@vegaprotocol/trades'; import { TradesContainer } from '@vegaprotocol/trades';
import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; import {
CandlesChartContainer,
CandlesMenu,
} from '@vegaprotocol/candles-chart';
import { Filter } from '@vegaprotocol/orders'; import { Filter } from '@vegaprotocol/orders';
import { NO_MARKET } from './constants'; import { NO_MARKET } from './constants';
import { OrderbookContainer } from '../../components/orderbook-container'; import { OrderbookContainer } from '../../components/orderbook-container';
@@ -35,6 +38,7 @@ export const TradingViews = {
candles: { candles: {
label: 'Candles', label: 'Candles',
component: requiresMarket(CandlesChartContainer), component: requiresMarket(CandlesChartContainer),
menu: CandlesMenu,
}, },
depth: { depth: {
label: 'Depth', label: 'Depth',
@@ -18,7 +18,7 @@ export const MarketsPage = () => {
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border border-default rounded-sm"> <div className="h-full my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets"> <Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}> <Tab id="open-markets" name={t('Open markets')}>
<Markets /> <Markets />
</Tab> </Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}> <Tab id="proposed-markets" name={t('Proposed markets')}>
@@ -71,7 +71,7 @@ export const AccountHistoryContainer = () => {
const { data: assets } = useAssetsDataProvider(); const { data: assets } = useAssetsDataProvider();
if (!pubKey) { if (!pubKey) {
return <Splash>{t('Connect wallet')}</Splash>; return <Splash>{t('Please connect Vega wallet')}</Splash>;
} }
return ( return (
@@ -1,15 +1,12 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { DepositsTable } from '@vegaprotocol/deposits'; import { DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits'; import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { useRef } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { useSidebar, ViewType } from '../../components/sidebar'; import { useSidebar, ViewType } from '../../components/sidebar';
export const DepositsContainer = () => { export const DepositsContainer = () => {
const gridRef = useRef<AgGridReact | null>(null);
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { data, error } = useDataProvider({ const { data, error } = useDataProvider({
dataProvider: depositsProvider, dataProvider: depositsProvider,
@@ -17,11 +14,13 @@ export const DepositsContainer = () => {
skip: !pubKey, skip: !pubKey,
}); });
const setView = useSidebar((store) => store.setView); const setView = useSidebar((store) => store.setView);
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return ( return (
<div className="h-full"> <div className="h-full">
<DepositsTable <DepositsTable
rowData={data} rowData={data}
ref={gridRef}
overlayNoRowsTemplate={error ? error.message : t('No deposits')} overlayNoRowsTemplate={error ? error.message : t('No deposits')}
/> />
{!isReadOnly && ( {!isReadOnly && (
@@ -13,7 +13,6 @@ import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container'; import { PositionsContainer } from '../../components/positions-container';
import { WithdrawalsContainer } from './withdrawals-container'; import { WithdrawalsContainer } from './withdrawals-container';
import { OrdersContainer } from '../../components/orders-container'; import { OrdersContainer } from '../../components/orders-container';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { LedgerContainer } from '../../components/ledger-container'; import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container'; import { AccountHistoryContainer } from './account-history-container';
import { import {
@@ -62,29 +61,19 @@ export const Portfolio = () => {
<PortfolioGridChild> <PortfolioGridChild>
<Tabs storageKey="console-portfolio-top"> <Tabs storageKey="console-portfolio-top">
<Tab id="account-history" name={t('Account history')}> <Tab id="account-history" name={t('Account history')}>
<VegaWalletContainer> <AccountHistoryContainer />
<AccountHistoryContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="positions" name={t('Positions')}> <Tab id="positions" name={t('Positions')}>
<VegaWalletContainer> <PositionsContainer onMarketClick={onMarketClick} allKeys />
<PositionsContainer onMarketClick={onMarketClick} allKeys />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="orders" name={t('Orders')}> <Tab id="orders" name={t('Orders')}>
<VegaWalletContainer> <OrdersContainer />
<OrdersContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="fills" name={t('Fills')}> <Tab id="fills" name={t('Fills')}>
<VegaWalletContainer> <FillsContainer onMarketClick={onMarketClick} />
<FillsContainer onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="ledger-entries" name={t('Ledger entries')}> <Tab id="ledger-entries" name={t('Ledger entries')}>
<VegaWalletContainer> <LedgerContainer />
<LedgerContainer />
</VegaWalletContainer>
</Tab> </Tab>
</Tabs> </Tabs>
</PortfolioGridChild> </PortfolioGridChild>
@@ -97,14 +86,10 @@ export const Portfolio = () => {
<PortfolioGridChild> <PortfolioGridChild>
<Tabs storageKey="console-portfolio-bottom"> <Tabs storageKey="console-portfolio-bottom">
<Tab id="collateral" name={t('Collateral')}> <Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer> <AccountsContainer />
<AccountsContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab id="deposits" name={t('Deposits')}> <Tab id="deposits" name={t('Deposits')}>
<VegaWalletContainer> <DepositsContainer />
<DepositsContainer />
</VegaWalletContainer>
</Tab> </Tab>
<Tab <Tab
id="withdrawals" id="withdrawals"
@@ -1,4 +1,4 @@
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { import {
withdrawalProvider, withdrawalProvider,
WithdrawalsTable, WithdrawalsTable,
@@ -7,7 +7,6 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { ViewType, useSidebar } from '../../components/sidebar'; import { ViewType, useSidebar } from '../../components/sidebar';
export const WithdrawalsContainer = () => { export const WithdrawalsContainer = () => {
@@ -19,9 +18,11 @@ export const WithdrawalsContainer = () => {
}); });
const setView = useSidebar((store) => store.setView); const setView = useSidebar((store) => store.setView);
const { ready, delayed } = useIncompleteWithdrawals(); const { ready, delayed } = useIncompleteWithdrawals();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return ( return (
<VegaWalletContainer> <>
<div className="h-full relative"> <div className="h-full relative">
<WithdrawalsTable <WithdrawalsTable
data-testid="withdrawals-history" data-testid="withdrawals-history"
@@ -43,6 +44,6 @@ export const WithdrawalsContainer = () => {
</Button> </Button>
</div> </div>
)} )}
</VegaWalletContainer> </>
); );
}; };
@@ -1,8 +1,8 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts'; import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager } from '@vegaprotocol/accounts'; import { AccountManager } from '@vegaprotocol/accounts';
+1 -1
View File
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
export const THROTTLE_UPDATE_TIME = 500; export const THROTTLE_UPDATE_TIME = 500;
export const RISK_ACCEPTED_KEY = 'vega_risk_accepted'; export const ONBOARDING_VIEWED_KEY = 'vega_onboarding_viewed';
export const MAINNET_WELCOME_HEADER = t( export const MAINNET_WELCOME_HEADER = t(
'Trade cash settled futures on the fully decentralised Vega network.' 'Trade cash settled futures on the fully decentralised Vega network.'
); );
@@ -1,10 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { FillsManager } from '@vegaprotocol/fills'; import { FillsManager } from '@vegaprotocol/fills';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n';
import { Splash } from '@vegaprotocol/ui-toolkit';
import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
@@ -12,10 +12,9 @@ import {
NetworkParams, NetworkParams,
useNetworkParams, useNetworkParams,
} from '@vegaprotocol/network-parameters'; } from '@vegaprotocol/network-parameters';
import type { AgGridReact } from 'ag-grid-react';
import type { DataGridSlice } from '../../stores/datagrid-store-slice'; import type { DataGridSlice } from '../../stores/datagrid-store-slice';
import { createDataGridSlice } from '../../stores/datagrid-store-slice'; import { createDataGridSlice } from '../../stores/datagrid-store-slice';
import { useEffect, useRef } from 'react'; import { useEffect } from 'react';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@@ -26,8 +25,6 @@ export const LiquidityContainer = ({
marketId: string | undefined; marketId: string | undefined;
filter?: Filter; filter?: Filter;
}) => { }) => {
const gridRef = useRef<AgGridReact | null>(null);
const gridStore = useLiquidityStore((store) => store.gridStore); const gridStore = useLiquidityStore((store) => store.gridStore);
const updateGridStore = useLiquidityStore((store) => store.updateGridStore); const updateGridStore = useLiquidityStore((store) => store.updateGridStore);
@@ -60,7 +57,6 @@ export const LiquidityContainer = ({
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LiquidityTable <LiquidityTable
ref={gridRef}
rowData={data} rowData={data}
symbol={symbol} symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces} assetDecimalPlaces={assetDecimalPlaces}
@@ -52,6 +52,9 @@ export const Navbar = ({
'bg-vega-yellow text-vega-clight-50': theme === 'yellow', 'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
'text-default': theme === 'system', 'text-default': theme === 'system',
})} })}
style={{
background: theme === 'yellow' ? 'url(/testnet-logo-bg.png' : 'none',
}}
> >
<VLogo className="w-4" /> <VLogo className="w-4" />
</NavLink> </NavLink>
@@ -1,7 +1,6 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid'; import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Filter, OrderListManager } from '@vegaprotocol/orders';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Filter } from '@vegaprotocol/orders';
import { OrderListManager } from '@vegaprotocol/orders';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
@@ -1,7 +1,15 @@
import { act, render, screen } from '@testing-library/react'; import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { Sidebar, SidebarContent, ViewType, useSidebar } from './sidebar'; import {
Sidebar,
SidebarButton,
SidebarContent,
ViewType,
useSidebar,
} from './sidebar';
import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({ jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />, NodeHealthContainer: () => <span data-testid="node-health" />,
@@ -19,16 +27,23 @@ jest.mock('../settings', () => ({
Settings: () => <div data-testid="settings" />, Settings: () => <div data-testid="settings" />,
})); }));
jest.mock('../welcome-dialog', () => ({
GetStarted: () => <div data-testid="get-started" />,
}));
describe('Sidebar', () => { describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])( it.each(['/markets/all', '/portfolio'])(
'does not render ticket and info', 'does not render ticket and info',
(path) => { (path) => {
render( render(
<MemoryRouter initialEntries={[path]}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={[path]}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -43,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => { it('renders ticket and info on market pages', () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument(); expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -61,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => { it('renders selected state', async () => {
render( render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Sidebar /> <MemoryRouter initialEntries={['/markets/ABC']}>
</MemoryRouter> <Sidebar />
</MemoryRouter>
</VegaWalletProvider>
); );
const settingsButton = screen.getByTestId(ViewType.Settings); const settingsButton = screen.getByTestId(ViewType.Settings);
@@ -87,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => { describe('SidebarContent', () => {
it('renders the correct content', () => { it('renders the correct content', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/markets/ABC']}>
<Route path="/markets/:marketId" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/markets/:marketId" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
@@ -111,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => { it('closes sidebar if market id is required but not present', () => {
const { container } = render( const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}> <VegaWalletProvider>
<Routes> <MemoryRouter initialEntries={['/portfolio']}>
<Route path="/portfolio" element={<SidebarContent />} /> <Routes>
</Routes> <Route path="/portfolio" element={<SidebarContent />} />
</MemoryRouter> </Routes>
</MemoryRouter>
</VegaWalletProvider>
); );
act(() => { act(() => {
@@ -137,3 +161,25 @@ describe('SidebarContent', () => {
expect(container).toBeEmptyDOMElement(); expect(container).toBeEmptyDOMElement();
}); });
}); });
describe('SidebarButton', () => {
it.each([ViewType.Info, ViewType.Deposit, ViewType.ViewAs])(
'runs given callback regardless of requested view (%s)',
async (view) => {
const onClick = jest.fn();
render(
<SidebarButton
icon={VegaIconNames.INFO}
tooltip="INFO"
onClick={onClick}
view={view}
/>
);
const btn = screen.getByTestId(view);
await userEvent.click(btn);
expect(onClick).toBeCalled();
}
);
});
+48 -16
View File
@@ -15,6 +15,8 @@ import { Tooltip } from '../../components/tooltip';
import { WithdrawContainer } from '../withdraw-container'; import { WithdrawContainer } from '../withdraw-container';
import { Routes as AppRoutes } from '../../pages/client-router'; import { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store'; const STORAGE_KEY = 'vega_sidebar_store';
@@ -25,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw', Withdraw = 'Withdraw',
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs',
} }
type SidebarView = type SidebarView =
@@ -52,6 +55,8 @@ type SidebarView =
export const Sidebar = () => { export const Sidebar = () => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
return ( return (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar"> <div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}> <nav className={navClasses}>
@@ -104,6 +109,16 @@ export const Sidebar = () => {
</Routes> </Routes>
</nav> </nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
/>
<SidebarButton <SidebarButton
view={ViewType.Settings} view={ViewType.Settings}
icon={VegaIconNames.COG} icon={VegaIconNames.COG}
@@ -115,25 +130,43 @@ export const Sidebar = () => {
); );
}; };
const SidebarButton = ({ export const SidebarButton = ({
view, view,
icon, icon,
tooltip, tooltip,
disabled = false,
onClick,
}: { }: {
view: ViewType; view?: ViewType;
icon: VegaIconNames; icon: VegaIconNames;
tooltip: string; tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => { }) => {
const { currView, setView } = useSidebar((store) => ({ const { currView, setView } = useSidebar((store) => ({
currView: store.view, currView: store.view,
setView: store.setView, setView: store.setView,
})); }));
const buttonClasses = classNames('flex items-center p-1 rounded', {
'text-vega-clight-200 dark:text-vega-cdark-200 hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500': const onSelect = (view: SidebarView['type']) => {
view !== currView?.type, if (view === currView?.type) {
'bg-vega-yellow hover:bg-vega-yellow-550 text-black': setView(null);
view === currView?.type, } else {
}); setView({ type: view });
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return ( return (
<Tooltip <Tooltip
description={tooltip} description={tooltip}
@@ -145,13 +178,8 @@ const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={() => { onClick={onClick || (() => onSelect(view as SidebarView['type']))}
if (view === currView?.type) { disabled={disabled}
setView(null);
} else {
setView({ type: view });
}
}}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
</button> </button>
@@ -184,6 +212,7 @@ export const SidebarContent = () => {
setView({ type: ViewType.Deposit, assetId }) setView({ type: ViewType.Deposit, assetId })
} }
/> />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} else { } else {
@@ -207,6 +236,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Deposit')}> <ContentWrapper title={t('Deposit')}>
<DepositContainer assetId={view.assetId} /> <DepositContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -215,6 +245,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Withdraw')}> <ContentWrapper title={t('Withdraw')}>
<WithdrawContainer assetId={view.assetId} /> <WithdrawContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -223,6 +254,7 @@ export const SidebarContent = () => {
return ( return (
<ContentWrapper title={t('Transfer')}> <ContentWrapper title={t('Transfer')}>
<TransferContainer assetId={view.assetId} /> <TransferContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper> </ContentWrapper>
); );
} }
@@ -277,7 +309,7 @@ export const useSidebar = create<{
view: null, view: null,
setView: (x) => setView: (x) =>
set(() => { set(() => {
if (x === null) { if (x == null) {
return { view: null, init: false }; return { view: null, init: false };
} }
@@ -27,6 +27,16 @@ describe('VegaWalletConnectButton', () => {
it('should fire dialog when not connected', () => { it('should fire dialog when not connected', () => {
render(generateJsx({ pubKey: null } as VegaWalletContextShape)); render(generateJsx({ pubKey: null } as VegaWalletContextShape));
const button = screen.getByTestId('connect-vega-wallet');
expect(button).toHaveTextContent('Get started');
fireEvent.click(button);
expect(mockUpdateDialogOpen).toHaveBeenCalled();
});
it('should render "Connect" when browser wallet is detected', () => {
window.vega = window.vega || ({} as Vega);
render(generateJsx({ pubKey: null } as VegaWalletContextShape));
const button = screen.getByTestId('connect-vega-wallet'); const button = screen.getByTestId('connect-vega-wallet');
expect(button).toHaveTextContent('Connect'); expect(button).toHaveTextContent('Connect');
fireEvent.click(button); fireEvent.click(button);
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard'; import CopyToClipboard from 'react-copy-to-clipboard';
import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
import { truncateByChars } from '@vegaprotocol/utils'; import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { import {
@@ -37,7 +38,7 @@ export const VegaWalletConnectButton = () => {
fetchPubKeys, fetchPubKeys,
} = useVegaWallet(); } = useVegaWallet();
const isConnected = pubKey !== null; const isConnected = pubKey !== null;
const walletInstalled = isBrowserWalletInstalled();
const activeKey = useMemo(() => { const activeKey = useMemo(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey); return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]); }, [pubKey, pubKeys]);
@@ -118,7 +119,9 @@ export const VegaWalletConnectButton = () => {
intent={Intent.None} intent={Intent.None}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />} icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
> >
<span className="whitespace-nowrap uppercase">{t('Connect')}</span> <span className="whitespace-nowrap uppercase">
{walletInstalled ? t('Connect') : t('Get started')}
</span>
</Button> </Button>
); );
}; };
@@ -0,0 +1,108 @@
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
import { ExternalLink, Intent, TradingButton } from '@vegaprotocol/ui-toolkit';
import {
useVegaWallet,
useVegaWalletDialogStore,
isBrowserWalletInstalled,
} from '@vegaprotocol/wallet';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import * as constants from '../constants';
interface Props {
lead?: string;
}
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 [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const onButtonClick = () => {
openVegaWalletDialog();
setOnboardingViewed('true');
};
const wrapperClasses = classNames(
'flex flex-col py-4 px-6 gap-4 rounded',
'bg-vega-blue-300 dark:bg-vega-blue-700',
'border border-vega-blue-350 dark:border-vega-blue-650',
{ 'mt-8': !lead }
);
if (!isBrowserWalletInstalled()) {
return (
<div className={wrapperClasses} data-testid="get-started-banner">
{lead && <h2>{lead}</h2>}
<h3 className="text-lg">{t('Get started')}</h3>
<div>
<ul className="list-decimal list-inside">
<li>{t('Get a Vega wallet')}</li>
<li>{t('Connect')}</li>
<li>{t('Deposit funds')}</li>
<li>{t('Open a position')}</li>
</ul>
</div>
<div>
<TradingButton
intent={Intent.Info}
onClick={onButtonClick}
data-testid="get-started-button"
>
{t('Get started')}
</TradingButton>
</div>
{VEGA_ENV === Networks.MAINNET && (
<p className="text-sm">
{t('Experiment for free with virtual assets on')}{' '}
<ExternalLink href={CANONICAL_URL}>
{t('Fairground Testnet')}
</ExternalLink>
</p>
)}
{VEGA_ENV === Networks.TESTNET && (
<p className="text-sm">
{t('Ready to trade with real funds?')}{' '}
<ExternalLink href={CANONICAL_URL}>
{t('Switch to Mainnet')}
</ExternalLink>
</p>
)}
</div>
);
}
if (!pubKey) {
return (
<div className={wrapperClasses}>
<p className="text-sm mb-1">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
to start trading in this market.
</p>
<TradingButton
onClick={openVegaWalletDialog}
size="small"
data-testid="order-connect-wallet"
intent={Intent.Info}
>
{t('Connect wallet')}
</TradingButton>
</div>
);
}
return null;
};
@@ -1,2 +1,3 @@
export * from './welcome-dialog'; export * from './welcome-dialog';
export * from './risk-message'; export * from './risk-message';
export * from './get-started';
@@ -1,50 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { RiskNoticeDialog } from './risk-notice-dialog';
jest.mock('@vegaprotocol/environment');
const mockEnvDefinitions = {
VEGA_CONFIG_URL: 'https://config.url',
VEGA_URL: 'https://test.url',
VEGA_NETWORKS: JSON.stringify({}),
};
describe('Risk notice dialog', () => {
const mockOnClose = jest.fn();
afterEach(() => {
jest.clearAllMocks();
});
it.each`
assertion | network
${'displays'} | ${Networks.CUSTOM}
${'displays'} | ${Networks.DEVNET}
${'displays'} | ${Networks.TESTNET}
`(
'$assertion a generic message on $network',
async ({ assertion, network }) => {
// @ts-ignore ignore mock implementation
useEnvironment.mockImplementation(() => ({
...mockEnvDefinitions,
VEGA_ENV: network,
}));
render(<RiskNoticeDialog onClose={mockOnClose} network={network} />);
expect(
screen.getByText(
new RegExp(
`This application for trading on Vega is connected to ${network}`
)
)
).toBeInTheDocument();
const button = screen.getByRole('button', {
name: 'Continue',
});
fireEvent.click(button);
expect(mockOnClose).toHaveBeenCalled();
}
);
});
@@ -1,89 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import {
Button,
Link,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RISK_ACCEPTED_KEY } from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import type { Networks } from '@vegaprotocol/environment';
import {
useEnvironment,
DocsLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
interface Props {
onClose: () => void;
network: Networks;
}
export const RiskNoticeDialog = ({ onClose, network }: Props) => {
const [, setValue] = useLocalStorage(RISK_ACCEPTED_KEY);
const handleAcceptRisk = () => {
onClose();
setValue('true');
};
return (
<TestnetContent network={network} handleAcceptRisk={handleAcceptRisk} />
);
};
const TestnetContent = ({
network,
handleAcceptRisk,
}: {
network: Networks;
handleAcceptRisk: () => void;
}) => {
const { GITHUB_FEEDBACK_URL } = useEnvironment();
return (
<>
<p className="mb-4">
{t(
'This application for trading on Vega is connected to %s, meaning you are free to try out trading with virtual assets and no risk.',
[network]
)}
</p>
<p className="mb-4">
{t(
'Your Vega wallet must also be connected to %s, and your Ethereum wallet must be connected to Sepolia.',
[network]
)}
</p>
{GITHUB_FEEDBACK_URL && DocsLinks && (
<ul className="list-disc pl-4">
<li className="mb-1">
<Link href={ExternalLinks.VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={DocsLinks.VEGA_WALLET_TOOLS_URL} target="_blank">
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={GITHUB_FEEDBACK_URL} target="_blank">
<span className="underline">{t('Provide feedback')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
</ul>
)}
<div className="my-4">
<TelemetryApproval
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data. You can change this in your settings at any time.'
)}
/>
</div>
<Button onClick={handleAcceptRisk}>{t('Continue')}</Button>
</>
);
};
@@ -0,0 +1,159 @@
import { t } from '@vegaprotocol/i18n';
import { GetStarted } from './get-started';
import { TradingButton } from '@vegaprotocol/ui-toolkit';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import * as constants from '../constants';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import type { ReactNode } from 'react';
export const WelcomeDialogContent = () => {
const { VEGA_ENV } = useEnvironment();
const [, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const navigate = useNavigate();
const browseMarkets = () => {
const link = Links[Routes.MARKETS]();
navigate(link);
setOnboardingViewed('true');
};
const lead =
VEGA_ENV === Networks.MAINNET
? t('Start trading on the worlds most advanced decentralised exchange.')
: t(
'Free from the risks of real trading, Fairground is a safe and fun place to try out Vega yourself with virtual assets.'
);
return (
<div className="flex flex-col sm:flex-row gap-8">
<div className="sm:w-1/2 flex flex-col justify-between pt-3">
<ul className="ml-0">
<ListItemContent
icon={<NonCustodialIcon />}
title={t('Non-custodial and pseudonymous')}
text={t('No third party has access to your funds.')}
/>
<ListItemContent
icon={<PurposeBuiltIcon />}
title={t('Purpose built proof of stake blockchain')}
text={t(
'Fully decentralised high performance peer-to-network trading.'
)}
/>
<ListItemContent
icon={<RewardIcon />}
title={t('Low fees and no cost to place orders')}
text={t(
'Fees work like a CEX with no per-transaction gas for orders'
)}
/>
</ul>
<TradingButton
onClick={browseMarkets}
className="block w-full"
data-testid="browse-markets-button"
>
{t('Browse the markets')}
</TradingButton>
</div>
<div className="sm:w-1/2">
<GetStarted lead={lead} />
</div>
</div>
);
};
const ListItemContent = ({
icon,
title,
text,
}: {
icon: ReactNode;
title: string;
text: string;
}) => {
return (
<li className="my-4 flex gap-3">
<div className="shrink-0 pt-1">{icon}</div>
<div>
<h3 className="text-lg leading-snug mb-2">{title}</h3>
<p className="text-sm text-secondary">{text}</p>
</div>
</li>
);
};
const PurposeBuiltIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="14" y="20" width="2" height="2" />
<rect x="12" y="18" width="2" height="2" />
<rect x="10" y="8" width="2" height="10" />
<rect x="16" y="18" width="2" height="2" />
<rect x="20" y="16" width="2" height="2" />
<rect x="18" y="8" width="2" height="8" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const NonCustodialIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" className="fill-current">
<rect x="8" y="22" width="14" height="2" />
<rect x="8" y="12" width="14" height="2" />
<rect
x="22"
y="24"
width="2"
height="12"
transform="rotate(-180 22 24)"
/>
<rect
x="10"
y="24"
width="2"
height="12"
transform="rotate(-180 10 24)"
/>
<rect x="12" y="12" width="2" height="2" transform="rotate(-180 12 12)" />
<rect x="20" y="12" width="2" height="2" transform="rotate(-180 20 12)" />
<rect x="12" y="10" width="2" height="2" transform="rotate(-180 12 10)" />
<rect x="20" y="10" width="2" height="2" transform="rotate(-180 20 10)" />
<rect x="18" y="8" width="2" height="2" transform="rotate(-180 18 8)" />
<rect x="14" y="8" width="2" height="2" transform="rotate(-180 14 8)" />
<rect x="16" y="8" width="2" height="2" transform="rotate(-180 16 8)" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
const RewardIcon = () => {
return (
<svg width="40" height="40" viewBox="0 0 30 30" fill="currentColor">
<rect x="6" y="16" width="4" height="2" />
<rect x="10" y="14" width="2" height="2" />
<rect x="12" y="24" width="4" height="2" transform="rotate(-90 12 24)" />
<rect x="10" y="20" width="2" height="2" transform="rotate(-90 10 20)" />
<rect x="20" y="18" width="4" height="2" transform="rotate(-180 20 18)" />
<rect x="16" y="20" width="2" height="2" transform="rotate(-180 16 20)" />
<rect x="16" y="14" width="2" height="2" transform="rotate(90 16 14)" />
<rect x="22" y="10" width="2" height="2" transform="rotate(90 22 10)" />
<rect x="20" y="8" width="2" height="2" transform="rotate(90 20 8)" />
<rect x="24" y="8" width="2" height="2" transform="rotate(90 24 8)" />
<rect x="22" y="6" width="2" height="2" transform="rotate(90 22 6)" />
<rect x="12" y="10" width="2" height="4" />
<rect x="28" y="2" width="2" height="26" />
<rect y="2" width="2" height="26" />
<rect x="28" width="2" height="26" transform="rotate(90 28 0)" />
<rect x="28" y="28" width="2" height="26" transform="rotate(90 28 28)" />
</svg>
);
};
@@ -1,68 +1,52 @@
import React, { useCallback } from 'react'; import React from 'react';
import { useLocation } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Dialog } from '@vegaprotocol/ui-toolkit'; import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeMarketsProvider } from '@vegaprotocol/markets';
import * as constants from '../constants';
import { RiskNoticeDialog } from './risk-notice-dialog';
import { WelcomeNoticeDialog } from './welcome-notice-dialog';
import { useGlobalStore } from '../../stores';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { Networks } from '@vegaprotocol/environment'; import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
import { isTestEnv } from '@vegaprotocol/utils'; import * as constants from '../constants';
import { WelcomeDialogContent } from './welcome-dialog-content';
import { getConfig } from '@vegaprotocol/wallet';
import { Links, Routes } from '../../pages/client-router';
import { useGlobalStore } from '../../stores';
export const WelcomeDialog = () => { export const WelcomeDialog = () => {
const { VEGA_ENV } = useEnvironment(); const { VEGA_ENV } = useEnvironment();
const { pathname } = useLocation(); const [onBoardingViewed, setOnboardingViewed] = useLocalStorage(
let dialogContent: React.ReactNode; constants.ONBOARDING_VIEWED_KEY
let title = ''; );
let size: 'small' | 'medium' = 'small'; const navigate = useNavigate();
let onClose: ((open: boolean) => void) | undefined = undefined; const isOnboardingDialogNeeded =
const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY); onBoardingViewed !== 'true' && !isBrowserWalletInstalled() && !getConfig();
const { data } = useDataProvider({ const marketId = useGlobalStore((store) => store.marketId);
dataProvider: activeMarketsProvider,
variables: undefined,
});
const update = useGlobalStore((store) => store.update); const onClose = () => {
const shouldDisplayWelcomeDialog = useGlobalStore( setOnboardingViewed('true');
(store) => store.shouldDisplayWelcomeDialog const link = marketId
? Links[Routes.MARKET](marketId)
: Links[Routes.HOME]();
navigate(link);
};
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>
); );
const isRiskDialogNeeded = return isOnboardingDialogNeeded ? (
riskAccepted !== 'true' && VEGA_ENV !== Networks.MAINNET && !isTestEnv();
const isWelcomeDialogNeeded = pathname === '/' || shouldDisplayWelcomeDialog;
const onCloseDialog = useCallback(() => {
update({
shouldDisplayWelcomeDialog: isRiskDialogNeeded,
});
}, [update, isRiskDialogNeeded]);
if (isRiskDialogNeeded) {
dialogContent = (
<RiskNoticeDialog onClose={onCloseDialog} network={VEGA_ENV} />
);
title = t('Vega Console');
size = 'medium';
} else if (isWelcomeDialogNeeded && data?.length === 0) {
dialogContent = <WelcomeNoticeDialog />;
onClose = onCloseDialog;
} else {
dialogContent = null as React.ReactNode;
}
return (
<Dialog <Dialog
open={Boolean(dialogContent)} open
title={title} title={title}
size={size} size="medium"
onChange={onClose} onChange={onClose}
intent={Intent.None}
dataTestId="welcome-dialog"
> >
{dialogContent} <WelcomeDialogContent />
</Dialog> </Dialog>
); ) : null;
}; };
@@ -1,71 +0,0 @@
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import {
DApp,
Networks,
TOKEN_NEW_MARKET_PROPOSAL,
TOKEN_PROPOSALS,
useEnvironment,
useLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { ProposedMarkets } from './proposed-markets';
import { TelemetryApproval } from './telemetry-approval';
export const WelcomeNoticeDialog = () => {
const { VEGA_ENV } = useEnvironment();
const tokenLink = useLinks(DApp.Token);
const consoleFairgroundLink = useLinks(DApp.Console, Networks.TESTNET);
const isMainnet = VEGA_ENV === Networks.MAINNET;
return (
<>
<h1
data-testid="welcome-notice-title"
className="text-2xl uppercase mb-6 text-center font-alpha calt"
>
{t('Welcome to Console')}
</h1>
<p className="leading-6 mb-4">
{t(
'There are no markets to trade on right now. Trading on Vega is now live, but markets need to pass a governance vote before they can be traded on. In the meantime:'
)}
</p>
<ul className="list-[square] pl-4 mb-4">
{isMainnet && (
<li className="mb-1">
<ExternalLink target="_blank" href={consoleFairgroundLink()}>
{t('Try out Console')}
</ExternalLink>
{t(' on Fairground, our Testnet')}
</li>
)}
<li className="mb-1">
<ExternalLink target="_blank" href={tokenLink(TOKEN_PROPOSALS)}>
{t('View and vote for proposed markets')}
</ExternalLink>
</li>
<li className="mb-1">
<ExternalLink
target="_blank"
href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}
>
{t('Propose a market')}
</ExternalLink>
</li>
<li className="mb-1">
<ExternalLink target="_blank" href={ExternalLinks.BLOG}>
{t('Read about the mainnet launch')}
</ExternalLink>
</li>
</ul>
{isMainnet && (
<TelemetryApproval
helpText={t(
'Help identify bugs and improve the service by sharing anonymous usage data. You can change this in your settings at any time.'
)}
/>
)}
<ProposedMarkets />
</>
);
};
+7
View File
@@ -31,6 +31,10 @@ const LazyDisclaimer = dynamic(() => import('../client-pages/disclaimer'), {
ssr: false, ssr: false,
}); });
const LazyDeposit = dynamic(() => import('../client-pages/deposit'), {
ssr: false,
});
export enum Routes { export enum Routes {
HOME = '/', HOME = '/',
MARKET = '/markets/:marketId', MARKET = '/markets/:marketId',
@@ -38,6 +42,7 @@ export enum Routes {
PORTFOLIO = '/portfolio', PORTFOLIO = '/portfolio',
LIQUIDITY = '/liquidity/:marketId', LIQUIDITY = '/liquidity/:marketId',
DISCLAIMER = '/disclaimer', DISCLAIMER = '/disclaimer',
DEPOSIT = '/deposit',
} }
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string }; type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -51,6 +56,7 @@ export const Links: ConsoleLinks = {
[Routes.LIQUIDITY]: (marketId: string) => [Routes.LIQUIDITY]: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)), trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
[Routes.DISCLAIMER]: () => Routes.DISCLAIMER, [Routes.DISCLAIMER]: () => Routes.DISCLAIMER,
[Routes.DEPOSIT]: () => Routes.DEPOSIT,
}; };
const routerConfig: RouteObject[] = [ const routerConfig: RouteObject[] = [
@@ -97,6 +103,7 @@ const routerConfig: RouteObject[] = [
path: Routes.DISCLAIMER, path: Routes.DISCLAIMER,
element: <LazyDisclaimer />, element: <LazyDisclaimer />,
}, },
{ path: Routes.DEPOSIT, element: <LazyDeposit /> },
{ {
path: '*', path: '*',
element: ( element: (
+2 -1
View File
@@ -2,7 +2,7 @@ import {
AssetDetailsDialog, AssetDetailsDialog,
useAssetDetailsDialogStore, useAssetDetailsDialogStore,
} from '@vegaprotocol/assets'; } from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet'; import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors'; import { Connectors } from '../lib/vega-connectors';
import { import {
Web3ConnectUncontrolledDialog, Web3ConnectUncontrolledDialog,
@@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors} connectors={Connectors}
riskMessage={<RiskMessage />} riskMessage={<RiskMessage />}
/> />
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog <AssetDetailsDialog
assetId={id} assetId={id}
trigger={trigger || null} trigger={trigger || null}
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

-2
View File
@@ -5,7 +5,6 @@ import produce from 'immer';
interface GlobalStore { interface GlobalStore {
marketId: string | null; marketId: string | null;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void; update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
shouldDisplayWelcomeDialog: boolean;
} }
interface PageTitleStore { interface PageTitleStore {
@@ -15,7 +14,6 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({ export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null, marketId: LocalStorage.getItem('marketId') || null,
shouldDisplayWelcomeDialog: false,
update: (newState) => { update: (newState) => {
set( set(
produce((state: GlobalStore) => { produce((state: GlobalStore) => {
+1 -3
View File
@@ -75,7 +75,7 @@ export const AccountBreakdownDialog = memo(
}) => { }) => {
return ( return (
<Dialog <Dialog
size="medium" size="large"
open={Boolean(assetId)} open={Boolean(assetId)}
onChange={(isOpen) => { onChange={(isOpen) => {
if (!isOpen) { if (!isOpen) {
@@ -118,7 +118,6 @@ export const AccountManager = ({
onMarketClick, onMarketClick,
gridProps, gridProps,
}: AccountManagerProps) => { }: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [breakdownAssetId, setBreakdownAssetId] = useState<string>(); const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
const { data, error } = useDataProvider({ const { data, error } = useDataProvider({
dataProvider: aggregatedAccountsDataProvider, dataProvider: aggregatedAccountsDataProvider,
@@ -138,7 +137,6 @@ export const AccountManager = ({
return ( return (
<div className="relative h-full"> <div className="relative h-full">
<AccountTable <AccountTable
ref={gridRef}
rowData={data} rowData={data}
onClickAsset={onClickAsset} onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit} onClickDeposit={onClickDeposit}
+237 -243
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo, useCallback } from 'react'; import { useMemo, useCallback } from 'react';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum, addDecimalsFormatNumberQuantum,
@@ -21,7 +21,7 @@ import type {
RowHeightParams, RowHeightParams,
ColDef, ColDef,
} from 'ag-grid-community'; } from 'ag-grid-community';
import type { AgGridReact, AgGridReactProps } from 'ag-grid-react'; import type { AgGridReactProps } from 'ag-grid-react';
import type { AccountFields } from './accounts-data-provider'; import type { AccountFields } from './accounts-data-provider';
import type { Asset } from '@vegaprotocol/types'; import type { Asset } from '@vegaprotocol/types';
import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid'; import { CenteredGridCellWrapper } from '@vegaprotocol/datagrid';
@@ -75,256 +75,250 @@ export interface AccountTableProps extends AgGridReactProps {
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
} }
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>( export const AccountTable = ({
( onClickAsset,
{ onClickWithdraw,
onClickAsset, onClickDeposit,
onClickWithdraw, onClickBreakdown,
onClickDeposit, onClickTransfer,
onClickBreakdown, rowData,
onClickTransfer, isReadOnly,
rowData, pinnedAsset,
isReadOnly, ...props
pinnedAsset, }: AccountTableProps) => {
...props const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props;
const getPinnedAssetRowHeight = useCallback(
(params: RowHeightParams) => {
if (
params.node.rowPinned &&
params.data.asset.id === pinnedAsset?.id &&
new BigNumber(params.data.total).isLessThanOrEqualTo(0)
) {
return 32;
}
return getRowHeight ? getRowHeight(params) : undefined;
}, },
ref [pinnedAsset?.id, getRowHeight]
) => { );
const pinnedRow = useMemo(() => {
if (!pinnedAsset) {
return;
}
const currentPinnedAssetRow = rowData?.find(
(row) => row.asset.id === pinnedAsset?.id
);
if (!currentPinnedAssetRow) {
return {
asset: pinnedAsset,
available: '0',
used: '0',
total: '0',
balance: '0',
};
}
return currentPinnedAssetRow;
}, [pinnedAsset, rowData]);
const { getRowHeight } = props; const showDepositButton = pinnedRow?.balance === '0';
const getPinnedAssetRowHeight = useCallback( const colDefs = useMemo(() => {
(params: RowHeightParams) => { const defs: ColDef[] = [
if ( {
params.node.rowPinned && headerName: t('Asset'),
params.data.asset.id === pinnedAsset?.id && field: 'asset.symbol',
new BigNumber(params.data.total).isLessThanOrEqualTo(0) headerTooltip: t(
) { 'Asset is the collateral that is deposited into the Vega protocol.'
return 32; ),
} cellClass: 'underline',
return getRowHeight ? getRowHeight(params) : undefined; onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
}, },
[pinnedAsset?.id, getRowHeight] {
); headerName: t('Used'),
type: 'rightAligned',
const showDepositButton = pinnedRow?.balance === '0'; field: 'used',
headerTooltip: t(
const colDefs = useMemo(() => { 'Currently allocated to a market as margin or bond. Check the breakdown for details.'
const defs: ColDef[] = [ ),
{ tooltipValueGetter: ({ value, data }) => {
headerName: t('Asset'), if (!value || !data) return null;
field: 'asset.symbol', return addDecimalsFormatNumber(value, data.asset.decimals);
headerTooltip: t(
'Asset is the collateral that is deposited into the Vega protocol.'
),
cellClass: 'underline',
onCellClicked: ({ data }) => {
if (data) {
onClickAsset(data.asset.id);
}
},
}, },
{ onCellClicked: ({ data }) => {
headerName: t('Used'), if (!data || !onClickBreakdown) return;
type: 'rightAligned', onClickBreakdown(data.asset.id);
field: 'used', },
headerTooltip: t( cellRenderer: ({
'Currently allocated to a market as margin or bond. Check the breakdown for details.' data,
), value,
tooltipValueGetter: ({ value, data }) => { }: VegaICellRendererParams<AccountFields, 'used'>) => {
if (!value || !data) return null; if (!value || !data) return '-';
return addDecimalsFormatNumber(value, data.asset.decimals); const percentageUsed = percentageValue(value, data.total);
}, const valueFormatted = addDecimalsFormatNumberQuantum(
onCellClicked: ({ data }) => {
if (!data || !onClickBreakdown) return;
onClickBreakdown(data.asset.id);
},
cellRenderer: ({
data,
value, value,
}: VegaICellRendererParams<AccountFields, 'used'>) => { data.asset.decimals,
if (!value || !data) return '-'; data.asset.quantum
const percentageUsed = percentageValue(value, data.total); );
const valueFormatted = addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
return data.breakdown ? ( return data.breakdown ? (
<> <>
<span className="underline">{valueFormatted}</span> <span className="underline">{valueFormatted}</span>
<span <span
className={classNames( className={classNames(
colorClass(percentageUsed), colorClass(percentageUsed),
'ml-1 inline-block w-14' 'ml-1 inline-block w-14'
)} )}
>
{percentageUsed.toFixed(2)}%
</span>
</>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
);
},
},
{
headerName: t('Available'),
field: 'available',
type: 'rightAligned',
headerTooltip: t(
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
cellClass: ({ data }) => {
const percentageUsed = percentageValue(data?.used, data?.total);
return colorClass(percentageUsed, true);
},
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<AccountFields, 'available'>) => {
if (!value || !data) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
> >
{percentageUsed.toFixed(2)}% <VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</span> </Button>
</> </CenteredGridCellWrapper>
) : (
<>
<span className="underline">{valueFormatted}</span>
<span className="ml-2 inline-block w-14 text-muted">
{t('0.00%')}
</span>
</>
); );
}, }
}, return isReadOnly ? null : (
{ <AccountsActionsDropdown
headerName: t('Available'), assetId={assetId}
field: 'available', assetContractAddress={
type: 'rightAligned', node.data?.asset.source?.__typename === 'ERC20'
headerTooltip: t( ? node.data.asset.source.contractAddress
'Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.' : undefined
), }
tooltipValueGetter: ({ value, data }) => { onClickDeposit={() => {
if (!value || !data) return null; onClickDeposit && onClickDeposit(assetId);
return addDecimalsFormatNumber(value, data.asset.decimals); }}
}, onClickWithdraw={() => {
cellClass: ({ data }) => { onClickWithdraw && onClickWithdraw(assetId);
const percentageUsed = percentageValue(data?.used, data?.total); }}
return colorClass(percentageUsed, true); onClickBreakdown={() => {
}, onClickBreakdown && onClickBreakdown(assetId);
valueFormatter: ({ }}
value, onClickTransfer={() => {
data, onClickTransfer && onClickTransfer(assetId);
}: VegaValueFormatterParams<AccountFields, 'available'>) => { }}
if (!value || !data) return '-'; />
return addDecimalsFormatNumberQuantum( );
value,
data.asset.decimals,
data.asset.quantum
);
},
}, },
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
{ const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
headerName: t('Total'),
type: 'rightAligned',
field: 'total',
headerTooltip: t(
'The total amount of each asset on this key. Includes used and available collateral.'
),
tooltipValueGetter: ({ value, data }) => {
if (!value || !data) return null;
return addDecimalsFormatNumber(value, data.asset.decimals);
},
valueFormatter: ({
data,
value,
}: VegaValueFormatterParams<AccountFields, 'total'>) => {
if (!data || !value) return '-';
return addDecimalsFormatNumberQuantum(
value,
data.asset.decimals,
data.asset.quantum
);
},
},
{
colId: 'accounts-actions',
field: 'asset.id',
...COL_DEFS.actions,
minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth,
maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth,
cellRenderer: ({
value: assetId,
node,
}: VegaICellRendererParams<AccountFields, 'asset.id'>) => {
if (!assetId) return null;
if (node.rowPinned && node.data?.total === '0') {
return (
<CenteredGridCellWrapper className="h-[30px] justify-end py-1">
<Button
size="xs"
variant="primary"
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(assetId);
}}
>
<VegaIcon name={VegaIconNames.DEPOSIT} /> {t('Deposit')}
</Button>
</CenteredGridCellWrapper>
);
}
return isReadOnly ? null : (
<AccountsActionsDropdown
assetId={assetId}
assetContractAddress={
node.data?.asset.source?.__typename === 'ERC20'
? node.data.asset.source.contractAddress
: undefined
}
onClickDeposit={() => {
onClickDeposit && onClickDeposit(assetId);
}}
onClickWithdraw={() => {
onClickWithdraw && onClickWithdraw(assetId);
}}
onClickBreakdown={() => {
onClickBreakdown && onClickBreakdown(assetId);
}}
onClickTransfer={() => {
onClickTransfer && onClickTransfer(assetId);
}}
/>
);
},
},
];
return defs;
}, [
onClickAsset,
onClickBreakdown,
onClickDeposit,
onClickWithdraw,
onClickTransfer,
isReadOnly,
showDepositButton,
]);
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id); return (
<AgGrid
return ( {...props}
<AgGrid style={{ width: '100%', height: '100%' }}
{...props} getRowId={({ data }: { data: AccountFields }) => data.asset.id}
style={{ width: '100%', height: '100%' }} tooltipShowDelay={500}
getRowId={({ data }: { data: AccountFields }) => data.asset.id} rowData={data}
ref={ref} defaultColDef={{
tooltipShowDelay={500} resizable: true,
rowData={data} tooltipComponent: TooltipCellComponent,
defaultColDef={{ sortable: true,
resizable: true, comparator: accountValuesComparator,
tooltipComponent: TooltipCellComponent, }}
sortable: true, columnDefs={colDefs}
comparator: accountValuesComparator, getRowHeight={getPinnedAssetRowHeight}
}} pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined}
columnDefs={colDefs} />
getRowHeight={getPinnedAssetRowHeight} );
pinnedTopRowData={pinnedRow ? [pinnedRow] : undefined} };
/>
);
}
);
@@ -1,71 +0,0 @@
import { CandlesChartContainer } from './candles-chart';
import { render, screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { CandlesEventsDocument } from './__generated__/Candles';
import type { CandlesEventsSubscription } from './__generated__/Candles';
const candles: CandlesEventsSubscription = {
candles: {
lastUpdateInPeriod: 0,
periodStart: 0,
open: '0',
high: '0',
low: '0',
close: '0',
volume: '0',
},
};
const mocks = [
{
request: {
query: CandlesEventsDocument,
variables: { marketId: 'market-id', interval: 'INTERVAL_I15M' },
},
result: { data: candles },
},
];
describe('TradingChart', () => {
it('should render successfully', () => {
const { baseElement } = render(
<MockedProvider>
<VegaWalletContext.Provider value={{} as never}>
<CandlesChartContainer marketId={'market-id'} />
</VegaWalletContext.Provider>
</MockedProvider>
);
expect(baseElement).toBeTruthy();
});
it('volume study should be preselected', async () => {
act(() => {
render(
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={{} as never}>
<CandlesChartContainer marketId={'market-id'} />
</VegaWalletContext.Provider>
</MockedProvider>
);
});
await waitFor(() => {
expect(
screen.getByText('Studies', {
selector: '[type="button"]',
})
).toBeInTheDocument();
});
act(() => {
userEvent.click(
screen.getByText('Studies', {
selector: '[type="button"]',
})
);
});
await waitFor(() => {
expect(screen.getByRole('menu')).toBeInTheDocument();
});
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
});
});
+24 -233
View File
@@ -1,92 +1,12 @@
import 'pennant/dist/style.css'; import 'pennant/dist/style.css';
import { import { CandlestickChart } from 'pennant';
CandlestickChart,
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import { VegaDataSource } from './data-source'; import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
useThemeSwitcher,
getValidItem,
getValidSubset,
} from '@vegaprotocol/react-helpers';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { create } from 'zustand'; import { useCandlesChartSettings } from './use-candles-chart-settings';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface StoredSettings {
interval?: Interval;
type?: ChartType;
overlays?: Overlay[];
studies?: Study[];
}
export const useCandlesChartSettings = create<
StoredSettings & {
merge: (settings: StoredSettings) => void;
setType: (type: ChartType) => void;
setInterval: (interval: Interval) => void;
setOverlays: (overlays: Overlay[]) => void;
setStudies: (studies: Study[]) => void;
}
>()(
persist(
immer((set) => ({
merge: (settings: StoredSettings) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type: ChartType) =>
set((state) => {
state.type = type;
}),
setInterval: (interval: Interval) =>
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays: Overlay[]) =>
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies: Study[]) =>
set((state) => {
state.studies = studies;
}),
})),
{
name: 'console-candles',
}
)
);
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export type CandlesChartContainerProps = { export type CandlesChartContainerProps = {
marketId: string; marketId: string;
@@ -99,161 +19,32 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher(); const { theme } = useThemeSwitcher();
const settings = useCandlesChartSettings(); const { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
);
const chartType: ChartType = getValidItem(
settings.type,
Object.values(ChartType),
ChartType.CANDLE
);
const overlays: Overlay[] = getValidSubset(
settings.overlays,
Object.values(Overlay),
[]
);
const studies: Study[] = getValidSubset(
settings.studies,
Object.values(Study),
[Study.VOLUME]
);
const dataSource = useMemo(() => { const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey); return new VegaDataSource(client, marketId, pubKey);
}, [client, marketId, pubKey]); }, [client, marketId, pubKey]);
return ( return (
<div className="h-full flex flex-col"> <CandlestickChart
<div className="px-3 lg:px-4 py-2 flex flex-row flex-wrap gap-2 bg-vega-clight-700 dark:bg-vega-cdark-700"> dataSource={dataSource}
<DropdownMenu options={{
trigger={ chartType,
<DropdownMenuTrigger> overlays,
{t(`Interval: ${intervalLabels[interval]}`)} studies,
</DropdownMenuTrigger> notEnoughDataText: (
} <span className="text-xs text-center">{t('No data')}</span>
> ),
<DropdownMenuContent> }}
<DropdownMenuRadioGroup interval={interval}
value={interval} theme={theme}
onValueChange={(value) => { onOptionsChanged={(options) => {
settings.setInterval(value as Interval); merge({
}} overlays: options.overlays,
> studies: options.studies,
{Object.values(Interval).map((timeInterval) => ( });
<DropdownMenuRadioItem }}
key={timeInterval} />
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
settings.setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
settings.setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>}
>
<DropdownMenuContent>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
settings.setStudies(newStudies);
}}
>
{studyLabels[study]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex-1">
<CandlestickChart
dataSource={dataSource}
options={{
chartType: chartType,
overlays: overlays,
studies: studies,
notEnoughDataText: (
<span className="text-xs text-center">{t('No data')}</span>
),
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
settings.merge({
overlays: options.overlays,
studies: options.studies,
});
}}
/>
</div>
</div>
); );
}; };
@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CandlesMenu } from './candles-menu';
describe('CandlesMenu', () => {
it('should render with volume study showing by default', async () => {
render(<CandlesMenu />);
await userEvent.click(
screen.getByText('Studies', {
selector: '[type="button"]',
})
);
expect(await screen.findByRole('menu')).toBeInTheDocument();
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
});
});
+160
View File
@@ -0,0 +1,160 @@
import 'pennant/dist/style.css';
import {
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@blueprintjs/icons';
import { IconNames } from '@blueprintjs/icons';
import { t } from '@vegaprotocol/i18n';
import { useCandlesChartSettings } from './use-candles-chart-settings';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
[ChartType.CANDLE, IconNames.WATERFALL_CHART],
[ChartType.LINE, IconNames.TIMELINE_LINE_CHART],
[ChartType.OHLC, IconNames.WATERFALL_CHART],
]);
export const CandlesMenu = () => {
const {
interval,
chartType,
studies,
overlays,
setInterval,
setType,
setStudies,
setOverlays,
} = useCandlesChartSettings();
const triggerClasses = 'text-xs';
const contentAlign = 'end';
return (
<>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
setInterval(value as Interval);
}}
>
{Object.values(Interval).map((timeInterval) => (
<DropdownMenuRadioItem
key={timeInterval}
inset
value={timeInterval}
>
{intervalLabels[timeInterval]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
<Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
<DropdownMenuRadioGroup
value={chartType}
onValueChange={(value) => {
setType(value as ChartType);
}}
>
{Object.values(ChartType).map((type) => (
<DropdownMenuRadioItem key={type} inset value={type}>
{chartTypeLabels[type]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Overlays')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
{Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem
key={overlay}
checked={overlays.includes(overlay)}
onCheckedChange={() => {
const newOverlays = [...overlays];
const index = overlays.findIndex((item) => item === overlay);
index !== -1
? newOverlays.splice(index, 1)
: newOverlays.push(overlay);
setOverlays(newOverlays);
}}
>
{overlayLabels[overlay]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
trigger={
<DropdownMenuTrigger className={triggerClasses}>
{t('Studies')}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align={contentAlign}>
{Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem
key={study}
checked={studies.includes(study)}
onCheckedChange={() => {
const newStudies = [...studies];
const index = studies.findIndex((item) => item === study);
index !== -1
? newStudies.splice(index, 1)
: newStudies.push(study);
setStudies(newStudies);
}}
>
{studyLabels[study]}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</>
);
};
+1
View File
@@ -1,4 +1,5 @@
export * from './__generated__/Candles'; export * from './__generated__/Candles';
export * from './__generated__/Chart'; export * from './__generated__/Chart';
export * from './candles-chart'; export * from './candles-chart';
export * from './candles-menu';
export * from './data-source'; export * from './data-source';
@@ -0,0 +1,99 @@
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
import { ChartType, Interval, Study } from 'pennant';
import { Overlay } from 'pennant';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
interface StoredSettings {
interval: Interval;
type: ChartType;
overlays: Overlay[];
studies: Study[];
}
const DEFAULT_CHART_SETTINGS = {
interval: Interval.I15M,
type: ChartType.CANDLE,
overlays: [],
studies: [Study.VOLUME],
};
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;
}
>()(
persist(
immer((set) => ({
...DEFAULT_CHART_SETTINGS,
merge: (settings: Partial<StoredSettings>) =>
set((state) => {
Object.assign(state, settings);
}),
setType: (type) =>
set((state) => {
state.type = type;
}),
setInterval: (interval) =>
set((state) => {
state.interval = interval;
}),
setOverlays: (overlays) =>
set((state) => {
state.overlays = overlays;
}),
setStudies: (studies) =>
set((state) => {
state.studies = studies;
}),
})),
{
name: 'vega_candles_chart_store',
}
)
);
export const useCandlesChartSettings = () => {
const settings = useCandlesChartSettingsStore();
const interval: Interval = getValidItem(
settings.interval,
Object.values(Interval),
Interval.I15M
);
const chartType: ChartType = getValidItem(
settings.type,
Object.values(ChartType),
ChartType.CANDLE
);
const overlays: Overlay[] = getValidSubset(
settings.overlays,
Object.values(Overlay),
[]
);
const studies: Study[] = getValidSubset(
settings.studies,
Object.values(Study),
[Study.VOLUME]
);
return {
interval,
chartType,
overlays,
studies,
setInterval: settings.setInterval,
setType: settings.setType,
setStudies: settings.setStudies,
setOverlays: settings.setOverlays,
merge: settings.merge,
};
};
+2
View File
@@ -12,6 +12,7 @@ import { addUpdateCapsuleMultiSig } from './lib/commands/add-validators-to-multi
import { import {
addVegaWalletConnect, addVegaWalletConnect,
addSetVegaWallet, addSetVegaWallet,
addSetOnBoardingViewed,
} from './lib/commands/vega-wallet-connect'; } from './lib/commands/vega-wallet-connect';
import { addMockTransactionResponse } from './lib/commands/mock-transaction-response'; import { addMockTransactionResponse } from './lib/commands/mock-transaction-response';
import { addCreateMarket } from './lib/commands/create-market'; import { addCreateMarket } from './lib/commands/create-market';
@@ -38,6 +39,7 @@ addGetNetworkParameters();
addUpdateCapsuleMultiSig(); addUpdateCapsuleMultiSig();
addVegaWalletConnect(); addVegaWalletConnect();
addSetVegaWallet(); addSetVegaWallet();
addSetOnBoardingViewed();
addMockTransactionResponse(); addMockTransactionResponse();
addCreateMarket(); addCreateMarket();
addConnectPublicKey(); addConnectPublicKey();
@@ -14,6 +14,10 @@ declare global {
interface Chainable<Subject> { interface Chainable<Subject> {
setVegaWallet(): void; setVegaWallet(): void;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
setOnBoardingViewed(): void;
}
} }
} }
@@ -60,7 +64,7 @@ export function addVegaWalletConnect() {
export function addSetVegaWallet() { export function addSetVegaWallet() {
Cypress.Commands.add('setVegaWallet', () => { Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => { cy.window().then((win) => {
win.localStorage.setItem('vega_risk_accepted', 'true'); win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem( win.localStorage.setItem(
'vega_wallet_config', 'vega_wallet_config',
JSON.stringify({ JSON.stringify({
@@ -72,3 +76,11 @@ export function addSetVegaWallet() {
}); });
}); });
} }
export function addSetOnBoardingViewed() {
Cypress.Commands.add('setOnBoardingViewed', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_onboarding_viewed', 'true');
});
});
}
-1
View File
@@ -21,5 +21,4 @@ export * from './lib/type-helpers';
export * from './lib/cells/grid-progress-bar'; export * from './lib/cells/grid-progress-bar';
export * from './lib/ag-grid-update';
export * from './lib/use-datagrid-events'; export * from './lib/use-datagrid-events';
-20
View File
@@ -1,20 +0,0 @@
import type { MutableRefObject, RefObject } from 'react';
import type { AgGridReact } from 'ag-grid-react';
type AnyArray = Array<any> | null; // eslint-disable-line @typescript-eslint/no-explicit-any
export const isXOrWasEmpty = (prev?: AnyArray, curr?: AnyArray) =>
Boolean(Number(!!prev?.length) ^ Number(!!curr?.length));
export const updateGridData = (
dataRef: MutableRefObject<AnyArray>,
data: AnyArray,
gridRef: RefObject<AgGridReact>
) => {
const rerender = isXOrWasEmpty(dataRef.current, data);
dataRef.current = data;
if (gridRef.current?.api?.getModel().getType() === 'infinite') {
gridRef.current?.api?.refreshInfiniteCache();
}
return !rerender;
};
@@ -158,16 +158,13 @@ describe('StopOrder', () => {
); );
}); });
it('shows no wallet warning and do not submit if no wallet connected', async () => { it('does not submit if no wallet connected', async () => {
render(generateJsx(null)); render(generateJsx(null));
await userEvent.type(screen.getByTestId(sizeInput), '1'); await userEvent.type(screen.getByTestId(sizeInput), '1');
await userEvent.type(screen.getByTestId(priceInput), '1'); await userEvent.type(screen.getByTestId(priceInput), '1');
await userEvent.type(screen.getByTestId(triggerPriceInput), '1'); await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
await userEvent.click(screen.getByTestId(submitButton)); await userEvent.click(screen.getByTestId(submitButton));
expect(submit).not.toBeCalled(); expect(submit).not.toBeCalled();
expect(
screen.getByTestId('deal-ticket-connect-wallet')
).toBeInTheDocument();
}); });
it('calls submit if form is valid', async () => { it('calls submit if form is valid', async () => {
@@ -488,19 +488,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</InputError> </InputError>
)} )}
</div> </div>
<div className="flex gap-2 pb-2 justify-end"> <div className="flex gap-2 pb-2 justify-between">
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div>
<div className="mb-2">
<Controller <Controller
name="expire" name="expire"
control={control} control={control}
@@ -511,11 +499,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
onCheckedChange={onCheckedChange} onCheckedChange={onCheckedChange}
checked={value} checked={value}
name="expire" name="expire"
label={'Expire'} label={<span className="text-xs">{t('Expire')}</span>}
/> />
); );
}} }}
/> />
<Checkbox
name="reduce-only"
checked={true}
disabled={true}
label={
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
<span className="text-xs">{t('Reduce only')}</span>
</Tooltip>
}
/>
</div> </div>
{expire && ( {expire && (
<> <>
@@ -570,7 +568,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</div> </div>
</> </>
)} )}
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} /> <NoWalletWarning isReadOnly={isReadOnly} />
<DealTicketButton side={side} label={t('Submit Stop Order')} /> <DealTicketButton side={side} label={t('Submit Stop Order')} />
<DealTicketFeeDetails <DealTicketFeeDetails
order={{ order={{
@@ -13,14 +13,9 @@ import { SideSelector } from './side-selector';
import { TimeInForceSelector } from './time-in-force-selector'; import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector'; import { TypeSelector } from './type-selector';
import type { OrderSubmission } from '@vegaprotocol/wallet'; import type { OrderSubmission } from '@vegaprotocol/wallet';
import { import { normalizeOrderSubmission, useVegaWallet } from '@vegaprotocol/wallet';
normalizeOrderSubmission,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { import {
Checkbox, Checkbox,
ExternalLink,
InputError, InputError,
Intent, Intent,
Notification, Notification,
@@ -637,13 +632,7 @@ interface SummaryMessageProps {
export const NoWalletWarning = ({ export const NoWalletWarning = ({
isReadOnly, isReadOnly,
pubKey, }: Pick<SummaryMessageProps, 'isReadOnly'>) => {
asset,
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
const assetSymbol = asset.symbol;
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
if (isReadOnly) { if (isReadOnly) {
return ( return (
<div className="mb-2"> <div className="mb-2">
@@ -655,31 +644,6 @@ export const NoWalletWarning = ({
</div> </div>
); );
} }
if (!pubKey) {
return (
<div className="mb-2">
<Notification
testId={'deal-ticket-connect-wallet'}
intent={Intent.Warning}
message={
<p className="text-sm pb-2">
You need a{' '}
<ExternalLink href="https://vega.xyz/wallet">
Vega wallet
</ExternalLink>{' '}
with {assetSymbol} to start trading in this market.
</p>
}
buttonProps={{
text: t('Connect wallet'),
action: openVegaWalletDialog,
dataTestId: 'order-connect-wallet',
size: 'small',
}}
/>
</div>
);
}
return null; return null;
}; };
@@ -698,13 +662,7 @@ const SummaryMessage = memo(
// Specific error UI for if balance is so we can // Specific error UI for if balance is so we can
// render a deposit dialog // render a deposit dialog
if (isReadOnly || !pubKey) { if (isReadOnly || !pubKey) {
return ( return <NoWalletWarning isReadOnly={isReadOnly} />;
<NoWalletWarning
isReadOnly={isReadOnly}
asset={asset}
pubKey={pubKey}
/>
);
} }
if (errorMessage === SummaryValidationType.NoCollateral) { if (errorMessage === SummaryValidationType.NoCollateral) {
@@ -17,7 +17,7 @@ export const SideSelector = (props: SideSelectorProps) => {
return ( return (
<RadioGroup.Root <RadioGroup.Root
name="order-side" name="order-side"
className="mb-2 flex h-10 leading-10" className="mb-4 flex h-10 leading-10"
{...props} {...props}
> >
{toggles.map(({ label, value }) => ( {toggles.map(({ label, value }) => (
@@ -1,5 +1,4 @@
import { import {
Icon,
InputError, InputError,
SimpleGrid, SimpleGrid,
Tooltip, Tooltip,
@@ -10,6 +9,8 @@ import {
TradingDropdownRadioGroup, TradingDropdownRadioGroup,
TradingDropdownRadioItem, TradingDropdownRadioItem,
TradingDropdownTrigger, TradingDropdownTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { Market, StaticMarketData } from '@vegaprotocol/markets'; import type { Market, StaticMarketData } from '@vegaprotocol/markets';
@@ -45,7 +46,7 @@ export const TypeToggle = ({
return ( return (
<RadioGroup.Root <RadioGroup.Root
name="order-type" name="order-type"
className={classNames('mb-2 grid h-8 leading-8 font-alpha text-sm', { className={classNames('mb-2 grid h-8 leading-8 font-alpha text-xs', {
'grid-cols-3': FLAGS.STOP_ORDERS, 'grid-cols-3': FLAGS.STOP_ORDERS,
'grid-cols-2': !FLAGS.STOP_ORDERS, 'grid-cols-2': !FLAGS.STOP_ORDERS,
})} })}
@@ -81,11 +82,11 @@ export const TypeToggle = ({
} }
)} )}
> >
<button> <button className="flex gap-1">
<span className="text-ellipsis whitespace-nowrap shrink overflow-hidden"> <span className="text-ellipsis whitespace-nowrap shrink overflow-hidden">
{t(selectedOption ? selectedOption.label : 'Stop')} {t(selectedOption ? selectedOption.label : 'Stop')}
</span> </span>
<Icon name="chevron-down" className="ml-1" /> <VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
</button> </button>
</TradingDropdownTrigger> </TradingDropdownTrigger>
} }
+5 -8
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo } from 'react'; import { useMemo } from 'react';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
getDateTimeFormat, getDateTimeFormat,
@@ -6,7 +6,6 @@ import {
isNumeric, isNumeric,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { import type {
VegaICellRendererParams, VegaICellRendererParams,
@@ -17,10 +16,9 @@ import type { DepositFieldsFragment } from './__generated__/Deposit';
import { EtherscanLink } from '@vegaprotocol/environment'; import { EtherscanLink } from '@vegaprotocol/environment';
import { DepositStatusMapping } from '@vegaprotocol/types'; import { DepositStatusMapping } from '@vegaprotocol/types';
export const DepositsTable = forwardRef< export const DepositsTable = (
AgGridReact, props: TypedDataAgGrid<DepositFieldsFragment>
TypedDataAgGrid<DepositFieldsFragment> ) => {
>((props, ref) => {
const columnDefs = useMemo<ColDef[]>( const columnDefs = useMemo<ColDef[]>(
() => [ () => [
{ headerName: 'Asset', field: 'asset.symbol' }, { headerName: 'Asset', field: 'asset.symbol' },
@@ -79,11 +77,10 @@ export const DepositsTable = forwardRef<
); );
return ( return (
<AgGrid <AgGrid
ref={ref}
defaultColDef={{ flex: 1 }} defaultColDef={{ flex: 1 }}
columnDefs={columnDefs} columnDefs={columnDefs}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
{...props} {...props}
/> />
); );
}); };
@@ -9,7 +9,7 @@ export const NodeSwitcherDialog = ({
setOpen: (x: boolean) => void; setOpen: (x: boolean) => void;
}) => { }) => {
return ( return (
<Dialog open={open} onChange={setOpen} size="medium"> <Dialog open={open} onChange={setOpen} size="large">
<NodeSwitcher closeDialog={() => setOpen(false)} /> <NodeSwitcher closeDialog={() => setOpen(false)} />
</Dialog> </Dialog>
); );
+1 -4
View File
@@ -1,8 +1,7 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type * as Schema from '@vegaprotocol/types'; import type * as Schema from '@vegaprotocol/types';
import type { FilterChangedEvent } from 'ag-grid-community'; import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react'; import { useCallback, useState, useMemo } from 'react';
import { useCallback, useRef, useState, useMemo } from 'react';
import { subDays, formatRFC3339 } from 'date-fns'; import { subDays, formatRFC3339 } from 'date-fns';
import { ledgerEntriesProvider } from './ledger-entries-data-provider'; import { ledgerEntriesProvider } from './ledger-entries-data-provider';
import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries'; import type { LedgerEntriesQueryVariables } from './__generated__/LedgerEntries';
@@ -32,7 +31,6 @@ export const LedgerManager = ({
partyId: string; partyId: string;
gridProps: ReturnType<typeof useDataGridEvents>; gridProps: ReturnType<typeof useDataGridEvents>;
}) => { }) => {
const gridRef = useRef<AgGridReact | null>(null);
const [filter, setFilter] = useState<Filter>(defaultFilter); const [filter, setFilter] = useState<Filter>(defaultFilter);
const variables = useMemo<LedgerEntriesQueryVariables>( const variables = useMemo<LedgerEntriesQueryVariables>(
@@ -64,7 +62,6 @@ export const LedgerManager = ({
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<LedgerTable <LedgerTable
ref={gridRef}
rowData={data} rowData={data}
overlayNoRowsTemplate={error ? error.message : t('No entries')} overlayNoRowsTemplate={error ? error.message : t('No entries')}
{...gridProps} {...gridProps}
+150 -154
View File
@@ -14,7 +14,6 @@ import {
DateRangeFilter, DateRangeFilter,
SetFilter, SetFilter,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type * as Types from '@vegaprotocol/types'; import type * as Types from '@vegaprotocol/types';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import { import {
@@ -23,7 +22,7 @@ import {
TransferTypeMapping, TransferTypeMapping,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import type { LedgerEntry } from './ledger-entries-data-provider'; import type { LedgerEntry } from './ledger-entries-data-provider';
import { forwardRef, useMemo } from 'react'; import { useMemo } from 'react';
import { formatRFC3339, subDays } from 'date-fns'; import { formatRFC3339, subDays } from 'date-fns';
export const TransferTooltipCellComponent = ({ export const TransferTooltipCellComponent = ({
@@ -45,162 +44,159 @@ const dateRangeFilterParams = {
}; };
type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>; type LedgerEntryProps = TypedDataAgGrid<LedgerEntry>;
export const LedgerTable = forwardRef<AgGridReact, LedgerEntryProps>( export const LedgerTable = (props: LedgerEntryProps) => {
(props, ref) => { const columnDefs = useMemo<ColDef[]>(
const columnDefs = useMemo<ColDef[]>( () => [
() => [ {
{ headerName: t('Sender'),
headerName: t('Sender'), field: 'fromAccountPartyId',
field: 'fromAccountPartyId', cellRenderer: ({
cellRenderer: ({ value,
value, }: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) =>
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountPartyId'>) => truncateByChars(value || ''),
truncateByChars(value || ''), },
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
}, },
{ field: 'fromAccountType',
headerName: t('Account type'), cellRenderer: ({
filter: SetFilter, value,
filterParams: { }: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) =>
set: AccountTypeMapping, value ? AccountTypeMapping[value] : '-',
}, },
field: 'fromAccountType', {
cellRenderer: ({ headerName: t('Market'),
value, field: 'marketSender.tradableInstrument.instrument.code',
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountType'>) => cellRenderer: ({
value ? AccountTypeMapping[value] : '-', value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketSender.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Receiver'),
field: 'toAccountPartyId',
cellRenderer: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) =>
truncateByChars(value || ''),
},
{
headerName: t('Account type'),
filter: SetFilter,
filterParams: {
set: AccountTypeMapping,
}, },
{ field: 'toAccountType',
headerName: t('Market'), cellRenderer: ({
field: 'marketSender.tradableInstrument.instrument.code', value,
cellRenderer: ({ }: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) =>
value, value ? AccountTypeMapping[value] : '-',
}: VegaValueFormatterParams< },
LedgerEntry, {
'marketSender.tradableInstrument.instrument.code' headerName: t('Market'),
>) => value || '-', field: 'marketReceiver.tradableInstrument.instrument.code',
cellRenderer: ({
value,
}: VegaValueFormatterParams<
LedgerEntry,
'marketReceiver.tradableInstrument.instrument.code'
>) => value || '-',
},
{
headerName: t('Transfer type'),
field: 'transferType',
tooltipField: 'transferType',
filter: SetFilter,
filterParams: {
set: TransferTypeMapping,
}, },
{ valueFormatter: ({
headerName: t('Receiver'), value,
field: 'toAccountPartyId', }: VegaValueFormatterParams<LedgerEntry, 'transferType'>) =>
cellRenderer: ({ value ? TransferTypeMapping[value] : '',
value, },
}: VegaValueFormatterParams<LedgerEntry, 'toAccountPartyId'>) => {
truncateByChars(value || ''), headerName: t('Quantity'),
field: 'quantity',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
}, },
{ },
headerName: t('Account type'), {
filter: SetFilter, headerName: t('Asset'),
filterParams: { field: 'assetId',
set: AccountTypeMapping, valueFormatter: ({
}, value,
field: 'toAccountType', data,
cellRenderer: ({ }: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
value, data?.asset?.symbol || '',
}: VegaValueFormatterParams<LedgerEntry, 'toAccountType'>) => },
value ? AccountTypeMapping[value] : '-', {
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
}, },
{ },
headerName: t('Market'), {
field: 'marketReceiver.tradableInstrument.instrument.code', headerName: t('Receiver account balance'),
cellRenderer: ({ field: 'toAccountBalance',
value, valueFormatter: ({
}: VegaValueFormatterParams< value,
LedgerEntry, data,
'marketReceiver.tradableInstrument.instrument.code' }: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
>) => value || '-', const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
}, },
{ },
headerName: t('Transfer type'), {
field: 'transferType', headerName: t('Vega time'),
tooltipField: 'transferType', field: 'vegaTime',
filter: SetFilter, valueFormatter: ({
filterParams: { value,
set: TransferTypeMapping, }: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
}, value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
valueFormatter: ({ filterParams: dateRangeFilterParams,
value, filter: DateRangeFilter,
}: VegaValueFormatterParams<LedgerEntry, 'transferType'>) => flex: 1,
value ? TransferTypeMapping[value] : '', },
],
[]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
}, },
{ }}
headerName: t('Quantity'), columnDefs={columnDefs}
field: 'quantity', {...props}
valueFormatter: ({ />
value, );
data, };
}: VegaValueFormatterParams<LedgerEntry, 'quantity'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Asset'),
field: 'assetId',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'asset'>) =>
data?.asset?.symbol || '',
},
{
headerName: t('Sender account balance'),
field: 'fromAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'fromAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Receiver account balance'),
field: 'toAccountBalance',
valueFormatter: ({
value,
data,
}: VegaValueFormatterParams<LedgerEntry, 'toAccountBalance'>) => {
const assetDecimalPlaces = data?.asset?.decimals || 0;
return value
? addDecimalsFormatNumber(value, assetDecimalPlaces)
: '';
},
},
{
headerName: t('Vega time'),
field: 'vegaTime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<LedgerEntry, 'vegaTime'>) =>
value ? getDateTimeFormat().format(fromNanoSeconds(value)) : '-',
filterParams: dateRangeFilterParams,
filter: DateRangeFilter,
flex: 1,
},
],
[]
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
sortable: true,
tooltipComponent: TransferTooltipCellComponent,
filterParams: {
...dateRangeFilterParams,
buttons: ['reset'],
},
}}
columnDefs={columnDefs}
{...props}
/>
);
}
);
+153 -158
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo } from 'react'; import { useMemo } from 'react';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum, addDecimalsFormatNumberQuantum,
@@ -9,7 +9,6 @@ import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type { import type {
ColDef, ColDef,
ITooltipParams, ITooltipParams,
@@ -40,168 +39,164 @@ export interface LiquidityTableProps
quantum?: string | number; quantum?: string | number;
} }
export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>( export const LiquidityTable = ({
( symbol = '',
{ symbol = '', assetDecimalPlaces, stakeToCcyVolume, quantum, ...props }, assetDecimalPlaces,
ref stakeToCcyVolume,
) => { quantum,
const colDefs = useMemo(() => { ...props
const assetDecimalsFormatter = ({ value }: ITooltipParams) => { }: LiquidityTableProps) => {
if (!value) return '-'; const colDefs = useMemo(() => {
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`; const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
}; if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
const assetDecimalsQuantumFormatter = ({ const assetDecimalsQuantumFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value, value,
}: ValueFormatterParams) => { assetDecimalPlaces ?? 0,
if (!value) return '-'; quantum ?? 0
return `${addDecimalsFormatNumberQuantum( )}`;
value, };
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => { const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-'; if (!value) return '-';
const newValue = new BigNumber(value) const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1) .times(Number(stakeToCcyVolume) || 1)
.toString(); .toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`; return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
}; };
const stakeToCcyVolumeQuantumFormatter = ({ const stakeToCcyVolumeQuantumFormatter = ({
value, value,
}: ValueFormatterParams) => { }: ValueFormatterParams) => {
if (!value) return '-'; if (!value) return '-';
const newValue = new BigNumber(value) const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1) .times(Number(stakeToCcyVolume) || 1)
.toString(); .toString();
return `${addDecimalsFormatNumberQuantum( return `${addDecimalsFormatNumberQuantum(
newValue, newValue,
assetDecimalPlaces ?? 0, assetDecimalPlaces ?? 0,
quantum ?? 0 quantum ?? 0
)}`; )}`;
}; };
const defs: ColDef[] = [ const defs: ColDef[] = [
{ {
headerName: t('Party'), headerName: t('Party'),
field: 'party.id', field: 'party.id',
headerTooltip: t( headerTooltip: t('The public key of the party making this commitment.'),
'The public key of the party making this commitment.' },
), {
headerName: t(`Commitment (${symbol})`),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
'The amount committed to the market by this liquidity provider.'
),
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t(`Share`),
field: 'equityLikeShare',
type: 'rightAligned',
headerTooltip: t(
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.'
),
valueFormatter: percentageFormatter,
},
{
headerName: t('Proposed fee'),
headerTooltip: t(
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
}, },
{ },
headerName: t(`Commitment (${symbol})`), {
field: 'commitmentAmount', headerName: t('Created'),
type: 'rightAligned', headerTooltip: t(
headerTooltip: t( 'The date and time this liquidity provision was created.'
'The amount committed to the market by this liquidity provider.' ),
), field: 'createdAt',
valueFormatter: assetDecimalsQuantumFormatter, type: 'rightAligned',
tooltipValueGetter: assetDecimalsFormatter, valueFormatter: dateValueFormatter,
}, },
{ {
headerName: t(`Share`), headerName: t('Updated'),
field: 'equityLikeShare', headerTooltip: t(
type: 'rightAligned', 'The date and time this liquidity provision was last updated.'
headerTooltip: t( ),
'The equity-like share of liquidity of the market used to determine allocation of LP fees. Calculated based on share of total liquidity, with a premium added for length of commitment.' field: 'updatedAt',
), type: 'rightAligned',
valueFormatter: percentageFormatter, valueFormatter: dateValueFormatter,
}, },
{ ];
headerName: t('Proposed fee'), return defs;
headerTooltip: t( }, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
'The fee percentage (per trade) proposed by each liquidity provider.'
),
field: 'fee',
type: 'rightAligned',
valueFormatter: percentageFormatter,
},
{
headerName: t('Market valuation at entry'),
field: 'averageEntryValuation',
type: 'rightAligned',
headerTooltip: t(
'The valuation of the market at the time the liquidity commitment was made. Commitments made at a lower valuation earlier in the lifetime of the market would be expected to have a higher equity-like share if the market has grown. If a commitment is amended, value will reflect the average of the market valuations across the lifetime of the commitment.'
),
minWidth: 160,
valueFormatter: assetDecimalsQuantumFormatter,
tooltipValueGetter: assetDecimalsFormatter,
},
{
headerName: t('Obligation'),
field: 'commitmentAmount',
type: 'rightAligned',
headerTooltip: t(
`The liquidity provider's obligation to the market, calculated as the liquidity commitment amount multiplied by the value of the stake_to_ccy_volume network parameter to convert into units of liquidity volume. The obligation can be met by a combination of LP orders and limit orders on the order book.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Supplied'),
field: 'balance',
type: 'rightAligned',
headerTooltip: t(
`The amount of liquidity volume supplied by the LP order in order to meet the obligation. If the obligation is already met in full by other limit orders from the same Vega key the LP order is not required and this value will be zero. Also note if the target stake for the market is less than the obligation the full value of the obligation may not be required.`
),
valueFormatter: stakeToCcyVolumeQuantumFormatter,
tooltipValueGetter: stakeToCcyVolumeFormatter,
},
{
headerName: t('Status'),
headerTooltip: t('The current status of this liquidity provision.'),
field: 'status',
valueFormatter: ({ value }) => {
if (!value) return value;
return LiquidityProvisionStatusMapping[
value as LiquidityProvisionStatus
];
},
},
{
headerName: t('Created'),
headerTooltip: t(
'The date and time this liquidity provision was created.'
),
field: 'createdAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
{
headerName: t('Updated'),
headerTooltip: t(
'The date and time this liquidity provision was last updated.'
),
field: 'updatedAt',
type: 'rightAligned',
valueFormatter: dateValueFormatter,
},
];
return defs;
}, [assetDecimalPlaces, quantum, stakeToCcyVolume, symbol]);
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')} overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''} getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
ref={ref} tooltipShowDelay={500}
tooltipShowDelay={500} defaultColDef={{
defaultColDef={{ resizable: true,
resizable: true, minWidth: 100,
minWidth: 100, tooltipComponent: TooltipCellComponent,
tooltipComponent: TooltipCellComponent, sortable: true,
sortable: true, }}
}} {...props}
{...props} columnDefs={colDefs}
columnDefs={colDefs} />
/> );
); };
}
);
export default LiquidityTable; export default LiquidityTable;
@@ -1,11 +1,9 @@
import React, { forwardRef } from 'react';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid'; import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { import {
AgGridLazy as AgGrid, AgGridLazy as AgGrid,
PriceFlashCell, PriceFlashCell,
MarketNameCell, MarketNameCell,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { MarketMaybeWithData } from '../../markets-provider'; import type { MarketMaybeWithData } from '../../markets-provider';
import { OracleStatus } from './oracle-status'; import { OracleStatus } from './oracle-status';
import { useColumnDefs } from './use-column-defs'; import { useColumnDefs } from './use-column-defs';
@@ -43,14 +41,15 @@ const defaultColDef = {
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
minWidth: 100, minWidth: 100,
}; };
type Props = TypedDataAgGrid<MarketMaybeWithData> & {
export const MarketListTable = forwardRef< onMarketClick: (marketId: string, metaKey?: boolean) => void;
AgGridReact, SuccessorMarketRenderer?: React.FC<{ value: string }>;
TypedDataAgGrid<MarketMaybeWithData> & { };
onMarketClick: (marketId: string, metaKey?: boolean) => void; export const MarketListTable = ({
SuccessorMarketRenderer?: React.FC<{ value: string }>; onMarketClick,
} SuccessorMarketRenderer,
>(({ onMarketClick, SuccessorMarketRenderer, ...props }, ref) => { ...props
}: Props) => {
const columnDefs = useColumnDefs({ onMarketClick }); const columnDefs = useColumnDefs({ onMarketClick });
const components = { const components = {
PriceFlashCell, PriceFlashCell,
@@ -61,7 +60,6 @@ export const MarketListTable = forwardRef<
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
getRowId={getRowId} getRowId={getRowId}
ref={ref}
defaultColDef={defaultColDef} defaultColDef={defaultColDef}
columnDefs={columnDefs} columnDefs={columnDefs}
suppressCellFocus suppressCellFocus
@@ -69,6 +67,6 @@ export const MarketListTable = forwardRef<
{...props} {...props}
/> />
); );
}); };
export default MarketListTable; export default MarketListTable;
@@ -1,6 +1,5 @@
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useEffect, useRef } from 'react'; import React, { useEffect } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { CellClickedEvent } from 'ag-grid-community'; import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { MarketListTable } from './market-list-table'; import { MarketListTable } from './market-list-table';
@@ -18,8 +17,6 @@ export const MarketsContainer = ({
onSelect, onSelect,
SuccessorMarketRenderer, SuccessorMarketRenderer,
}: MarketsContainerProps) => { }: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, reload } = useDataProvider({ const { data, error, reload } = useDataProvider({
dataProvider, dataProvider,
variables: undefined, variables: undefined,
@@ -37,7 +34,6 @@ export const MarketsContainer = ({
return ( return (
<div className="h-full relative"> <div className="h-full relative">
<MarketListTable <MarketListTable
ref={gridRef}
rowData={data} rowData={data}
onCellClicked={(cellEvent: CellClickedEvent) => { onCellClicked={(cellEvent: CellClickedEvent) => {
const { data, column, event } = cellEvent; const { data, column, event } = cellEvent;
@@ -1 +0,0 @@
export * from './order-actions-dropdown';
@@ -1,13 +0,0 @@
import {
ActionsDropdown,
TradingDropdownCopyItem,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
export const OrderActionsDropdown = ({ id }: { id: string }) => {
return (
<ActionsDropdown data-testid="order-actions-content">
<TradingDropdownCopyItem value={id} text={t('Copy order ID')} />
</ActionsDropdown>
);
};
@@ -14,6 +14,7 @@ import {
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet'; import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog'; import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider'; import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
export enum Filter { export enum Filter {
'Open' = 'Open', 'Open' = 'Open',
@@ -42,6 +43,7 @@ export const OrderListManager = ({
}: OrderListManagerProps) => { }: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const [editOrder, setEditOrder] = useState<Order | null>(null); const [editOrder, setEditOrder] = useState<Order | null>(null);
const [viewOrder, setViewOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create); const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId); const hasAmendableOrder = useHasAmendableOrder(marketId);
const variables = const variables =
@@ -89,6 +91,7 @@ export const OrderListManager = ({
filter={filter} filter={filter}
onCancel={cancel} onCancel={cancel}
onEdit={setEditOrder} onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick} onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick} onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
@@ -134,6 +137,14 @@ export const OrderListManager = ({
}} }}
/> />
)} )}
{viewOrder && (
<OrderViewDialog
isOpen={Boolean(viewOrder)}
order={viewOrder}
onChange={() => setViewOrder(null)}
onMarketClick={onMarketClick}
/>
)}
</> </>
); );
}; };
@@ -6,7 +6,7 @@ import type { PartialDeep } from 'type-fest';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import type { OrderListTableProps } from '../'; import type { OrderFieldsFragment, OrderListTableProps } from '../';
import { OrderListTable } from '../'; import { OrderListTable } from '../';
import { import {
generateOrder, generateOrder,
@@ -27,6 +27,7 @@ const defaultProps: OrderListTableProps = {
rowData: [], rowData: [],
onEdit: jest.fn(), onEdit: jest.fn(),
onCancel: jest.fn(), onCancel: jest.fn(),
onView: jest.fn(),
isReadOnly: false, isReadOnly: false,
}; };
@@ -110,6 +111,39 @@ describe('OrderListTable', () => {
); );
}); });
it('should apply correct formatting applied for an iceberg order', async () => {
const icebergOrder = {
...limitOrder,
icebergOrder: {
__typename: 'IcebergOrder',
minimumVisibleSize: '100',
peakSize: '50',
reservedRemaining: '50',
} as OrderFieldsFragment['icebergOrder'],
};
await act(async () => {
render(generateJsx({ rowData: [icebergOrder] }));
});
const cells = screen.getAllByRole('gridcell');
const expectedValues: string[] = [
icebergOrder.market?.tradableInstrument.instrument.code || '',
'0.05',
'0.10',
Schema.OrderTypeMapping[
icebergOrder.type || Schema.OrderType.TYPE_LIMIT
] + ' (Iceberg)',
Schema.OrderStatusMapping[icebergOrder.status],
'-',
`${
Schema.OrderTimeInForceCode[icebergOrder.timeInForce]
}: ${getDateTimeFormat().format(new Date(icebergOrder.expiresAt ?? ''))}`,
getDateTimeFormat().format(new Date(icebergOrder.createdAt)),
];
expectedValues.forEach((expectedValue, i) =>
expect(cells[i]).toHaveTextContent(expectedValue)
);
});
it('should apply correct formatting for a rejected order', async () => { it('should apply correct formatting for a rejected order', async () => {
const rejectedOrder = { const rejectedOrder = {
...marketOrder, ...marketOrder,
@@ -22,6 +22,9 @@ const Template: Story = (args) => {
onEdit={() => { onEdit={() => {
return; return;
}} }}
onView={() => {
return;
}}
isReadOnly={false} isReadOnly={false}
/> />
</div> </div>
@@ -49,6 +52,9 @@ const Template2: Story = (args) => {
rowData={args.data} rowData={args.data}
onCancel={cancel} onCancel={cancel}
onEdit={setEditOrder} onEdit={setEditOrder}
onView={() => {
return;
}}
isReadOnly={false} isReadOnly={false}
/> />
</div> </div>
@@ -6,7 +6,14 @@ import {
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import {
ActionsDropdown,
ButtonLink,
TradingDropdownCopyItem,
DropdownMenuItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import { memo, forwardRef, useMemo } from 'react'; import { memo, forwardRef, useMemo } from 'react';
import { import {
@@ -27,7 +34,6 @@ import type {
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider'; import type { Order } from '../order-data-provider';
import { OrderActionsDropdown } from '../order-actions-dropdown';
import { Filter } from '../order-list-manager'; import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
@@ -35,6 +41,7 @@ export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string; marketId?: string;
onCancel: (order: Order) => void; onCancel: (order: Order) => void;
onEdit: (order: Order) => void; onEdit: (order: Order) => void;
onView: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void; onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
filter?: Filter; filter?: Filter;
@@ -46,7 +53,15 @@ export const OrderListTable = memo<
>( >(
forwardRef<AgGridReact, OrderListTableProps>( forwardRef<AgGridReact, OrderListTableProps>(
( (
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props }, {
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
filter,
...props
},
ref ref
) => { ) => {
const showAllActions = props.isReadOnly const showAllActions = props.isReadOnly
@@ -69,6 +84,18 @@ export const OrderListTable = memo<
cellClass: 'font-mono text-right', cellClass: 'font-mono text-right',
type: 'rightAligned', type: 'rightAligned',
valueGetter: ({ data }: VegaValueGetterParams<Order>) => { valueGetter: ({ data }: VegaValueGetterParams<Order>) => {
if (data?.icebergOrder) {
return data?.size && data.market
? toBigNum(
(
BigInt(data.size) -
BigInt(data.remaining) -
BigInt(data.icebergOrder.reservedRemaining)
).toString(),
data.market.positionDecimalPlaces ?? 0
).toNumber()
: undefined;
}
return data?.size && data.market return data?.size && data.market
? toBigNum( ? toBigNum(
(BigInt(data.size) - BigInt(data.remaining)).toString(), (BigInt(data.size) - BigInt(data.remaining)).toString(),
@@ -283,7 +310,20 @@ export const OrderListTable = memo<
</ButtonLink> </ButtonLink>
</> </>
)} )}
<OrderActionsDropdown id={data?.id} /> <ActionsDropdown data-testid="market-actions-content">
<TradingDropdownCopyItem
value={data.id}
text={t('Copy order ID')}
/>
<DropdownMenuItem
key={'view-order'}
data-testid="view-order"
onClick={() => onView(data)}
>
<VegaIcon name={VegaIconNames.INFO} size={16} />
{t('View order details')}
</DropdownMenuItem>
</ActionsDropdown>
</div> </div>
); );
}, },
@@ -293,6 +333,7 @@ export const OrderListTable = memo<
filter, filter,
onCancel, onCancel,
onEdit, onEdit,
onView,
onMarketClick, onMarketClick,
onOrderTypeClick, onOrderTypeClick,
props.isReadOnly, props.isReadOnly,
@@ -0,0 +1,177 @@
import { render, screen } from '@testing-library/react';
import { OrderViewDialog } from './order-view-dialog';
import type { Order } from '../order-data-provider';
import { BrowserRouter } from 'react-router-dom';
import {
MarketState,
MarketTradingMode,
OrderStatus,
OrderTimeInForce,
OrderType,
Side,
} from '@vegaprotocol/types';
describe('OrderViewDialog', () => {
it('should render the order view dialog if the order is provided', async () => {
const order: Order = {
id: '7f2a78f370062e41683d26a0fcc4521b2bd4b747530b78bc6bc86195db0e5fb3',
market: {
__typename: 'Market',
id: 'b66cd4be223dfd900a4750bb5175e17d8f678996877d262be4c749a99e22a970',
decimalPlaces: 5,
positionDecimalPlaces: 3,
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
makerFee: '0.0002',
infrastructureFee: '0.0005',
liquidityFee: '0.001',
},
},
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '',
name: 'Tesla Quarterly (Sep 2023)',
code: 'TSLA.QM21',
metadata: {
__typename: 'InstrumentMetadata',
tags: [
'formerly:5A86B190C384997F',
'quote:EURO',
'ticker:TSLA',
'class:equities/single-stock-futures',
'sector:tech',
'listing_venue:NASDAQ',
'country:US',
'auto:tsla',
],
},
product: {
__typename: 'Future',
settlementAsset: {
__typename: 'Asset',
id: '177e8f6c25a955bd18475084b99b2b1d37f28f3dec393fab7755a7e69c3d8c3b',
symbol: 'tEURO',
name: 'tEURO Fairground',
decimals: 5,
quantum: '1',
},
quoteName: 'EURO',
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: '3f5941ba047dabb3bec23f112db0a8cc8aecbbbc6b34d1366d9383ed1b0f39df',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: '05ca5485678aa7c705aa6a683fd062714cbcfdac2d1a27641cd27b66168d9c1d',
data: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionExternal',
sourceType: {
__typename: 'DataSourceSpecConfiguration',
signers: [
{
__typename: 'Signer',
signer: {
__typename: 'PubKey',
key: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
},
},
],
filters: [],
},
},
},
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: 'prices.TSLA.value',
tradingTerminationProperty: 'termination.TSLA.value',
},
},
},
},
marketTimestamps: {
__typename: 'MarketTimestamps',
open: '2023-07-19T12:05:25.822854221Z',
close: null,
},
},
type: OrderType.TYPE_LIMIT,
side: Side.SIDE_BUY,
size: '10000',
status: OrderStatus.STATUS_ACTIVE,
rejectionReason: null,
price: '15000000',
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTT,
remaining: '5000',
expiresAt: null,
createdAt: '2023-08-01T14:47:34.977742Z',
updatedAt: null,
postOnly: false,
reduceOnly: false,
liquidityProvision: null,
peggedOrder: null,
icebergOrder: {
__typename: 'IcebergOrder',
peakSize: '5000',
minimumVisibleSize: '2000',
reservedRemaining: '5000',
},
__typename: 'Order',
};
render(
<BrowserRouter>
<OrderViewDialog order={order} onChange={jest.fn()} isOpen={true} />
</BrowserRouter>
);
expect(screen.getByTestId('order-market-label')).toHaveTextContent(
'Market'
);
expect(screen.getByTestId('order-market-value')).toHaveTextContent(
'Tesla Quarterly (Sep 2023)'
);
expect(screen.getByTestId('order-type-label')).toHaveTextContent('Type');
expect(screen.getByTestId('order-type-value')).toHaveTextContent('Limit');
expect(screen.getByTestId('order-price-label')).toHaveTextContent('Price');
expect(screen.getByTestId('order-price-value')).toHaveTextContent('150.00');
expect(screen.getByTestId('order-size-label')).toHaveTextContent('Size');
expect(screen.getByTestId('order-size-value')).toHaveTextContent('+10.00');
expect(screen.getByTestId('order-remaining-label')).toHaveTextContent(
'Remaining'
);
expect(screen.getByTestId('order-remaining-value')).toHaveTextContent(
'+5.00'
);
expect(
screen.getByTestId('order-iceberg-order-reserved-remaining-value')
).toHaveTextContent('5.00');
});
});
@@ -0,0 +1,280 @@
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { Size } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import {
ButtonLink,
Dialog,
KeyValueTable,
KeyValueTableRow,
Tooltip,
VegaIcon,
VegaIconNames,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import type { Order } from '../order-data-provider';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useCopyTimeout } from '@vegaprotocol/react-helpers';
interface OrderViewDialogProps {
isOpen: boolean;
order: Order;
onChange: (open: boolean) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}
export const OrderViewDialog = ({
isOpen,
order,
onChange,
onMarketClick,
}: OrderViewDialogProps) => {
const [, setCopied] = useCopyTimeout();
return (
<Dialog open={isOpen} title={t('Order details')} onChange={onChange}>
<KeyValueTable>
<KeyValueTableRow key={'order-market'}>
<div data-testid={'order-market-label'}>{t('Market')}</div>
<div data-testid={`order-market-value`}>
{onMarketClick ? (
<ButtonLink
onClick={() => order.market && onMarketClick(order.market?.id)}
>
{order.market?.tradableInstrument.instrument.name}
</ButtonLink>
) : (
order.market?.tradableInstrument.instrument.name
)}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-type'}>
<div data-testid={'order-type-label'}>{t('Type')}</div>
<div data-testid={`order-type-value`}>
{Schema.OrderTypeMapping[order.type as Schema.OrderType]}
</div>
</KeyValueTableRow>
{order.market && (
<KeyValueTableRow key={'order-price'}>
<div data-testid={'order-price-label'}>{t('Price')}</div>
<div data-testid={`order-price-value`}>
{addDecimalsFormatNumber(
order.price,
order.market.decimalPlaces as number
)}
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-size'}>
<div data-testid={'order-size-label'}>{t('Size')}</div>
<div data-testid={`order-size-value`}>
<Size
value={order.size}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.market && (
<KeyValueTableRow key={'order-remaining'} className="mb-4">
<div data-testid={'order-remaining-label'}>{t('Remaining')}</div>
<div data-testid={`order-remaining-value`}>
<Size
value={order.remaining}
side={order.side}
positionDecimalPlaces={
order.market.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-status'}>
<div data-testid={'order-status-label'}>{t('Status')}</div>
<div data-testid={`order-status-value`}>
{Schema.OrderStatusMapping[order.status as Schema.OrderStatus]}
</div>
</KeyValueTableRow>
{order.rejectionReason && (
<KeyValueTableRow key={'order-rejection-reason'}>
<div data-testid={'order-rejection-reason-label'}>
{t('rejection reason')}
</div>
<div data-testid={`order-rejection-reason-value`}>
{
Schema.OrderRejectionReasonMapping[
order.rejectionReason as Schema.OrderRejectionReason
]
}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-id'}>
<div data-testid={'order-id-label'}>{t('Order ID')}</div>
<div data-testid={`order-id-value`}>
{truncateMiddle(order.id, 10)}
<CopyToClipboard text={order.id} onCopy={() => setCopied(true)}>
<button
type="button"
data-testid="copy-order-id"
onClick={(e) => e.stopPropagation()}
>
<span className="sr-only">{t('Copy')}</span>
<VegaIcon name={VegaIconNames.COPY} />
</button>
</CopyToClipboard>
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-created'}>
<div data-testid={'order-created-label'}>{t('Created')}</div>
<div data-testid={`order-created-value`}>
{getDateTimeFormat().format(new Date(order.createdAt))}
</div>
</KeyValueTableRow>
{order.updatedAt && (
<KeyValueTableRow key={'order-updated'}>
<div data-testid={'order-updated-label'}>{t('Updated')}</div>
<div data-testid={`order-updated-value`}>
{getDateTimeFormat().format(new Date(order.updatedAt))}
</div>
</KeyValueTableRow>
)}
{order.expiresAt && (
<KeyValueTableRow key={'order-expires'}>
<div data-testid={'order-expires-label'}>{t('Expires')}</div>
<div data-testid={`order-expires-value`}>
{getDateTimeFormat().format(new Date(order.expiresAt))}
</div>
</KeyValueTableRow>
)}
<KeyValueTableRow key={'order-time-in-force'} className="mt-4">
<div data-testid={'order-time-in-force-label'}>
{t('Time in force')}
</div>
<div data-testid={`order-time-in-force-value`}>
{
Schema.OrderTimeInForceMapping[
order.timeInForce as Schema.OrderTimeInForce
]
}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-post-only'}>
<div data-testid={'order-post-only-label'}>{t('Post only')}</div>
<div data-testid={`order-post-only-value`}>
{order.postOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-reduce-only'}>
<div data-testid={'order-reduce-only-label'}>{t('Reduce only')}</div>
<div data-testid={`order-reduce-only-value`}>
{order.reduceOnly ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-pegged'}>
<div data-testid={'order-pegged-label'}>{t('Pegged')}</div>
<div data-testid={`order-pegged-value`}>
{order.peggedOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
<KeyValueTableRow key={'order-liquidity-provision'}>
<div data-testid={'order-liquidity-provision-label'}>
{t('Liquidity provision')}
</div>
<div data-testid={`order-liquidity-provision-value`}>
{order.liquidityProvision ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
</KeyValueTable>
<KeyValueTableRow key={'order-iceberg-order'}>
<div data-testid={'order-iceberg-order-label'}>
{t('Iceberg order')}
</div>
<div data-testid={`order-iceberg-order-value`}>
{order.icebergOrder ? t('Yes') : t('-')}
</div>
</KeyValueTableRow>
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-peak-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-peak-size-label'}>
<Tooltip
description={t(
'The maximum volume that can be traded at once. Must be less than the total size of the order.'
)}
>
<span>{t('Peak size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-peak-size-value`}>
<Size
value={order.icebergOrder.peakSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-minimum-visible-size'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-minimum-visible-size-label'}>
<Tooltip
description={t(
'When the order trades and its size falls below this threshold, it will be reset to the peak size and moved to the back of the priority order. Must be less than or equal to peak size, and greater than 0.'
)}
>
<span>{t('Minimum size')}</span>
</Tooltip>
</div>
<div data-testid={`order-iceberg-order-minimum-visible-size-value`}>
<Size
value={order.icebergOrder.minimumVisibleSize}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
{order.icebergOrder && (
<KeyValueTableRow
key={'order-iceberg-order-reserved-remaining'}
className="ml-4"
>
<div data-testid={'order-iceberg-order-reserved-remaining-label'}>
{t('Reserved remaining')}
</div>
<div data-testid={`order-iceberg-order-reserved-remaining-value`}>
<Size
value={order.icebergOrder.reservedRemaining}
side={order.side}
positionDecimalPlaces={
order.market?.positionDecimalPlaces as number
}
/>
</div>
</KeyValueTableRow>
)}
</Dialog>
);
};
@@ -2,17 +2,13 @@ import { render, screen, act } from '@testing-library/react';
import { StopOrdersManager } from './stop-orders-manager'; import { StopOrdersManager } from './stop-orders-manager';
import * as useDataProviderHook from '@vegaprotocol/data-provider'; import * as useDataProviderHook from '@vegaprotocol/data-provider';
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider'; import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
import * as stopOrdersTableMock from '../stop-orders-table/stop-orders-table';
import { forwardRef } from 'react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
// @ts-ignore StopOrdersTable is read only but we need to override with the forwardRef to jest.mock('../stop-orders-table/stop-orders-table', () => ({
// avoid warnings about padding refs StopOrdersTable: () => <div>StopOrdersTable</div>,
stopOrdersTableMock.StopOrdersTable = forwardRef(() => ( }));
<div>StopOrdersTable</div>
));
const generateJsx = () => { const generateJsx = () => {
const pubKey = '0x123'; const pubKey = '0x123';
@@ -8,7 +8,7 @@ import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import { memo, forwardRef, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { import {
AgGridLazy as AgGrid, AgGridLazy as AgGrid,
SetFilter, SetFilter,
@@ -36,265 +36,252 @@ export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
export const StopOrdersTable = memo< export const StopOrdersTable = memo<
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> } StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
>( >(({ onCancel, onMarketClick, ...props }: StopOrdersTableProps) => {
forwardRef<AgGridReact, StopOrdersTableProps>( const showAllActions = !props.isReadOnly;
({ onCancel, onMarketClick, ...props }, ref) => { const columnDefs: ColDef[] = useMemo(
const showAllActions = !props.isReadOnly; () => [
const columnDefs: ColDef[] = useMemo( {
() => [ headerName: t('Market'),
{ field: 'market.tradableInstrument.instrument.code',
headerName: t('Market'), cellRenderer: 'MarketNameCell',
field: 'market.tradableInstrument.instrument.code', cellRendererParams: { idPath: 'market.id', onMarketClick },
cellRenderer: 'MarketNameCell', minWidth: 150,
cellRendererParams: { idPath: 'market.id', onMarketClick }, },
minWidth: 150, {
}, headerName: t('Trigger'),
{ field: 'trigger',
headerName: t('Trigger'), cellClass: 'font-mono text-right',
field: 'trigger', type: 'rightAligned',
cellClass: 'font-mono text-right', sortable: false,
type: 'rightAligned', valueFormatter: ({
sortable: false, data,
valueFormatter: ({ value,
data, }: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
value, if (data && value?.__typename === 'StopOrderPrice') {
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => { return `${t('Mark')} ${
if (data && value?.__typename === 'StopOrderPrice') { data?.triggerDirection ===
return `${t('Mark')} ${ Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
data?.triggerDirection === ? '<'
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW : '>'
? '<' } ${addDecimalsFormatNumber(
: '>' value.price,
} ${addDecimalsFormatNumber( data.market.decimalPlaces
value.price, )}`;
data.market.decimalPlaces }
)}`; if (data && value?.__typename === 'StopOrderTrailingPercentOffset') {
} return `${t('Mark')} ${
if ( data?.triggerDirection ===
data && Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
value?.__typename === 'StopOrderTrailingPercentOffset' ? '+'
) { : '-'
return `${t('Mark')} ${ }${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
data?.triggerDirection === }
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW return '-';
? '+' },
: '-' minWidth: 100,
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`; },
} {
return '-'; field: 'expiresAt',
}, valueFormatter: ({
minWidth: 100, value,
}, data,
{ }: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
field: 'expiresAt', if (
valueFormatter: ({ data &&
value, value &&
data, data?.expiryStrategy !==
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => { Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
if ( ) {
data && const expiresAt = getDateTimeFormat().format(new Date(value));
value && const expiryStrategy =
data?.expiryStrategy !== data.expiryStrategy ===
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
) { ? t('Submit')
const expiresAt = getDateTimeFormat().format(new Date(value)); : t('Cancels');
const expiryStrategy = return `${expiryStrategy} ${expiresAt}`;
data.expiryStrategy === }
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT return '';
? t('Submit') },
: t('Cancels'); minWidth: 150,
return `${expiryStrategy} ${expiresAt}`; },
} {
return ''; headerName: t('Size'),
}, field: 'submission.size',
minWidth: 150, cellClass: 'font-mono text-right',
}, type: 'rightAligned',
{ cellClassRules: {
headerName: t('Size'), [positiveClassNames]: ({ data }: { data: StopOrder }) =>
field: 'submission.size', data?.submission.size === Schema.Side.SIDE_BUY,
cellClass: 'font-mono text-right', [negativeClassNames]: ({ data }: { data: StopOrder }) =>
type: 'rightAligned', data?.submission.size === Schema.Side.SIDE_SELL,
cellClassRules: { },
[positiveClassNames]: ({ data }: { data: StopOrder }) => valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
data?.submission.size === Schema.Side.SIDE_BUY, return data?.submission.size && data.market
[negativeClassNames]: ({ data }: { data: StopOrder }) => ? toBigNum(
data?.submission.size === Schema.Side.SIDE_SELL, data.submission.size,
}, data.market.positionDecimalPlaces ?? 0
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => { )
return data?.submission.size && data.market .multipliedBy(
? toBigNum( data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
data.submission.size,
data.market.positionDecimalPlaces ?? 0
)
.multipliedBy(
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
)
.toNumber()
: undefined;
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
if (!data) {
return '';
}
if (!data?.market || !isNumeric(data.submission.size)) {
return '-';
}
const prefix = data
? data.submission.side === Schema.Side.SIDE_BUY
? '+'
: '-'
: '';
return (
prefix +
addDecimalsFormatNumber(
data.submission.size,
data.market.positionDecimalPlaces
) )
); .toNumber()
}, : undefined;
minWidth: 80, },
}, valueFormatter: ({
{ data,
field: 'submission.type', }: VegaValueFormatterParams<StopOrder, 'size'>) => {
filter: SetFilter, if (!data) {
filterParams: { return '';
set: Schema.OrderTypeMapping, }
}, if (!data?.market || !isNumeric(data.submission.size)) {
cellRenderer: ({ return '-';
value, }
}: VegaICellRendererParams<StopOrder, 'submission.type'>) => const prefix = data
value ? Schema.OrderTypeMapping[value] : '', ? data.submission.side === Schema.Side.SIDE_BUY
minWidth: 80, ? '+'
}, : '-'
{ : '';
field: 'status', return (
filter: SetFilter, prefix +
filterParams: { addDecimalsFormatNumber(
set: Schema.StopOrderStatusMapping, data.submission.size,
}, data.market.positionDecimalPlaces
valueFormatter: ({ )
value, );
}: VegaValueFormatterParams<StopOrder, 'status'>) => { },
return value ? Schema.StopOrderStatusMapping[value] : ''; minWidth: 80,
}, },
cellRenderer: ({ {
valueFormatted, field: 'submission.type',
data, filter: SetFilter,
}: { filterParams: {
valueFormatted: string; set: Schema.OrderTypeMapping,
data: StopOrder; },
}) => ( cellRenderer: ({
<span data-testid={`order-status-${data?.id}`}> value,
{valueFormatted} }: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
</span> value ? Schema.OrderTypeMapping[value] : '',
), minWidth: 80,
minWidth: 100, },
}, {
{ field: 'status',
field: 'submission.price', filter: SetFilter,
type: 'rightAligned', filterParams: {
cellClass: 'font-mono text-right', set: Schema.StopOrderStatusMapping,
valueFormatter: ({ },
value, valueFormatter: ({
data, value,
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => { }: VegaValueFormatterParams<StopOrder, 'status'>) => {
if (!data) { return value ? Schema.StopOrderStatusMapping[value] : '';
return ''; },
} cellRenderer: ({
if ( valueFormatted,
!data?.market || data,
data.submission.type === Schema.OrderType.TYPE_MARKET || }: {
!isNumeric(value) valueFormatted: string;
) { data: StopOrder;
return '-'; }) => (
} <span data-testid={`order-status-${data?.id}`}>{valueFormatted}</span>
return addDecimalsFormatNumber(value, data.market.decimalPlaces); ),
}, minWidth: 100,
minWidth: 100, },
}, {
{ field: 'submission.price',
field: 'submission.timeInForce', type: 'rightAligned',
filter: SetFilter, cellClass: 'font-mono text-right',
filterParams: { valueFormatter: ({
set: Schema.OrderTimeInForceMapping, value,
}, data,
valueFormatter: ({ }: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
value, if (!data) {
}: VegaValueFormatterParams< return '';
StopOrder, }
'submission.timeInForce' if (
>) => { !data?.market ||
return value ? Schema.OrderTimeInForceCode[value] : ''; data.submission.type === Schema.OrderType.TYPE_MARKET ||
}, !isNumeric(value)
minWidth: 150, ) {
}, return '-';
{ }
field: 'updatedAt', return addDecimalsFormatNumber(value, data.market.decimalPlaces);
filter: DateRangeFilter, },
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => minWidth: 100,
data?.updatedAt || data?.createdAt, },
cellRenderer: ({ {
data, field: 'submission.timeInForce',
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => { filter: SetFilter,
if (!data) { filterParams: {
return undefined; set: Schema.OrderTimeInForceMapping,
} },
const value = data.updatedAt || data.createdAt; valueFormatter: ({
return ( value,
<span data-value={value}> }: VegaValueFormatterParams<StopOrder, 'submission.timeInForce'>) => {
{value ? getDateTimeFormat().format(new Date(value)) : '-'} return value ? Schema.OrderTimeInForceCode[value] : '';
</span> },
); minWidth: 150,
}, },
minWidth: 150, {
}, field: 'updatedAt',
{ filter: DateRangeFilter,
colId: 'actions', valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
...COL_DEFS.actions, data?.updatedAt || data?.createdAt,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth, cellRenderer: ({
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth, data,
cellRenderer: ({ data }: { data?: StopOrder }) => { }: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
if (!data) return null; if (!data) {
return undefined;
}
const value = data.updatedAt || data.createdAt;
return (
<span data-value={value}>
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
</span>
);
},
minWidth: 150,
},
{
colId: 'actions',
...COL_DEFS.actions,
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
cellRenderer: ({ data }: { data?: StopOrder }) => {
if (!data) return null;
return ( return (
<div className="flex gap-2 items-center justify-end"> <div className="flex gap-2 items-center justify-end">
{data.status === Schema.StopOrderStatus.STATUS_PENDING && {data.status === Schema.StopOrderStatus.STATUS_PENDING &&
!props.isReadOnly && ( !props.isReadOnly && (
<ButtonLink <ButtonLink
data-testid="cancel" data-testid="cancel"
onClick={() => onCancel(data)} onClick={() => onCancel(data)}
> >
{t('Cancel')} {t('Cancel')}
</ButtonLink> </ButtonLink>
)} )}
</div> </div>
); );
}, },
}, },
], ],
[onCancel, onMarketClick, props.isReadOnly, showAllActions] [onCancel, onMarketClick, props.isReadOnly, showAllActions]
); );
return ( return (
<AgGrid <AgGrid
ref={ref} defaultColDef={{
defaultColDef={{ resizable: true,
resizable: true, sortable: true,
sortable: true, filterParams: { buttons: ['reset'] },
filterParams: { buttons: ['reset'] }, }}
}} columnDefs={columnDefs}
columnDefs={columnDefs} style={{
style={{ width: '100%',
width: '100%', height: '100%',
height: '100%', }}
}} getRowId={({ data }) => data.id}
getRowId={({ data }) => data.id} components={{ MarketNameCell }}
components={{ MarketNameCell }} {...props}
{...props} />
/> );
); });
}
)
);
+348 -365
View File
@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { forwardRef, useMemo } from 'react'; import { useMemo } from 'react';
import type { CSSProperties, ReactNode } from 'react'; import type { CSSProperties, ReactNode } from 'react';
import type { ColDef } from 'ag-grid-community'; import type { ColDef } from 'ag-grid-community';
import type { import type {
@@ -33,7 +33,6 @@ import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { AgGridReact } from 'ag-grid-react';
import type { Position } from './positions-data-providers'; import type { Position } from './positions-data-providers';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types'; import { PositionStatus, PositionStatusMapping } from '@vegaprotocol/types';
@@ -87,383 +86,367 @@ AmountCell.displayName = 'AmountCell';
export const getRowId = ({ data }: { data: Position }) => export const getRowId = ({ data }: { data: Position }) =>
`${data.partyId}-${data.marketId}`; `${data.partyId}-${data.marketId}`;
export const PositionsTable = forwardRef<AgGridReact, Props>( export const PositionsTable = ({
( onClose,
{ onMarketClick,
onClose, multipleKeys,
onMarketClick, isReadOnly,
multipleKeys, pubKeys,
isReadOnly, pubKey,
pubKeys, ...props
pubKey, }: Props) => {
...props const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
}, return (
ref <AgGrid
) => { style={{ width: '100%', height: '100%' }}
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); overlayNoRowsTemplate={t('No positions')}
return ( getRowId={getRowId}
<AgGrid tooltipShowDelay={500}
style={{ width: '100%', height: '100%' }} defaultColDef={{
overlayNoRowsTemplate={t('No positions')} resizable: true,
getRowId={getRowId} sortable: true,
ref={ref} filter: true,
tooltipShowDelay={500} filterParams: { buttons: ['reset'] },
defaultColDef={{ tooltipComponent: TooltipCellComponent,
resizable: true, }}
sortable: true, components={{
filter: true, AmountCell,
filterParams: { buttons: ['reset'] }, PriceFlashCell,
tooltipComponent: TooltipCellComponent, ProgressBarCell,
}} MarketNameCell,
components={{ }}
AmountCell, {...props}
PriceFlashCell, columnDefs={useMemo<ColDef[]>(() => {
ProgressBarCell, const columnDefs: (ColDef | null)[] = [
MarketNameCell, multipleKeys
}} ? {
{...props} headerName: t('Vega key'),
columnDefs={useMemo<ColDef[]>(() => { field: 'partyId',
const columnDefs: (ColDef | null)[] = [ valueGetter: ({ data }: VegaValueGetterParams<Position>) =>
multipleKeys (data?.partyId &&
? { pubKeys &&
headerName: t('Vega key'), pubKeys.find((key) => key.publicKey === data.partyId)
field: 'partyId', ?.name) ||
valueGetter: ({ data }: VegaValueGetterParams<Position>) => data?.partyId,
(data?.partyId && minWidth: 190,
pubKeys && }
pubKeys.find((key) => key.publicKey === data.partyId) : null,
?.name) || {
data?.partyId, headerName: t('Market'),
minWidth: 190, field: 'marketName',
} cellRenderer: 'MarketNameCell',
: null, cellRendererParams: { idPath: 'marketId', onMarketClick },
{ minWidth: 190,
headerName: t('Market'), },
field: 'marketName', {
cellRenderer: 'MarketNameCell', headerName: t('Notional'),
cellRendererParams: { idPath: 'marketId', onMarketClick }, headerTooltip: t('Mark price x open volume.'),
minWidth: 190, field: 'notional',
type: 'rightAligned',
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data?.notional
? undefined
: toBigNum(data.notional, data.marketDecimalPlaces).toNumber();
}, },
{ valueFormatter: ({
headerName: t('Notional'), data,
headerTooltip: t('Mark price x open volume.'), }: VegaValueFormatterParams<Position, 'notional'>) => {
field: 'notional', return !data || !data.notional
type: 'rightAligned', ? '-'
cellClass: 'font-mono text-right', : addDecimalsFormatNumber(
filter: 'agNumberColumnFilter', data.notional,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { data.marketDecimalPlaces
return !data?.notional );
? undefined
: toBigNum(
data.notional,
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'notional'>) => {
return !data || !data.notional
? '-'
: addDecimalsFormatNumber(
data.notional,
data.marketDecimalPlaces
);
},
minWidth: 80,
}, },
{ minWidth: 80,
headerName: t('Open volume'), },
field: 'openVolume', {
type: 'rightAligned', headerName: t('Open volume'),
cellClass: 'font-mono text-right', field: 'openVolume',
cellClassRules: signedNumberCssClassRules, type: 'rightAligned',
filter: 'agNumberColumnFilter', cellClass: 'font-mono text-right',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { cellClassRules: signedNumberCssClassRules,
return data?.openVolume === undefined filter: 'agNumberColumnFilter',
? undefined valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
: toBigNum( return data?.openVolume === undefined
data?.openVolume, ? undefined
: toBigNum(
data?.openVolume,
data.positionDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => {
return data?.openVolume === undefined
? ''
: volumePrefix(
addDecimalsFormatNumber(
data.openVolume,
data.positionDecimalPlaces data.positionDecimalPlaces
).toNumber(); )
}, );
valueFormatter: ({ },
data, cellRenderer: OpenVolumeCell,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => { minWidth: 100,
return data?.openVolume === undefined },
? '' {
: volumePrefix( headerName: t('Mark price'),
addDecimalsFormatNumber( field: 'markPrice',
data.openVolume, type: 'rightAligned',
data.positionDecimalPlaces cellRenderer: PriceFlashCell,
) filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data ||
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
? undefined
: toBigNum(data.markPrice, data.marketDecimalPlaces).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
{
headerName: t('Liquidation price'),
colId: 'liquidationPrice',
type: 'rightAligned',
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<LiquidationPrice
marketId={data.marketId}
openVolume={data.openVolume}
collateralAvailable={data.totalBalance}
decimalPlaces={data.decimals}
formatDecimals={data.marketDecimalPlaces}
/>
);
},
},
{
headerName: t('Settlement asset'),
field: 'assetSymbol',
colId: 'asset',
minWidth: 100,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data) return null;
return (
<ButtonLink
title={t('View settlement asset details')}
onClick={(e) => {
openAssetDetailsDialog(
data.assetId,
e.target as HTMLElement
); );
}, }}
cellRenderer: OpenVolumeCell, >
minWidth: 100, {data?.assetSymbol}
</ButtonLink>
);
}, },
{ },
headerName: t('Mark price'), {
field: 'markPrice', headerName: t('Entry price'),
type: 'rightAligned', field: 'averageEntryPrice',
cellRenderer: PriceFlashCell, type: 'rightAligned',
filter: 'agNumberColumnFilter', cellRenderer: PriceFlashCell,
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { filter: 'agNumberColumnFilter',
return !data || valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
!data.markPrice || return data?.markPrice === undefined || !data
data.marketTradingMode === ? undefined
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION : toBigNum(
? undefined data.averageEntryPrice,
: toBigNum( data.marketDecimalPlaces
data.markPrice, ).toNumber();
data.marketDecimalPlaces
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data) {
return '';
}
if (
!data.markPrice ||
data.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
) {
return '-';
}
return addDecimalsFormatNumber(
data.markPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
}, },
{ valueFormatter: ({
headerName: t('Liquidation price'), data,
colId: 'liquidationPrice', }: VegaValueFormatterParams<
type: 'rightAligned', Position,
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => { 'averageEntryPrice'
if (!data) return null; >): string => {
return ( if (!data) {
<LiquidationPrice return '';
marketId={data.marketId} }
openVolume={data.openVolume} return addDecimalsFormatNumber(
collateralAvailable={data.totalBalance} data.averageEntryPrice,
decimalPlaces={data.decimals} data.marketDecimalPlaces
formatDecimals={data.marketDecimalPlaces} );
/>
);
},
}, },
{ minWidth: 100,
headerName: t('Settlement asset'), },
field: 'assetSymbol', multipleKeys
colId: 'asset', ? null
minWidth: 100, : {
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => { headerName: t('Leverage'),
if (!data) return null; field: 'currentLeverage',
return ( type: 'rightAligned',
<ButtonLink filter: 'agNumberColumnFilter',
title={t('View settlement asset details')} cellRenderer: PriceFlashCell,
onClick={(e) => { valueFormatter: ({
openAssetDetailsDialog( value,
data.assetId, }: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
e.target as HTMLElement value === undefined ? '' : formatNumber(value.toString(), 1),
); minWidth: 100,
}}
>
{data?.assetSymbol}
</ButtonLink>
);
}, },
}, multipleKeys
{ ? null
headerName: t('Entry price'), : {
field: 'averageEntryPrice', headerName: t('Margin allocated'),
type: 'rightAligned', field: 'marginAccountBalance',
cellRenderer: PriceFlashCell, type: 'rightAligned',
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { cellRenderer: PriceFlashCell,
return data?.markPrice === undefined || !data valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
? undefined return !data
: toBigNum( ? undefined
data.averageEntryPrice, : toBigNum(
data.marketDecimalPlaces data.marginAccountBalance,
).toNumber(); data.decimals
}, ).toNumber();
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'averageEntryPrice'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.averageEntryPrice,
data.marketDecimalPlaces
);
},
minWidth: 100,
},
multipleKeys
? null
: {
headerName: t('Leverage'),
field: 'currentLeverage',
type: 'rightAligned',
filter: 'agNumberColumnFilter',
cellRenderer: PriceFlashCell,
valueFormatter: ({
value,
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined
? ''
: formatNumber(value.toString(), 1),
minWidth: 100,
}, },
multipleKeys valueFormatter: ({
? null data,
: { }: VegaValueFormatterParams<
headerName: t('Margin allocated'), Position,
field: 'marginAccountBalance', 'marginAccountBalance'
type: 'rightAligned', >): string => {
filter: 'agNumberColumnFilter', if (!data) {
cellRenderer: PriceFlashCell, return '';
valueGetter: ({ data }: VegaValueGetterParams<Position>) => { }
return !data return addDecimalsFormatNumber(
? undefined data.marginAccountBalance,
: toBigNum( data.decimals
data.marginAccountBalance, );
data.decimals
).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<
Position,
'marginAccountBalance'
>): string => {
if (!data) {
return '';
}
return addDecimalsFormatNumber(
data.marginAccountBalance,
data.decimals
);
},
minWidth: 100,
}, },
{ minWidth: 100,
headerName: t('Realised PNL'),
field: 'realisedPNL',
type: 'rightAligned',
cellClassRules: signedNumberCssClassRules,
cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.realisedPNL, data.decimals).toNumber();
}, },
valueFormatter: ({ {
data, headerName: t('Realised PNL'),
}: VegaValueFormatterParams<Position, 'realisedPNL'>) => { field: 'realisedPNL',
return !data type: 'rightAligned',
? '' cellClassRules: signedNumberCssClassRules,
: addDecimalsFormatNumber(data.realisedPNL, data.decimals); cellClass: 'font-mono text-right',
}, filter: 'agNumberColumnFilter',
headerTooltip: t( valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' return !data
), ? undefined
cellRenderer: PNLCell, : toBigNum(data.realisedPNL, data.decimals).toNumber();
minWidth: 100,
}, },
{ valueFormatter: ({
headerName: t('Unrealised PNL'), data,
field: 'unrealisedPNL', }: VegaValueFormatterParams<Position, 'realisedPNL'>) => {
type: 'rightAligned', return !data
cellClassRules: signedNumberCssClassRules, ? ''
cellClass: 'font-mono text-right', : addDecimalsFormatNumber(data.realisedPNL, data.decimals);
filter: 'agNumberColumnFilter',
valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
},
valueFormatter: ({
data,
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
!data
? ''
: addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
headerTooltip: t(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
),
cellRenderer: PNLCell,
minWidth: 100,
}, },
{ headerTooltip: t(
headerName: t('Updated'), 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
field: 'updatedAt', ),
type: 'rightAligned', cellRenderer: PNLCell,
filter: DateRangeFilter, minWidth: 100,
valueFormatter: ({ },
value, {
}: VegaValueFormatterParams<Position, 'updatedAt'>) => { headerName: t('Unrealised PNL'),
if (!value) { field: 'unrealisedPNL',
return ''; type: 'rightAligned',
} cellClassRules: signedNumberCssClassRules,
return getDateTimeFormat().format(new Date(value)); cellClass: 'font-mono text-right',
}, filter: 'agNumberColumnFilter',
minWidth: 150, valueGetter: ({ data }: VegaValueGetterParams<Position>) => {
return !data
? undefined
: toBigNum(data.unrealisedPNL, data.decimals).toNumber();
}, },
onClose && !isReadOnly valueFormatter: ({
? { data,
...COL_DEFS.actions, }: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
cellRenderer: ({ !data
data, ? ''
}: VegaICellRendererParams<Position>) => { : addDecimalsFormatNumber(data.unrealisedPNL, data.decimals),
return ( headerTooltip: t(
<div className="flex gap-2 items-center justify-end"> 'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
{data?.openVolume && ),
data?.openVolume !== '0' && cellRenderer: PNLCell,
data.partyId === pubKey ? ( minWidth: 100,
<ButtonLink },
data-testid="close-position" {
onClick={() => data && onClose(data)} headerName: t('Updated'),
> field: 'updatedAt',
{t('Close')} type: 'rightAligned',
</ButtonLink> filter: DateRangeFilter,
) : null} valueFormatter: ({
{data?.assetId && ( value,
<PositionActionsDropdown assetId={data?.assetId} /> }: VegaValueFormatterParams<Position, 'updatedAt'>) => {
)} if (!value) {
</div> return '';
); }
}, return getDateTimeFormat().format(new Date(value));
minWidth: 90, },
maxWidth: 90, minWidth: 150,
} },
: null, onClose && !isReadOnly
]; ? {
return columnDefs.filter<ColDef>( ...COL_DEFS.actions,
(colDef: ColDef | null): colDef is ColDef => colDef !== null cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
); return (
}, [ <div className="flex gap-2 items-center justify-end">
isReadOnly, {data?.openVolume &&
multipleKeys, data?.openVolume !== '0' &&
onClose, data.partyId === pubKey ? (
onMarketClick, <ButtonLink
openAssetDetailsDialog, data-testid="close-position"
pubKey, onClick={() => data && onClose(data)}
pubKeys, >
])} {t('Close')}
/> </ButtonLink>
); ) : null}
} {data?.assetId && (
); <PositionActionsDropdown assetId={data?.assetId} />
)}
</div>
);
},
minWidth: 90,
maxWidth: 90,
}
: null,
];
return columnDefs.filter<ColDef>(
(colDef: ColDef | null): colDef is ColDef => colDef !== null
);
}, [
isReadOnly,
multipleKeys,
onClose,
onMarketClick,
openAssetDetailsDialog,
pubKey,
pubKeys,
])}
/>
);
};
export default PositionsTable; export default PositionsTable;
@@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import { useRef } from 'react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types'; import * as Types from '@vegaprotocol/types';
import type { AgGridReact } from 'ag-grid-react';
import { useColumnDefs } from './use-column-defs'; import { useColumnDefs } from './use-column-defs';
import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals'; import type { ProposalListFieldsFragment } from '../../lib/proposals-data-provider/__generated__/Proposals';
import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals'; import { useProposalsListQuery } from '../../lib/proposals-data-provider/__generated__/Proposals';
@@ -25,7 +23,6 @@ interface ProposalListProps {
export const ProposalsList = ({ export const ProposalsList = ({
SuccessorMarketRenderer, SuccessorMarketRenderer,
}: ProposalListProps) => { }: ProposalListProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const { data } = useProposalsListQuery({ const { data } = useProposalsListQuery({
variables: { variables: {
proposalType: Types.ProposalType.TYPE_NEW_MARKET, proposalType: Types.ProposalType.TYPE_NEW_MARKET,
@@ -40,7 +37,6 @@ export const ProposalsList = ({
return ( return (
<div className="relative h-full"> <div className="relative h-full">
<AgGrid <AgGrid
ref={gridRef}
className="w-full h-full" className="w-full h-full"
columnDefs={columnDefs} columnDefs={columnDefs}
rowData={filteredData} rowData={filteredData}

Some files were not shown because too many files have changed in this diff Show More