Compare commits

...
Author SHA1 Message Date
Matthew Russell e0316cbd9a fix: remove hover state from disabled view as button 2023-08-03 18:24:13 +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
102 changed files with 2297 additions and 1180 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
---
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: '
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
---
@@ -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 () {
const proposalTitle = 'Test new market proposal';
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.fixture('/proposals/new-market').then((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 () {
cy.reload();
waitForSpinner();
@@ -96,12 +96,14 @@ context('rewards - flow', { tags: '@slow' }, function () {
.within(() => {
cy.get('h2').first().should('contain.text', 'EPOCH');
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut)
.should('contain.text', '0.4415')
.and('contain.text', '(44.15%)');
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')
.should('contain.text', '0.0004')
.and('contain.text', '(44.15%)');
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut).should(
'contain.text',
'0.4415'
);
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
'contain.text',
'0.0004'
);
cy.getByTestId('total').should('have.text', '0.4419');
});
});
@@ -339,7 +339,7 @@ context(
cy.getByTestId(vegaWalletCurrencyTitle)
.contains(name)
.parent()
.siblings()
.siblings(txTimeout)
.should((elementAmount) => {
const displayedAmount = parseFloat(elementAmount.text());
expect(displayedAmount).be.gte(expectedAmount);
@@ -107,7 +107,7 @@ export function dissociateFromSecondWalletKey() {
cy.getByTestId('vega-in-wallet')
.first()
.within(() => {
cy.getByTestId('eth-wallet-associated-balances')
cy.getByTestId('eth-wallet-associated-balances', txTimeout)
.last()
.within(() => {
cy.getByTestId('associated-key')
@@ -78,13 +78,21 @@ export function stakingPageAssociateTokens(
}
cy.get(tokenAmountInputBox, epochTimeout).type(amount);
if (approve) {
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.getByTestId('wallet-associate').then((walletAssociateField) => {
if (
walletAssociateField.find('[data-testid="token-input-approve-button"]')
.length
) {
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();
@@ -200,7 +200,7 @@ export const ProposalMarketData = ({
title={marketData.tradableInstrument.instrument.code}
open={isOpen}
onChange={(isOpen) => (isOpen ? open() : close())}
size="medium"
size="large"
dataTestId="market-json-dialog"
>
<CopyWithTooltip text={JSON.stringify(marketData)}>
@@ -57,7 +57,7 @@ const ROWS = [
export const HealthDialog = ({ onChange, isOpen }: HealthDialogProps) => {
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">
{t('Health')}
</h1>
@@ -2,6 +2,7 @@ describe('charts', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
cy.getByTestId('Depth').click();
@@ -202,6 +202,7 @@ describe('Closed markets', { tags: '@smoke' }, () => {
const specDataConnection = createDataConnection();
before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
@@ -455,6 +456,7 @@ describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
@@ -4,6 +4,7 @@ const nodeHealthTrigger = 'node-health-trigger';
describe('home', { tags: '@regression' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/');
+38 -37
View File
@@ -2,7 +2,7 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import * as Schema from '@vegaprotocol/types';
const dialogContent = 'dialog-content';
const dialogContent = 'welcome-dialog';
const generateProposal = (code: string): ProposalListFieldsFragment => ({
__typename: 'Proposal',
@@ -140,43 +140,43 @@ describe('home', { tags: '@regression' }, () => {
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.getByTestId('welcome-notice-title').should(
'contain.text',
'Welcome to Console'
);
});
});
describe('no proposal nor markets found', () => {
it('there are welcome text and a link to propose market', () => {
cy.mockGQL((req) => {
const data = {
marketsConnection: {
__typename: 'MarketConnection',
edges: [],
},
};
aliasGQLQuery(req, 'Markets', data);
aliasGQLQuery(req, 'MarketsData', data);
aliasGQLQuery(req, 'ProposalsList', {
proposalsConnection: {
__typename: 'ProposalsConnection',
edges: null,
},
});
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('welcome-title').should('contain.text', 'Console CUSTOM');
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`);
cy.window().then((window) => {
expect(window.localStorage.getItem('vega_onboarding_viewed')).to.equal(
'true'
);
});
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.getByTestId('welcome-notice-title').should(
'contain.text',
'Welcome to Console'
);
cy.getByTestId('external-link')
.contains('Propose a market')
.should('exist');
});
it('click browse markets button should redirect to market/all', () => {
cy.getByTestId('welcome-dialog').should('be.visible');
cy.getByTestId('browse-markets-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'
);
});
});
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) => {
window.localStorage.setItem('marketId', 'market-1');
cy.visit('/');
cy.wait('@Markets');
cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-1');
cy.getByTestId(dialogContent).should('not.exist');
});
@@ -199,6 +199,7 @@ describe('home', { tags: '@regression' }, () => {
});
cy.visit('/');
cy.wait('@Markets');
cy.getByTestId('dialog-close').click();
cy.location('hash').should('equal', '#/markets/market-not-existing');
cy.getByTestId(dialogContent).should('not.exist');
});
@@ -3,12 +3,13 @@ import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
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"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -34,7 +35,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'Settlement asset',
'',
];
cy.getByTestId('tab-all-markets').within(($headers) => {
cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
@@ -44,7 +45,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
});
it('markets tab should be rendered properly', () => {
cy.get('[data-testid="All markets"]').should(
cy.get('[data-testid="Open markets"]').should(
'have.attr',
'data-state',
'active'
@@ -166,7 +167,7 @@ describe('markets all table', { tags: '@smoke' }, () => {
'ETHBTC.QM21',
'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.contains('AAPL.MF21').should('be.visible');
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(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
@@ -199,11 +200,12 @@ describe('no all markets', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
});
it.skip('can see no markets message', () => {
// 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(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(MarketState.STATE_ACTIVE);
cy.mockSubscription();
cy.visit('/#/markets/market-0');
@@ -36,6 +36,7 @@ const headers = [
describe('liquidity table - trading', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
@@ -119,6 +120,7 @@ describe('liquidity table - trading', { tags: '@smoke' }, () => {
describe('liquidity table view', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockSubscription();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
@@ -12,6 +12,7 @@ describe('markets selector', { tags: '@smoke' }, () => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.setOnBoardingViewed();
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@@ -30,6 +30,7 @@ describe('Market trading page', () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@MarketData');
cy.getByTestId(marketSummaryBlock).should('be.visible');
@@ -9,6 +9,7 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
@@ -213,6 +214,7 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
});
it('can see no markets message', () => {
@@ -12,6 +12,7 @@ describe('markets table', { tags: '@smoke' }, () => {
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
});
});
@@ -6,6 +6,7 @@ const oracleFullProfile = 'oracle-full-profile';
describe('oracle information', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
undefined,
@@ -14,6 +14,7 @@ const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
@@ -4,6 +4,7 @@ describe('Settings page', { tags: '@smoke' }, () => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/');
// Only click if not already active otherwise sidebar will close
@@ -77,6 +77,7 @@ function getButtonSelectorByText(text: string): string {
beforeEach(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
@@ -14,6 +14,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
cy.mockTradingPage();
cy.mockSubscription();
cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
@@ -15,6 +15,7 @@ describe('time in force validation', { tags: '@smoke' }, () => {
cy.mockTradingPage();
cy.mockSubscription();
cy.clearAllLocalStorage();
cy.setOnBoardingViewed();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
@@ -9,6 +9,7 @@ describe('trades', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.intercept('POST', '/graphql', (req) => {
if (req.body.operationName === 'Trades') {
req.alias = '@Trades';
@@ -17,6 +17,7 @@ describe(
cy.visit('/#/portfolio');
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.get('[data-testid="pathname-/portfolio"]').should('exist');
});
@@ -122,6 +123,7 @@ describe('connect vega wallet', { tags: '@smoke', testIsolation: true }, () => {
cy.visit('/#/portfolio');
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
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 assetBalance = 'asset-balance';
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 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;
const collateralTab = 'Collateral';
const toastCloseBtn = 'toast-close';
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 }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
+1 -1
View File
@@ -20,4 +20,4 @@ NX_APP_VERSION=v0.20.21-core-0.71.6
NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
# NX_PRODUCT_PERPETUALS
+1 -1
View File
@@ -18,5 +18,5 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
NX_ICEBERG_ORDERS=true
# 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,
});
} else if (data) {
update({ shouldDisplayWelcomeDialog: true });
const marketDataId = data[0]?.id;
if (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 { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import {
ResizableGrid,
ResizableGridPanel,
@@ -58,7 +57,11 @@ const MainGrid = memo(
>
<TradeGridChild>
<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} />
</Tab>
<Tab id="depth" name={t('Depth')}>
@@ -95,64 +98,48 @@ const MainGrid = memo(
<TradeGridChild>
<Tabs storageKey="console-trade-grid-bottom">
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.positions.component
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
<TradingViews.positions.component
onMarketClick={onMarketClick}
/>
</Tab>
<Tab id="open-orders" name={t('Open')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
/>
</VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Open}
/>
</Tab>
<Tab id="closed-orders" name={t('Closed')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
/>
</VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Closed}
/>
</Tab>
<Tab id="rejected-orders" name={t('Rejected')}>
<VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
/>
</VegaWalletContainer>
<TradingViews.orders.component
marketId={marketId}
filter={Filter.Rejected}
/>
</Tab>
<Tab id="orders" name={t('All')}>
<VegaWalletContainer>
<TradingViews.orders.component marketId={marketId} />
</VegaWalletContainer>
<TradingViews.orders.component marketId={marketId} />
</Tab>
{FLAGS.STOP_ORDERS ? (
<Tab id="stop-orders" name={t('Stop orders')}>
<VegaWalletContainer>
<TradingViews.stopOrders.component />
</VegaWalletContainer>
<TradingViews.stopOrders.component />
</Tab>
) : null}
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.fills.component
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
<TradingViews.fills.component
marketId={marketId}
onMarketClick={onMarketClick}
/>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
/>
</VegaWalletContainer>
<TradingViews.collateral.component
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
hideButtons
/>
</Tab>
</Tabs>
</TradeGridChild>
@@ -37,6 +37,7 @@ export const TradePanels = ({
const onOrderTypeClick = useMarketLiquidityClickHandler();
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
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 (
<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>
{FLAGS.SUCCESSOR_MARKETS && (
<>
@@ -76,6 +92,7 @@ export const TradePanels = ({
)}
<OracleBanner marketId={market?.id || ''} />
</div>
<div>{renderMenu()}</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
@@ -2,7 +2,10 @@ import type { ComponentProps } from 'react';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { TradesContainer } from '@vegaprotocol/trades';
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 { NO_MARKET } from './constants';
import { OrderbookContainer } from '../../components/orderbook-container';
@@ -35,6 +38,7 @@ export const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
menu: CandlesMenu,
},
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 my-1 border border-default rounded-sm">
<Tabs storageKey="console-markets">
<Tab id="all-markets" name={t('All markets')}>
<Tab id="open-markets" name={t('Open markets')}>
<Markets />
</Tab>
<Tab id="proposed-markets" name={t('Proposed markets')}>
@@ -71,7 +71,7 @@ export const AccountHistoryContainer = () => {
const { data: assets } = useAssetsDataProvider();
if (!pubKey) {
return <Splash>{t('Connect wallet')}</Splash>;
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
@@ -1,4 +1,4 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import { DepositsTable } from '@vegaprotocol/deposits';
import { depositsProvider } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
@@ -17,6 +17,9 @@ export const DepositsContainer = () => {
skip: !pubKey,
});
const setView = useSidebar((store) => store.setView);
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<div className="h-full">
<DepositsTable
@@ -13,7 +13,6 @@ import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container';
import { WithdrawalsContainer } from './withdrawals-container';
import { OrdersContainer } from '../../components/orders-container';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container';
import {
@@ -62,29 +61,19 @@ export const Portfolio = () => {
<PortfolioGridChild>
<Tabs storageKey="console-portfolio-top">
<Tab id="account-history" name={t('Account history')}>
<VegaWalletContainer>
<AccountHistoryContainer />
</VegaWalletContainer>
<AccountHistoryContainer />
</Tab>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<PositionsContainer onMarketClick={onMarketClick} allKeys />
</VegaWalletContainer>
<PositionsContainer onMarketClick={onMarketClick} allKeys />
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<OrdersContainer />
</VegaWalletContainer>
<OrdersContainer />
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<FillsContainer onMarketClick={onMarketClick} />
</VegaWalletContainer>
<FillsContainer onMarketClick={onMarketClick} />
</Tab>
<Tab id="ledger-entries" name={t('Ledger entries')}>
<VegaWalletContainer>
<LedgerContainer />
</VegaWalletContainer>
<LedgerContainer />
</Tab>
</Tabs>
</PortfolioGridChild>
@@ -97,14 +86,10 @@ export const Portfolio = () => {
<PortfolioGridChild>
<Tabs storageKey="console-portfolio-bottom">
<Tab id="collateral" name={t('Collateral')}>
<VegaWalletContainer>
<AccountsContainer />
</VegaWalletContainer>
<AccountsContainer />
</Tab>
<Tab id="deposits" name={t('Deposits')}>
<VegaWalletContainer>
<DepositsContainer />
</VegaWalletContainer>
<DepositsContainer />
</Tab>
<Tab
id="withdrawals"
@@ -1,4 +1,4 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
import {
withdrawalProvider,
WithdrawalsTable,
@@ -7,7 +7,6 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { ViewType, useSidebar } from '../../components/sidebar';
export const WithdrawalsContainer = () => {
@@ -19,9 +18,11 @@ export const WithdrawalsContainer = () => {
});
const setView = useSidebar((store) => store.setView);
const { ready, delayed } = useIncompleteWithdrawals();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<VegaWalletContainer>
<>
<div className="h-full relative">
<WithdrawalsTable
data-testid="withdrawals-history"
@@ -43,6 +44,6 @@ export const WithdrawalsContainer = () => {
</Button>
</div>
)}
</VegaWalletContainer>
</>
);
};
@@ -1,8 +1,8 @@
import { useCallback } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager } from '@vegaprotocol/accounts';
+1 -1
View File
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n';
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(
'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 { FillsManager } from '@vegaprotocol/fills';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
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 { createDataGridSlice } from '../../stores/datagrid-store-slice';
@@ -12,10 +12,9 @@ import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import type { AgGridReact } from 'ag-grid-react';
import type { DataGridSlice } 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 { persist } from 'zustand/middleware';
@@ -26,8 +25,6 @@ export const LiquidityContainer = ({
marketId: string | undefined;
filter?: Filter;
}) => {
const gridRef = useRef<AgGridReact | null>(null);
const gridStore = useLiquidityStore((store) => store.gridStore);
const updateGridStore = useLiquidityStore((store) => store.updateGridStore);
@@ -60,7 +57,6 @@ export const LiquidityContainer = ({
return (
<div className="h-full relative">
<LiquidityTable
ref={gridRef}
rowData={data}
symbol={symbol}
assetDecimalPlaces={assetDecimalPlaces}
@@ -52,6 +52,9 @@ export const Navbar = ({
'bg-vega-yellow text-vega-clight-50': theme === 'yellow',
'text-default': theme === 'system',
})}
style={{
background: theme === 'yellow' ? 'url(/testnet-logo-bg.png' : 'none',
}}
>
<VLogo className="w-4" />
</NavLink>
@@ -1,7 +1,6 @@
import { useDataGridEvents } from '@vegaprotocol/datagrid';
import { Filter, OrderListManager } from '@vegaprotocol/orders';
import { t } from '@vegaprotocol/i18n';
import { Filter } from '@vegaprotocol/orders';
import { OrderListManager } from '@vegaprotocol/orders';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
@@ -1,7 +1,15 @@
import { act, render, screen } from '@testing-library/react';
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 { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
jest.mock('../node-health', () => ({
NodeHealthContainer: () => <span data-testid="node-health" />,
@@ -19,16 +27,23 @@ jest.mock('../settings', () => ({
Settings: () => <div data-testid="settings" />,
}));
jest.mock('../welcome-dialog', () => ({
GetStarted: () => <div data-testid="get-started" />,
}));
describe('Sidebar', () => {
it.each(['/markets/all', '/portfolio'])(
'does not render ticket and info',
(path) => {
render(
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={[path]}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -43,11 +58,14 @@ describe('Sidebar', () => {
it('renders ticket and info on market pages', () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
expect(screen.getByTestId(ViewType.ViewAs)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Settings)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Transfer)).toBeInTheDocument();
expect(screen.getByTestId(ViewType.Deposit)).toBeInTheDocument();
@@ -61,9 +79,11 @@ describe('Sidebar', () => {
it('renders selected state', async () => {
render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Sidebar />
</MemoryRouter>
</VegaWalletProvider>
);
const settingsButton = screen.getByTestId(ViewType.Settings);
@@ -87,11 +107,13 @@ describe('Sidebar', () => {
describe('SidebarContent', () => {
it('renders the correct content', () => {
const { container } = render(
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/markets/ABC']}>
<Routes>
<Route path="/markets/:marketId" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
expect(container).toBeEmptyDOMElement();
@@ -111,11 +133,13 @@ describe('SidebarContent', () => {
it('closes sidebar if market id is required but not present', () => {
const { container } = render(
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
<VegaWalletProvider>
<MemoryRouter initialEntries={['/portfolio']}>
<Routes>
<Route path="/portfolio" element={<SidebarContent />} />
</Routes>
</MemoryRouter>
</VegaWalletProvider>
);
act(() => {
@@ -137,3 +161,25 @@ describe('SidebarContent', () => {
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 { Routes as AppRoutes } from '../../pages/client-router';
import { persist } from 'zustand/middleware';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
const STORAGE_KEY = 'vega_sidebar_store';
@@ -25,6 +27,7 @@ export enum ViewType {
Withdraw = 'Withdraw',
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
}
type SidebarView =
@@ -52,6 +55,8 @@ type SidebarView =
export const Sidebar = () => {
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 (
<div className="flex lg:flex-col gap-2 h-full p-1" data-testid="sidebar">
<nav className={navClasses}>
@@ -104,6 +109,16 @@ export const Sidebar = () => {
</Routes>
</nav>
<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
view={ViewType.Settings}
icon={VegaIconNames.COG}
@@ -115,25 +130,43 @@ export const Sidebar = () => {
);
};
const SidebarButton = ({
export const SidebarButton = ({
view,
icon,
tooltip,
disabled = false,
onClick,
}: {
view: ViewType;
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
}) => {
const { currView, setView } = useSidebar((store) => ({
currView: store.view,
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':
view !== currView?.type,
'bg-vega-yellow hover:bg-vega-yellow-550 text-black':
view === currView?.type,
});
const onSelect = (view: SidebarView['type']) => {
if (view === currView?.type) {
setView(null);
} 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 (
<Tooltip
description={tooltip}
@@ -145,13 +178,8 @@ const SidebarButton = ({
<button
className={buttonClasses}
data-testid={view}
onClick={() => {
if (view === currView?.type) {
setView(null);
} else {
setView({ type: view });
}
}}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
</button>
@@ -184,6 +212,7 @@ export const SidebarContent = () => {
setView({ type: ViewType.Deposit, assetId })
}
/>
<GetStarted />
</ContentWrapper>
);
} else {
@@ -207,6 +236,7 @@ export const SidebarContent = () => {
return (
<ContentWrapper title={t('Deposit')}>
<DepositContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper>
);
}
@@ -215,6 +245,7 @@ export const SidebarContent = () => {
return (
<ContentWrapper title={t('Withdraw')}>
<WithdrawContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper>
);
}
@@ -223,6 +254,7 @@ export const SidebarContent = () => {
return (
<ContentWrapper title={t('Transfer')}>
<TransferContainer assetId={view.assetId} />
<GetStarted />
</ContentWrapper>
);
}
@@ -277,7 +309,7 @@ export const useSidebar = create<{
view: null,
setView: (x) =>
set(() => {
if (x === null) {
if (x == null) {
return { view: null, init: false };
}
@@ -27,6 +27,16 @@ describe('VegaWalletConnectButton', () => {
it('should fire dialog when not connected', () => {
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');
expect(button).toHaveTextContent('Connect');
fireEvent.click(button);
@@ -1,5 +1,6 @@
import { useMemo, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
@@ -37,7 +38,7 @@ export const VegaWalletConnectButton = () => {
fetchPubKeys,
} = useVegaWallet();
const isConnected = pubKey !== null;
const walletInstalled = isBrowserWalletInstalled();
const activeKey = useMemo(() => {
return pubKeys?.find((pk) => pk.publicKey === pubKey);
}, [pubKey, pubKeys]);
@@ -118,7 +119,9 @@ export const VegaWalletConnectButton = () => {
intent={Intent.None}
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>
);
};
@@ -0,0 +1,86 @@
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');
};
if (!pubKey && !isBrowserWalletInstalled()) {
return (
<div
className={classNames(
'flex flex-col bg-vega-blue-300 dark:bg-vega-blue-700 border border-vega-blue-350 dark:border-vega-blue-650 px-6 gap-4',
{ 'py-4': lead },
{ 'mt-8 py-6': !lead }
)}
data-testid="get-started-banner"
>
{lead && <h2>{lead}</h2>}
<h3>{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}
className="block w-full"
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>
);
}
return null;
};
@@ -1,2 +1,3 @@
export * from './welcome-dialog';
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-cutodial and pseudonymous')}
text={t('No third party has access to your funds.')}
/>
<ListItemContent
icon={<PurposeBuiltIcon />}
title={t('Purpose built proof of stake blockhain')}
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 { useLocation } from 'react-router-dom';
import { Dialog } from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
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 { Networks } from '@vegaprotocol/environment';
import { isTestEnv } from '@vegaprotocol/utils';
import { isBrowserWalletInstalled } from '@vegaprotocol/wallet';
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 = () => {
const { VEGA_ENV } = useEnvironment();
const { pathname } = useLocation();
let dialogContent: React.ReactNode;
let title = '';
let size: 'small' | 'medium' = 'small';
let onClose: ((open: boolean) => void) | undefined = undefined;
const [riskAccepted] = useLocalStorage(constants.RISK_ACCEPTED_KEY);
const { data } = useDataProvider({
dataProvider: activeMarketsProvider,
variables: undefined,
});
const [onBoardingViewed, setOnboardingViewed] = useLocalStorage(
constants.ONBOARDING_VIEWED_KEY
);
const navigate = useNavigate();
const isOnboardingDialogNeeded =
onBoardingViewed !== 'true' && !isBrowserWalletInstalled() && !getConfig();
const marketId = useGlobalStore((store) => store.marketId);
const update = useGlobalStore((store) => store.update);
const shouldDisplayWelcomeDialog = useGlobalStore(
(store) => store.shouldDisplayWelcomeDialog
const onClose = () => {
setOnboardingViewed('true');
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 =
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 (
return isOnboardingDialogNeeded ? (
<Dialog
open={Boolean(dialogContent)}
open
title={title}
size={size}
size="medium"
onChange={onClose}
intent={Intent.None}
dataTestId="welcome-dialog"
>
{dialogContent}
<WelcomeDialogContent />
</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,
});
const LazyDeposit = dynamic(() => import('../client-pages/deposit'), {
ssr: false,
});
export enum Routes {
HOME = '/',
MARKET = '/markets/:marketId',
@@ -38,6 +42,7 @@ export enum Routes {
PORTFOLIO = '/portfolio',
LIQUIDITY = '/liquidity/:marketId',
DISCLAIMER = '/disclaimer',
DEPOSIT = '/deposit',
}
type ConsoleLinks = { [r in Routes]: (...args: string[]) => string };
@@ -51,6 +56,7 @@ export const Links: ConsoleLinks = {
[Routes.LIQUIDITY]: (marketId: string) =>
trimEnd(Routes.LIQUIDITY.replace(':marketId', marketId)),
[Routes.DISCLAIMER]: () => Routes.DISCLAIMER,
[Routes.DEPOSIT]: () => Routes.DEPOSIT,
};
const routerConfig: RouteObject[] = [
@@ -97,6 +103,7 @@ const routerConfig: RouteObject[] = [
path: Routes.DISCLAIMER,
element: <LazyDisclaimer />,
},
{ path: Routes.DEPOSIT, element: <LazyDeposit /> },
{
path: '*',
element: (
+2 -1
View File
@@ -2,7 +2,7 @@ import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { VegaConnectDialog } from '@vegaprotocol/wallet';
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
import { Connectors } from '../lib/vega-connectors';
import {
Web3ConnectUncontrolledDialog,
@@ -19,6 +19,7 @@ const DialogsContainer = () => {
connectors={Connectors}
riskMessage={<RiskMessage />}
/>
<ViewAsDialog connector={Connectors.view} />
<AssetDetailsDialog
assetId={id}
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 {
marketId: string | null;
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
shouldDisplayWelcomeDialog: boolean;
}
interface PageTitleStore {
@@ -15,7 +14,6 @@ interface PageTitleStore {
export const useGlobalStore = create<GlobalStore>()((set) => ({
marketId: LocalStorage.getItem('marketId') || null,
shouldDisplayWelcomeDialog: false,
update: (newState) => {
set(
produce((state: GlobalStore) => {
+1 -1
View File
@@ -75,7 +75,7 @@ export const AccountBreakdownDialog = memo(
}) => {
return (
<Dialog
size="medium"
size="large"
open={Boolean(assetId)}
onChange={(isOpen) => {
if (!isOpen) {
@@ -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 {
CandlestickChart,
ChartType,
Interval,
Overlay,
Study,
chartTypeLabels,
intervalLabels,
overlayLabels,
studyLabels,
} from 'pennant';
import { CandlestickChart } from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
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 { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/i18n';
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[];
}
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],
]);
import { useCandlesChartSettings } from './use-candles-chart-settings';
export type CandlesChartContainerProps = {
marketId: string;
@@ -99,161 +19,32 @@ export const CandlesChartContainer = ({
const { pubKey } = useVegaWallet();
const { theme } = useThemeSwitcher();
const settings = 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 { interval, chartType, overlays, studies, merge } =
useCandlesChartSettings();
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, pubKey);
}, [client, marketId, pubKey]);
return (
<div className="h-full flex flex-col">
<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">
<DropdownMenu
trigger={
<DropdownMenuTrigger>
{t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={interval}
onValueChange={(value) => {
settings.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>
<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>
<CandlestickChart
dataSource={dataSource}
options={{
chartType,
overlays,
studies,
notEnoughDataText: (
<span className="text-xs text-center">{t('No data')}</span>
),
}}
interval={interval}
theme={theme}
onOptionsChanged={(options) => {
merge({
overlays: options.overlays,
studies: options.studies,
});
}}
/>
);
};
@@ -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__/Chart';
export * from './candles-chart';
export * from './candles-menu';
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 {
addVegaWalletConnect,
addSetVegaWallet,
addSetOnBoardingViewed,
} from './lib/commands/vega-wallet-connect';
import { addMockTransactionResponse } from './lib/commands/mock-transaction-response';
import { addCreateMarket } from './lib/commands/create-market';
@@ -38,6 +39,7 @@ addGetNetworkParameters();
addUpdateCapsuleMultiSig();
addVegaWalletConnect();
addSetVegaWallet();
addSetOnBoardingViewed();
addMockTransactionResponse();
addCreateMarket();
addConnectPublicKey();
@@ -14,6 +14,10 @@ declare global {
interface Chainable<Subject> {
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() {
Cypress.Commands.add('setVegaWallet', () => {
cy.window().then((win) => {
win.localStorage.setItem('vega_risk_accepted', 'true');
win.localStorage.setItem('vega_onboarding_viewed', 'true');
win.localStorage.setItem(
'vega_wallet_config',
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');
});
});
}
@@ -488,19 +488,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</InputError>
)}
</div>
<div className="flex gap-2 pb-2 justify-end">
<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">
<div className="flex gap-2 pb-2 justify-between">
<Controller
name="expire"
control={control}
@@ -511,11 +499,21 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
onCheckedChange={onCheckedChange}
checked={value}
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>
{expire && (
<>
@@ -17,7 +17,7 @@ export const SideSelector = (props: SideSelectorProps) => {
return (
<RadioGroup.Root
name="order-side"
className="mb-2 flex h-10 leading-10"
className="mb-4 flex h-10 leading-10"
{...props}
>
{toggles.map(({ label, value }) => (
@@ -1,5 +1,4 @@
import {
Icon,
InputError,
SimpleGrid,
Tooltip,
@@ -10,6 +9,8 @@ import {
TradingDropdownRadioGroup,
TradingDropdownRadioItem,
TradingDropdownTrigger,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
@@ -45,7 +46,7 @@ export const TypeToggle = ({
return (
<RadioGroup.Root
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-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">
{t(selectedOption ? selectedOption.label : 'Stop')}
</span>
<Icon name="chevron-down" className="ml-1" />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
</button>
</TradingDropdownTrigger>
}
@@ -9,7 +9,7 @@ export const NodeSwitcherDialog = ({
setOpen: (x: boolean) => void;
}) => {
return (
<Dialog open={open} onChange={setOpen} size="medium">
<Dialog open={open} onChange={setOpen} size="large">
<NodeSwitcher closeDialog={() => setOpen(false)} />
</Dialog>
);
+153 -158
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo } from 'react';
import { useMemo } from 'react';
import {
addDecimalsFormatNumber,
addDecimalsFormatNumberQuantum,
@@ -9,7 +9,6 @@ import { t } from '@vegaprotocol/i18n';
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import type {
ColDef,
ITooltipParams,
@@ -40,168 +39,164 @@ export interface LiquidityTableProps
quantum?: string | number;
}
export const LiquidityTable = forwardRef<AgGridReact, LiquidityTableProps>(
(
{ symbol = '', assetDecimalPlaces, stakeToCcyVolume, quantum, ...props },
ref
) => {
const colDefs = useMemo(() => {
const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
export const LiquidityTable = ({
symbol = '',
assetDecimalPlaces,
stakeToCcyVolume,
quantum,
...props
}: LiquidityTableProps) => {
const colDefs = useMemo(() => {
const assetDecimalsFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0)}`;
};
const assetDecimalsQuantumFormatter = ({
const assetDecimalsQuantumFormatter = ({ value }: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value,
}: ValueFormatterParams) => {
if (!value) return '-';
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const stakeToCcyVolumeFormatter = ({ value }: ITooltipParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const stakeToCcyVolumeQuantumFormatter = ({
value,
}: ValueFormatterParams) => {
if (!value) return '-';
const newValue = new BigNumber(value)
.times(Number(stakeToCcyVolume) || 1)
.toString();
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
)}`;
};
const defs: ColDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t(
'The public key of the party making this commitment.'
),
const defs: ColDef[] = [
{
headerName: t('Party'),
field: 'party.id',
headerTooltip: t('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',
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('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]);
},
{
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 (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
ref={ref}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
minWidth: 100,
tooltipComponent: TooltipCellComponent,
sortable: true,
}}
{...props}
columnDefs={colDefs}
/>
);
}
);
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No liquidity provisions')}
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
tooltipShowDelay={500}
defaultColDef={{
resizable: true,
minWidth: 100,
tooltipComponent: TooltipCellComponent,
sortable: true,
}}
{...props}
columnDefs={colDefs}
/>
);
};
export default LiquidityTable;
@@ -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 { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
export enum Filter {
'Open' = 'Open',
@@ -42,6 +43,7 @@ export const OrderListManager = ({
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const [viewOrder, setViewOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const variables =
@@ -89,6 +91,7 @@ export const OrderListManager = ({
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
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 { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
import type { OrderListTableProps } from '../';
import type { OrderFieldsFragment, OrderListTableProps } from '../';
import { OrderListTable } from '../';
import {
generateOrder,
@@ -27,6 +27,7 @@ const defaultProps: OrderListTableProps = {
rowData: [],
onEdit: jest.fn(),
onCancel: jest.fn(),
onView: jest.fn(),
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 () => {
const rejectedOrder = {
...marketOrder,
@@ -22,6 +22,9 @@ const Template: Story = (args) => {
onEdit={() => {
return;
}}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>
@@ -49,6 +52,9 @@ const Template2: Story = (args) => {
rowData={args.data}
onCancel={cancel}
onEdit={setEditOrder}
onView={() => {
return;
}}
isReadOnly={false}
/>
</div>
@@ -6,7 +6,14 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
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 { memo, forwardRef, useMemo } from 'react';
import {
@@ -27,7 +34,6 @@ import type {
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import { OrderActionsDropdown } from '../order-actions-dropdown';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
@@ -35,6 +41,7 @@ export type OrderListTableProps = TypedDataAgGrid<Order> & {
marketId?: string;
onCancel: (order: Order) => void;
onEdit: (order: Order) => void;
onView: (order: Order) => void;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
onOrderTypeClick?: (marketId: string, metaKey?: boolean) => void;
filter?: Filter;
@@ -46,7 +53,15 @@ export const OrderListTable = memo<
>(
forwardRef<AgGridReact, OrderListTableProps>(
(
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props },
{
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
filter,
...props
},
ref
) => {
const showAllActions = props.isReadOnly
@@ -69,6 +84,18 @@ export const OrderListTable = memo<
cellClass: 'font-mono text-right',
type: 'rightAligned',
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
? toBigNum(
(BigInt(data.size) - BigInt(data.remaining)).toString(),
@@ -283,7 +310,20 @@ export const OrderListTable = memo<
</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>
);
},
@@ -293,6 +333,7 @@ export const OrderListTable = memo<
filter,
onCancel,
onEdit,
onView,
onMarketClick,
onOrderTypeClick,
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>
);
};
@@ -16,7 +16,7 @@ interface DialogProps {
title?: string | ReactNode;
icon?: ReactNode;
intent?: Intent;
size?: 'small' | 'medium';
size?: 'small' | 'medium' | 'large';
dataTestId?: string;
}
@@ -46,7 +46,8 @@ export function Dialog({
getIntentBorder(intent),
{
'w-[520px]': size === 'small',
'w-[720px] lg:w-[940px]': size === 'medium',
'w-[680px]': size === 'medium',
'w-[720px] lg:w-[940px]': size === 'large',
}
);
@@ -43,17 +43,18 @@ export const DropdownMenuTrigger = forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
DropdownMenuPrimitive.DropdownMenuTriggerProps
>(({ className, children, ...props }, forwardedRef) => {
const defaultClasses = [
const triggerClasses = classNames(
'text-sm py-1 px-2 rounded bg-transparent border whitespace-nowrap',
'border-vega-light-200 dark:border-vega-dark-200',
'hover:border-vega-light-300 dark:hover:border-vega-dark-300',
].join(' ');
className
);
return (
<DropdownMenuPrimitive.Trigger
asChild={true}
ref={forwardedRef}
className={className || defaultClasses}
className={triggerClasses}
{...props}
>
<button>{children}</button>
@@ -0,0 +1,8 @@
export const IconEye = ({ size = 16 }: { size: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 16 16">
<path d="M0.678395 7.44128C2.71173 4.52663 5.34817 2.99829 7.9894 2.99829C11.0914 2.99829 13.7635 5.01915 15.34 7.45404L15.3408 7.45519C15.4451 7.61729 15.5006 7.80599 15.5006 7.99876C15.5006 8.19118 15.4453 8.37954 15.3414 8.54144C13.7662 11.0077 11.1114 12.9983 7.9894 12.9983C4.8342 12.9983 2.23054 11.0118 0.659261 8.55277C0.552495 8.38698 0.497119 8.19332 0.500115 7.99613C0.503119 7.79845 0.564651 7.6061 0.676935 7.44338L0.67839 7.44127L0.678395 7.44128ZM1.5 8.01133L1.50139 8.01348L1.50139 8.01349C2.95267 10.2852 5.27875 11.9983 7.9894 11.9983C10.6697 11.9983 13.0459 10.278 14.4989 8.00262L14.4999 8.00113C14.5001 8.00073 14.5003 8.0003 14.5005 7.99985C14.5005 7.9995 14.5006 7.99913 14.5006 7.99876C14.5006 7.99792 14.5003 7.9971 14.4999 7.99639L14.9203 7.72579L14.5006 7.99754C13.0421 5.74493 10.6461 3.99829 7.9894 3.99829C5.75997 3.99829 3.40017 5.28866 1.5 8.01133Z" />
<path d="M8 5.99951C6.89543 5.99951 6 6.89494 6 7.99951C6 9.10408 6.89543 9.99951 8 9.99951C9.10457 9.99951 10 9.10408 10 7.99951C10 6.89494 9.10457 5.99951 8 5.99951ZM5 7.99951C5 6.34266 6.34315 4.99951 8 4.99951C9.65685 4.99951 11 6.34266 11 7.99951C11 9.65637 9.65685 10.9995 8 10.9995C6.34315 10.9995 5 9.65637 5 7.99951Z" />
</svg>
);
};
@@ -12,6 +12,7 @@ import { IconCross } from './svg-icons/icon-cross';
import { IconDeposit } from './svg-icons/icon-deposit';
import { IconEdit } from './svg-icons/icon-edit';
import { IconExclaimationMark } from './svg-icons/icon-exclaimation-mark';
import { IconEye } from './svg-icons/icon-eye';
import { IconForum } from './svg-icons/icon-forum';
import { IconGlobe } from './svg-icons/icon-globe';
import { IconInfo } from './svg-icons/icon-info';
@@ -48,6 +49,7 @@ export enum VegaIconNames {
DEPOSIT = 'deposit',
EDIT = 'edit',
EXCLAIMATION_MARK = 'exclaimation-mark',
EYE = 'eye',
FORUM = 'forum',
GLOBE = 'globe',
INFO = 'info',
@@ -88,6 +90,7 @@ export const VegaIconNameMap: Record<
deposit: IconDeposit,
edit: IconEdit,
'exclaimation-mark': IconExclaimationMark,
eye: IconEye,
forum: IconForum,
globe: IconGlobe,
info: IconInfo,
@@ -21,7 +21,7 @@ export const NotificationBanner = ({
return (
<div
className={classNames(
'flex items-center p-3 border-b min-h-[56px]',
'flex items-center px-1 py-3 border-b min-h-[56px]',
'text-[12px] leading-[16px] font-normal',
{
'bg-vega-light-100 dark:bg-vega-dark-100 ': intent === Intent.None,
@@ -30,4 +30,43 @@ describe('Tabs', () => {
await userEvent.click(screen.getByText('Tab two'));
expect(await screen.getByText('Tab two content')).toBeInTheDocument();
});
it('shows the menu component for the selected tab', async () => {
render(
<Tabs>
<Tab
id="one"
name="Tab one"
menu={
<>
<button>Tab 1 button 1</button>
<button>Tab 1 button 2</button>
</>
}
>
<p>Tab one content</p>
</Tab>
<Tab
id="two"
name="Tab two"
menu={
<>
<button>Tab 2 button 1</button>
<button>Tab 2 button 2</button>
</>
}
>
<p>Tab two content</p>
</Tab>
</Tabs>
);
expect(screen.getByText('Tab one content')).toBeInTheDocument();
expect(screen.getByText('Tab 1 button 1')).toBeInTheDocument();
expect(screen.getByText('Tab 1 button 2')).toBeInTheDocument();
await userEvent.click(screen.getByText('Tab two'));
expect(await screen.getByText('Tab two content')).toBeInTheDocument();
expect(screen.getByText('Tab 2 button 1')).toBeInTheDocument();
expect(screen.getByText('Tab 2 button 2')).toBeInTheDocument();
});
});
+52 -2
View File
@@ -2,10 +2,11 @@ import * as TabsPrimitive from '@radix-ui/react-tabs';
import {
useLocalStorageSnapshot,
getValidItem,
useResizeObserver,
} from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
import type { ReactElement, ReactNode } from 'react';
import { Children, isValidElement, useState } from 'react';
import { Children, isValidElement, useRef, useState } from 'react';
export interface TabsProps extends TabsPrimitive.TabsProps {
children: (ReactElement<TabProps> | null)[];
}
@@ -24,6 +25,19 @@ export const Tabs = ({
return children.find((v) => v)?.props.id;
});
// Bunch of refs in order to detect wrapping in side the tabs so that we
// can apply a bg color
const wrapperRef = useRef<HTMLDivElement | null>(null);
const tabsRef = useRef<HTMLDivElement | null>(null);
const menuRef = useRef<HTMLDivElement | null>(null);
const [wrapped, setWrapped] = useState(() =>
isWrapped(tabsRef.current, menuRef.current)
);
useResizeObserver(wrapperRef.current, () => {
setWrapped(isWrapped(tabsRef.current, menuRef.current));
});
return (
<TabsPrimitive.Root
{...props}
@@ -31,10 +45,14 @@ export const Tabs = ({
onValueChange={onValueChange || setActiveTab}
className="h-full grid grid-rows-[min-content_1fr]"
>
<div className="border-b border-default min-w-0">
<div
ref={wrapperRef}
className="flex flex-wrap justify-between border-b border-default min-w-0"
>
<TabsPrimitive.List
className="flex flex-nowrap overflow-visible"
role="tablist"
ref={tabsRef}
>
{Children.map(children, (child) => {
if (!isValidElement(child) || child.props.hidden) return null;
@@ -67,6 +85,27 @@ export const Tabs = ({
);
})}
</TabsPrimitive.List>
<div
ref={menuRef}
className={classNames('flex-1 p-1', {
'bg-vega-clight-700 dark:bg-vega-cdark-700': wrapped,
'': wrapped,
})}
>
{Children.map(children, (child) => {
if (!isValidElement(child) || child.props.hidden) return null;
return (
<TabsPrimitive.Content
value={child.props.id}
className={classNames('flex flex-nowrap gap-1', {
'justify-end': !wrapped,
})}
>
{child.props.menu}
</TabsPrimitive.Content>
);
})}
</div>
</div>
<div className="h-full overflow-auto">
{Children.map(children, (child) => {
@@ -92,6 +131,7 @@ interface TabProps {
name: string;
indicator?: ReactNode;
hidden?: boolean;
menu?: ReactNode;
}
export const Tab = ({ children, ...props }: TabProps) => {
@@ -122,3 +162,13 @@ export const LocalStoragePersistTabs = ({
/>
);
};
const isWrapped = (
tabs: HTMLDivElement | null,
menu: HTMLDivElement | null
) => {
if (!tabs || !menu) return;
const listRect = tabs.getBoundingClientRect();
const menuRect = menu.getBoundingClientRect();
return menuRect.y > listRect.y;
};
@@ -8,7 +8,7 @@ import type {
import { Intent } from '../../utils/intent';
type TradingButtonProps = {
size?: 'large' | 'medium' | 'small';
size?: 'large' | 'medium' | 'small' | 'extra-small';
intent?: Intent;
children?: ReactNode;
icon?: ReactNode;
@@ -24,24 +24,31 @@ const getClassName = (
className?: string
) =>
classNames(
'flex gap-2 items-center justify-center rounded',
'flex gap-2 items-center justify-center rounded disabled:opacity-40',
// size
{
'h-12': !subLabel && size === 'large',
'h-10': !subLabel && (!size || size === 'medium'),
'h-8': !subLabel && size === 'small',
'px-3 text-sm': !subLabel && size === 'small',
'h-6 px-2 text-xs': !subLabel && size === 'extra-small',
'px-4 text-base': !subLabel && size !== 'small',
'flex-col items-center justify-center px-3 pt-2.5 pb-2': subLabel,
},
// colours
{
'bg-vega-yellow dark:bg-vega-yellow': intent === Intent.Primary,
'bg-vega-clight-500 dark:bg-vega-cdark-500': intent === Intent.None,
'bg-vega-blue-350 dark:bg-vega-blue-650': intent === Intent.Info,
'bg-vega-orange-350 dark:bg-vega-orange-650': intent === Intent.Warning,
'bg-vega-red-350 dark:bg-vega-red-650': intent === Intent.Danger,
'bg-vega-green-350 dark:bg-vega-green-650': intent === Intent.Success,
'bg-vega-yellow hover:bg-vega-yellow-550 dark:bg-vega-yellow dark:hover:bg-vega-yellow-450':
intent === Intent.Primary,
'bg-vega-clight-500 hover:bg-vega-clight-400 dark:bg-vega-cdark-500 dark:hover:bg-vega-cdark-400':
intent === Intent.None,
'bg-vega-blue-350 hover:bg-vega-blue-400 dark:bg-vega-blue-650 dark:hover:bg-vega-blue-600':
intent === Intent.Info,
'bg-vega-orange-350 hover:bg-vega-orange-400 dark:bg-vega-orange-650 dark:hover:bg-vega-orange-600':
intent === Intent.Warning,
'bg-vega-red-350 hover:bg-vega-red-400 dark:bg-vega-red-650 dark:hover:bg-vega-red-600':
intent === Intent.Danger,
'bg-vega-green-350 hover:bg-vega-green-400 dark:bg-vega-green-650 dark:hover:bg-vega-green-600':
intent === Intent.Success,
'text-vega-clight-50 dark:text-vega-cdark-50': intent !== Intent.Primary,
'text-vega-clight-900 dark:text-vega-cdark-900':
intent === Intent.Primary,
@@ -1,6 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import { NotificationBanner } from '../notification-banner';
import { Intent } from '../../utils/intent';
import { TradingButton } from '../trading-button';
export function truncateMiddle(address: string, start = 6, end = 4) {
if (address.length < 11) return address;
@@ -24,19 +25,20 @@ export const ViewingAsBanner = ({
<NotificationBanner
data-testid="view-banner"
intent={Intent.None}
className="py-2 min-h-fit"
className="!px-1 !py-1 min-h-fit"
>
<div className="flex justify-between items-baseline">
<span>
{t('Viewing as Vega user:')} {pubKey && truncateMiddle(pubKey)}{' '}
</span>
<button
className="p-2 bg-light-dark-150 dark:bg-vega-dark-150 rounded uppercase"
<TradingButton
intent={Intent.None}
size="extra-small"
data-testid="exit-view"
onClick={disconnect}
>
{t('Exit view as')}
</button>
</TradingButton>
</div>
</NotificationBanner>
);
+1
View File
@@ -1 +1,2 @@
export * from './connect-dialog';
export * from './view-as-dialog';
@@ -0,0 +1,40 @@
import { Dialog } from '@vegaprotocol/ui-toolkit';
import { ViewConnectorForm } from './view-connector-form';
import type { ViewConnector } from '../connectors';
import { create } from 'zustand';
type ViewAsDialogStore = {
open: boolean;
setOpen: (open: boolean) => void;
};
export const useViewAsDialog = create<ViewAsDialogStore>()((set) => ({
open: false,
setOpen: (open) => set({ open }),
}));
type ViewAsDialogProps = {
connector: ViewConnector;
};
export const ViewAsDialog = ({ connector }: ViewAsDialogProps) => {
const open = useViewAsDialog((state) => state.open);
const setOpen = useViewAsDialog((state) => state.setOpen);
return (
<Dialog
open={open}
size="small"
onChange={(open) => {
setOpen(open);
}}
>
<ViewConnectorForm
connector={connector}
onConnect={() => {
setOpen(false);
}}
/>
</Dialog>
);
};
@@ -2,9 +2,10 @@ import { t } from '@vegaprotocol/i18n';
import {
Button,
FormGroup,
Icon,
Input,
InputError,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useForm } from 'react-hook-form';
import type { ViewConnector } from '../connectors';
@@ -17,7 +18,7 @@ interface FormFields {
interface RestConnectorFormProps {
connector: ViewConnector;
onConnect: (connector: ViewConnector) => void;
reset: () => void;
reset?: () => void;
}
export function ViewConnectorForm({
@@ -50,13 +51,19 @@ export function ViewConnectorForm({
return (
<>
<button
onClick={reset}
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
data-testid="back-button"
>
<Icon name={'chevron-left'} ariaLabel="back" size={4} />
</button>
{reset && (
<button
onClick={reset}
className="absolute p-2 top-0 left-0 md:top-2 md:left-2"
data-testid="back-button"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
aria-label="back"
size={16}
/>
</button>
)}
<form onSubmit={handleSubmit(onSubmit)} data-testid="view-connector-form">
<h1 className="text-2xl uppercase mb-6 text-center font-alpha calt">
{t('VIEW AS VEGA USER')}
+2
View File
@@ -12,5 +12,7 @@ export * from './vega-transaction-dialog';
export * from './provider';
export * from './connect-dialog';
export * from './utils';
export * from './storage';
export * from './is-browser-wallet-installed';
export * from './__generated__/TransactionResult';
export * from './__generated__/WithdrawalApproval';

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