feat: trading page market summary & select markets modal opening from market title & fix: positions table realised PnL (#505)
* feat: [#456] select markets modal opening from market title * feat: add a global zustand store for managing connect dialogs and landing dialog * feat: add tests * feat: [#456] make arrow configurable * feat: [#456] make arrow configurable * feat: [#456] trading tab active only on portfolio * chore: update tranches Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * fix: [#445] shallow routing from index (#484) * fix: [#445] shallow routing from index * fix: [#445] use link to redirect to market - an attempt to fix reload * fix: [#445] remove stretched link from last link - it makes all the other links unusable * fix: [#445] fix lint on select market list - remove stretched link * fix: [#456] put everything in landing folder to avoid conflicts * fix: remove condition for cypress for auto connecting * feat: [#456] add global store and fix href routing * feat: [#456] add global store and fix href routing * feat: [#456] add one more test * feat: [#154] pull market data summary * feat: [#154] move header above the trade grid child sections * feat: [#154] flex oerflow and styling updates for market summary * feat: [#154] fix styling * fix: [154] fix cyp tests and styling * fix: [#154] fix markets navigation cypress step * fix: [#154] fix for navigate to markets link * fix: failing tests from market change * fix: [#154] set nav items based on market id and show last viewed market on landing * fix: [#412] invalid decimal place on realised PnL field * fix: [#154] remove redundant curly braces * fix: [#154] show hyphen on volume if market data is undefined Co-authored-by: Matthew Russell <mattrussell36@gmail.com> Co-authored-by: dexturr <dexturr@users.noreply.github.com> Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
parent
29aa93dd3c
commit
a65c52d7d4
@ -1,6 +1,6 @@
|
|||||||
export default class BasePage {
|
export default class BasePage {
|
||||||
closeDialogBtn = 'dialog-close';
|
closeDialogBtn = 'dialog-close';
|
||||||
porfolioUrl = '/portfolio';
|
portfolioUrl = '/portfolio';
|
||||||
marketsUrl = '/markets';
|
marketsUrl = '/markets';
|
||||||
assetSelectField = 'select[name="asset"]';
|
assetSelectField = 'select[name="asset"]';
|
||||||
toAddressField = 'input[name="to"]';
|
toAddressField = 'input[name="to"]';
|
||||||
@ -10,21 +10,22 @@ export default class BasePage {
|
|||||||
dialogText = 'dialog-text';
|
dialogText = 'dialog-text';
|
||||||
|
|
||||||
closeDialog() {
|
closeDialog() {
|
||||||
cy.getByTestId(this.closeDialogBtn, { timeout: 8000 }).click({
|
cy.getByTestId(this.closeDialogBtn, { timeout: 8000 })?.click({
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToPortfolio() {
|
navigateToPortfolio() {
|
||||||
cy.get(`a[href='${this.porfolioUrl}']`).should('be.visible').click();
|
cy.get(`a[href='${this.portfolioUrl}']`)
|
||||||
|
.should('be.visible')
|
||||||
|
.click({ force: true });
|
||||||
cy.url().should('include', '/portfolio');
|
cy.url().should('include', '/portfolio');
|
||||||
cy.getByTestId('portfolio');
|
cy.getByTestId('portfolio');
|
||||||
}
|
}
|
||||||
|
|
||||||
navigateToMarkets() {
|
navigateToMarkets() {
|
||||||
cy.get(`a[href='${this.marketsUrl}']`).should('be.visible').click();
|
cy.getByTestId('markets-link').should('be.visible').click({ force: true });
|
||||||
cy.url().should('include', '/markets');
|
cy.url().should('include', '/markets');
|
||||||
cy.getByTestId('markets');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyFormErrorDisplayed(expectedError: string, expectedNumErrors: number) {
|
verifyFormErrorDisplayed(expectedError: string, expectedNumErrors: number) {
|
||||||
@ -35,7 +36,7 @@ export default class BasePage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTransactionform(args?: {
|
updateTransactionForm(args?: {
|
||||||
asset?: string;
|
asset?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
amount?: string;
|
amount?: string;
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import BasePage from './base-page';
|
import BasePage from './base-page';
|
||||||
|
|
||||||
export default class MarketPage extends BasePage {
|
export default class MarketPage extends BasePage {
|
||||||
marketRowHeaderClassname = '.ag-header-cell-text';
|
marketRowHeaderClassname = 'div > span.ag-header-cell-text';
|
||||||
marketRowNameColumn = 'tradableInstrument.instrument.code';
|
marketRowNameColumn = 'tradableInstrument.instrument.code';
|
||||||
marketRowSymbolColumn =
|
marketRowSymbolColumn =
|
||||||
'tradableInstrument.instrument.product.settlementAsset.symbol';
|
'tradableInstrument.instrument.product.settlementAsset.symbol';
|
||||||
marketRowPrices = 'flash-cell';
|
marketRowPrices = 'flash-cell';
|
||||||
marketRowDescription = 'name';
|
marketRowDescription = 'name';
|
||||||
marketStateColId = 'data';
|
marketStateColId = 'data';
|
||||||
|
openMarketMenu = 'arrow-down';
|
||||||
|
|
||||||
validateMarketsAreDisplayed() {
|
validateMarketsAreDisplayed() {
|
||||||
// We need this to ensure that ag-grid is fully rendered before asserting
|
// We need this to ensure that ag-grid is fully rendered before asserting
|
||||||
@ -27,16 +28,12 @@ export default class MarketPage extends BasePage {
|
|||||||
'Description',
|
'Description',
|
||||||
];
|
];
|
||||||
|
|
||||||
cy.get(this.marketRowHeaderClassname)
|
for (let index = 0; index < expectedMarketHeaders.length; index++) {
|
||||||
.each(($marketHeader, index) => {
|
cy.get(this.marketRowHeaderClassname).should(
|
||||||
cy.wrap($marketHeader).should(
|
'contain.text',
|
||||||
'have.text',
|
|
||||||
expectedMarketHeaders[index]
|
expectedMarketHeaders[index]
|
||||||
);
|
);
|
||||||
})
|
}
|
||||||
.then(($list) => {
|
|
||||||
cy.wrap($list).should('have.length', expectedMarketHeaders.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
cy.get(`[col-id='${this.marketRowNameColumn}']`).each(($marketName) => {
|
cy.get(`[col-id='${this.marketRowNameColumn}']`).each(($marketName) => {
|
||||||
cy.wrap($marketName).should('not.be.empty');
|
cy.wrap($marketName).should('not.be.empty');
|
||||||
@ -65,4 +62,8 @@ export default class MarketPage extends BasePage {
|
|||||||
'portfolio=orders&trade=orderbook'
|
'portfolio=orders&trade=orderbook'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickOpenMarketMenu() {
|
||||||
|
cy.getByTestId(this.openMarketMenu).click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,10 @@ import { Given } from 'cypress-cucumber-preprocessor/steps';
|
|||||||
import { hasOperationName } from '..';
|
import { hasOperationName } from '..';
|
||||||
import { generateMarketList } from '../mocks/generate-market-list';
|
import { generateMarketList } from '../mocks/generate-market-list';
|
||||||
import BasePage from '../pages/base-page';
|
import BasePage from '../pages/base-page';
|
||||||
|
import MarketPage from '../pages/markets-page';
|
||||||
|
|
||||||
const basePage = new BasePage();
|
const basePage = new BasePage();
|
||||||
|
const marketPage = new MarketPage();
|
||||||
|
|
||||||
Given('I am on the homepage', () => {
|
Given('I am on the homepage', () => {
|
||||||
cy.mockGQL('MarketsList', (req) => {
|
cy.mockGQL('MarketsList', (req) => {
|
||||||
@ -15,4 +17,5 @@ Given('I am on the homepage', () => {
|
|||||||
});
|
});
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
basePage.closeDialog();
|
basePage.closeDialog();
|
||||||
|
marketPage.validateMarketsAreDisplayed();
|
||||||
});
|
});
|
||||||
|
@ -30,7 +30,7 @@ Then('I can see the deposit form', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When('I submit a deposit with empty fields', () => {
|
When('I submit a deposit with empty fields', () => {
|
||||||
depositsPage.updateTransactionform();
|
depositsPage.updateTransactionForm();
|
||||||
depositsPage.submitForm();
|
depositsPage.submitForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ Then('I can see empty form validation errors present', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Then('I enter the following deposit details in deposit form', (table) => {
|
Then('I enter the following deposit details in deposit form', (table) => {
|
||||||
depositsPage.updateTransactionform({
|
depositsPage.updateTransactionForm({
|
||||||
asset: table.rowsHash().asset,
|
asset: table.rowsHash().asset,
|
||||||
to: Cypress.env(table.rowsHash().to),
|
to: Cypress.env(table.rowsHash().to),
|
||||||
amount: table.rowsHash().amount,
|
amount: table.rowsHash().amount,
|
||||||
@ -59,7 +59,7 @@ Then('Amount too small message shown', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
And('I enter a valid amount', () => {
|
And('I enter a valid amount', () => {
|
||||||
depositsPage.updateTransactionform({ amount: '1' });
|
depositsPage.updateTransactionForm({ amount: '1' });
|
||||||
});
|
});
|
||||||
|
|
||||||
Then('Not approved message shown', () => {
|
Then('Not approved message shown', () => {
|
||||||
|
@ -19,6 +19,7 @@ const mockMarkets = () => {
|
|||||||
Then('I navigate to markets page', () => {
|
Then('I navigate to markets page', () => {
|
||||||
mockMarkets();
|
mockMarkets();
|
||||||
marketsPage.navigateToMarkets();
|
marketsPage.navigateToMarkets();
|
||||||
|
marketsPage.clickOpenMarketMenu();
|
||||||
cy.wait('@Markets');
|
cy.wait('@Markets');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ Given('I am on the trading page for an active market', () => {
|
|||||||
|
|
||||||
cy.visit('/markets/market-id');
|
cy.visit('/markets/market-id');
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
cy.contains('Market: ACTIVE MARKET');
|
cy.contains('ACTIVE MARKET');
|
||||||
});
|
});
|
||||||
|
|
||||||
Given('I am on the trading page for a suspended market', () => {
|
Given('I am on the trading page for a suspended market', () => {
|
||||||
@ -96,7 +96,7 @@ Given('I am on the trading page for a suspended market', () => {
|
|||||||
|
|
||||||
cy.visit('/markets/market-id');
|
cy.visit('/markets/market-id');
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
cy.contains('Market: SUSPENDED MARKET');
|
cy.contains('SUSPENDED MARKET');
|
||||||
});
|
});
|
||||||
|
|
||||||
When('I click on {string} mocked market', (marketType) => {
|
When('I click on {string} mocked market', (marketType) => {
|
||||||
@ -115,11 +115,11 @@ Then('trading page for {string} market is displayed', (marketType) => {
|
|||||||
switch (marketType) {
|
switch (marketType) {
|
||||||
case 'active':
|
case 'active':
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
cy.contains('Market: ACTIVE MARKET');
|
cy.contains('ACTIVE MARKET');
|
||||||
break;
|
break;
|
||||||
case 'suspended':
|
case 'suspended':
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
cy.contains('Market: SUSPENDED MARKET');
|
cy.contains('SUSPENDED MARKET');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
tradingPage.clickOnTradesTab();
|
tradingPage.clickOnTradesTab();
|
||||||
|
@ -27,6 +27,7 @@ When('I connect to Vega Wallet', () => {
|
|||||||
Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE')
|
Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE')
|
||||||
);
|
);
|
||||||
vegaWallet.clickConnectVegaWallet();
|
vegaWallet.clickConnectVegaWallet();
|
||||||
|
vegaWallet.validateWalletConnected();
|
||||||
});
|
});
|
||||||
|
|
||||||
When('I open wallet dialog', () => {
|
When('I open wallet dialog', () => {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
|
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
|
||||||
|
import MarketPage from '../pages/markets-page';
|
||||||
import PortfolioPage from '../pages/portfolio-page';
|
import PortfolioPage from '../pages/portfolio-page';
|
||||||
import WithdrawalsPage from '../pages/withdrawals-page';
|
import WithdrawalsPage from '../pages/withdrawals-page';
|
||||||
|
|
||||||
|
const marketPage = new MarketPage();
|
||||||
const portfolioPage = new PortfolioPage();
|
const portfolioPage = new PortfolioPage();
|
||||||
const withdrawalsPage = new WithdrawalsPage();
|
const withdrawalsPage = new WithdrawalsPage();
|
||||||
|
|
||||||
Given('I navigate to withdrawal page', () => {
|
Given('I navigate to withdrawal page', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
portfolioPage.closeDialog();
|
portfolioPage.closeDialog();
|
||||||
|
marketPage.validateMarketsAreDisplayed();
|
||||||
portfolioPage.navigateToPortfolio();
|
portfolioPage.navigateToPortfolio();
|
||||||
portfolioPage.navigateToWithdraw();
|
portfolioPage.navigateToWithdraw();
|
||||||
});
|
});
|
||||||
@ -26,14 +29,14 @@ When('click submit', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When('I enter an invalid ethereum address', () => {
|
When('I enter an invalid ethereum address', () => {
|
||||||
withdrawalsPage.updateTransactionform({
|
withdrawalsPage.updateTransactionForm({
|
||||||
to: '0x0dAAACaa868f87BB4666F918742141cAEAe893Fa',
|
to: '0x0dAAACaa868f87BB4666F918742141cAEAe893Fa',
|
||||||
});
|
});
|
||||||
withdrawalsPage.clickSubmit();
|
withdrawalsPage.clickSubmit();
|
||||||
});
|
});
|
||||||
|
|
||||||
When('I select {string}', (selectedAsset) => {
|
When('I select {string}', (selectedAsset) => {
|
||||||
withdrawalsPage.updateTransactionform({
|
withdrawalsPage.updateTransactionForm({
|
||||||
asset: selectedAsset,
|
asset: selectedAsset,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -47,7 +50,7 @@ When('I click Use maximum', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When('I enter the following details in withdrawal form', (table) => {
|
When('I enter the following details in withdrawal form', (table) => {
|
||||||
withdrawalsPage.updateTransactionform({
|
withdrawalsPage.updateTransactionForm({
|
||||||
asset: table.rowsHash().asset,
|
asset: table.rowsHash().asset,
|
||||||
to: table.rowsHash().to,
|
to: table.rowsHash().to,
|
||||||
amount: table.rowsHash().amount,
|
amount: table.rowsHash().amount,
|
||||||
@ -56,7 +59,7 @@ When('I enter the following details in withdrawal form', (table) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
When('I succesfully fill in and submit withdrawal form', () => {
|
When('I succesfully fill in and submit withdrawal form', () => {
|
||||||
withdrawalsPage.updateTransactionform({
|
withdrawalsPage.updateTransactionForm({
|
||||||
asset: Cypress.env('WITHDRAWAL_ASSET_ID'),
|
asset: Cypress.env('WITHDRAWAL_ASSET_ID'),
|
||||||
amount: '0.1',
|
amount: '0.1',
|
||||||
});
|
});
|
||||||
|
@ -20,11 +20,11 @@ export default class DealTicket {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isBuy == false) {
|
if (isBuy == false) {
|
||||||
cy.getByTestId(this.sellOrder).click();
|
cy.getByTestId(this.sellOrder)?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.getByTestId(this.orderSizeField).clear().type(orderSize);
|
cy.getByTestId(this.orderSizeField)?.clear().type(orderSize);
|
||||||
cy.getByTestId(this.orderTypeDropDown).select(orderType);
|
cy.getByTestId(this.orderTypeDropDown)?.select(orderType);
|
||||||
}
|
}
|
||||||
|
|
||||||
placeLimitOrder(
|
placeLimitOrder(
|
||||||
@ -33,10 +33,10 @@ export default class DealTicket {
|
|||||||
orderPrice: string,
|
orderPrice: string,
|
||||||
orderType: string
|
orderType: string
|
||||||
) {
|
) {
|
||||||
cy.getByTestId(this.limitOrderType).click();
|
cy.getByTestId(this.limitOrderType)?.click();
|
||||||
|
|
||||||
if (isBuy == false) {
|
if (isBuy == false) {
|
||||||
cy.getByTestId(this.sellOrder).click();
|
cy.getByTestId(this.sellOrder)?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.getByTestId(this.orderSizeField).clear().type(orderSize);
|
cy.getByTestId(this.orderSizeField).clear().type(orderSize);
|
||||||
|
@ -50,6 +50,10 @@ export default class VegaWallet {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateWalletConnected() {
|
||||||
|
cy.getByTestId(this.connectVegaBtn).should('contain.text', '…');
|
||||||
|
}
|
||||||
|
|
||||||
selectPublicKey() {
|
selectPublicKey() {
|
||||||
cy.getByTestId(this.selectPublicKeyBtn).click();
|
cy.getByTestId(this.selectPublicKeyBtn).click();
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,38 @@ import { useRouter } from 'next/router';
|
|||||||
import { Vega } from '../icons/vega';
|
import { Vega } from '../icons/vega';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
import { AnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { LocalStorage, t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
|
const initNavItemsState = [
|
||||||
|
{
|
||||||
|
name: t('Portfolio'),
|
||||||
|
path: '/portfolio',
|
||||||
|
testId: 'portfolio-link',
|
||||||
|
slug: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const [navItems, setNavItems] = useState(initNavItemsState);
|
||||||
|
const marketId = LocalStorage.getItem('marketId') ?? '';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNavItems([
|
||||||
|
{
|
||||||
|
name: t('Trading'),
|
||||||
|
path: '/markets',
|
||||||
|
testId: 'markets-link',
|
||||||
|
slug: marketId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('Portfolio'),
|
||||||
|
path: '/portfolio',
|
||||||
|
testId: 'portfolio-link',
|
||||||
|
slug: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [marketId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex items-center">
|
<nav className="flex items-center">
|
||||||
<Link href="/" passHref={true}>
|
<Link href="/" passHref={true}>
|
||||||
@ -12,10 +41,7 @@ export const Navbar = () => {
|
|||||||
<Vega className="fill-black dark:fill-white" />
|
<Vega className="fill-black dark:fill-white" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{[
|
{navItems.map((route) => (
|
||||||
{ name: t('Trading'), path: '/markets' },
|
|
||||||
{ name: t('Portfolio'), path: '/portfolio' },
|
|
||||||
].map((route) => (
|
|
||||||
<NavLink key={route.path} {...route} />
|
<NavLink key={route.path} {...route} />
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
@ -26,20 +52,30 @@ interface NavLinkProps {
|
|||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
exact?: boolean;
|
exact?: boolean;
|
||||||
|
testId?: string;
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NavLink = ({ name, path, exact }: NavLinkProps) => {
|
const NavLink = ({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
exact,
|
||||||
|
testId = name,
|
||||||
|
slug = '',
|
||||||
|
}: NavLinkProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isActive =
|
const isActive =
|
||||||
router.asPath === path || (!exact && router.asPath.startsWith(path));
|
router.asPath === path || (!exact && router.asPath.startsWith(path));
|
||||||
|
const href = slug !== '' ? `${path}/${slug}` : path;
|
||||||
return (
|
return (
|
||||||
<AnchorButton
|
<AnchorButton
|
||||||
variant={isActive ? 'accent' : 'inline'}
|
variant={isActive ? 'accent' : 'inline'}
|
||||||
className="px-16 py-6 h-[38px] uppercase border-0 self-end xs:text-ui sm:text-body-large md:text-h5 lg:text-h4"
|
className="px-16 py-6 h-[38px] uppercase border-0 self-end xs:text-ui sm:text-body-large md:text-h5 lg:text-h4"
|
||||||
href={path}
|
data-testid={testId}
|
||||||
|
href={href}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(path);
|
router.push(href);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { gql, useQuery } from '@apollo/client';
|
import { gql, useQuery } from '@apollo/client';
|
||||||
|
import { LocalStorage } from '@vegaprotocol/react-helpers';
|
||||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
@ -35,10 +36,13 @@ export function Index() {
|
|||||||
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
|
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
|
||||||
const { data, error, loading } = useQuery<MarketsLanding>(MARKETS_QUERY);
|
const { data, error, loading } = useQuery<MarketsLanding>(MARKETS_QUERY);
|
||||||
const setLandingDialog = useGlobalStore((state) => state.setLandingDialog);
|
const setLandingDialog = useGlobalStore((state) => state.setLandingDialog);
|
||||||
|
const lastSelectedMarketId = LocalStorage.getItem('marketId');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const marketId = marketList(data)[0]?.id;
|
const marketId = lastSelectedMarketId
|
||||||
|
? lastSelectedMarketId
|
||||||
|
: marketList(data)[0]?.id;
|
||||||
|
|
||||||
// If a default market is found, go to it with the landing dialog open
|
// If a default market is found, go to it with the landing dialog open
|
||||||
if (marketId) {
|
if (marketId) {
|
||||||
@ -50,7 +54,7 @@ export function Index() {
|
|||||||
replace('/markets');
|
replace('/markets');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, replace, setLandingDialog]);
|
}, [data, lastSelectedMarketId, replace, setLandingDialog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer data={data} loading={loading} error={error}>
|
<AsyncRenderer data={data} loading={loading} error={error}>
|
||||||
|
@ -1,21 +1,55 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import type { Market, MarketVariables } from './__generated__/Market';
|
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { PageQueryContainer } from '../../components/page-query-container';
|
import { PageQueryContainer } from '../../components/page-query-container';
|
||||||
import { TradeGrid, TradePanels } from './trade-grid';
|
import { TradeGrid, TradePanels } from './trade-grid';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { LocalStorage, t } from '@vegaprotocol/react-helpers';
|
||||||
import { useGlobalStore } from '../../stores';
|
import { useGlobalStore } from '../../stores';
|
||||||
import { LandingDialog } from '@vegaprotocol/market-list';
|
import { LandingDialog } from '@vegaprotocol/market-list';
|
||||||
|
import type { Market, MarketVariables } from './__generated__/Market';
|
||||||
|
import { Interval } from '@vegaprotocol/types';
|
||||||
|
|
||||||
// Top level page query
|
// Top level page query
|
||||||
const MARKET_QUERY = gql`
|
const MARKET_QUERY = gql`
|
||||||
query Market($marketId: ID!) {
|
query Market($marketId: ID!, $interval: Interval!, $since: String!) {
|
||||||
market(id: $marketId) {
|
market(id: $marketId) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
tradingMode
|
||||||
|
state
|
||||||
|
decimalPlaces
|
||||||
|
data {
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
markPrice
|
||||||
|
indicativeVolume
|
||||||
|
bestBidVolume
|
||||||
|
bestOfferVolume
|
||||||
|
bestStaticBidVolume
|
||||||
|
bestStaticOfferVolume
|
||||||
|
indicativeVolume
|
||||||
|
}
|
||||||
|
tradableInstrument {
|
||||||
|
instrument {
|
||||||
|
name
|
||||||
|
code
|
||||||
|
metadata {
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
marketTimestamps {
|
||||||
|
open
|
||||||
|
close
|
||||||
|
}
|
||||||
|
candles(interval: $interval, since: $since) {
|
||||||
|
open
|
||||||
|
close
|
||||||
|
volume
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -29,6 +63,9 @@ const MarketPage = ({ id }: { id?: string }) => {
|
|||||||
const marketId =
|
const marketId =
|
||||||
id || (Array.isArray(query.marketId) ? query.marketId[0] : query.marketId);
|
id || (Array.isArray(query.marketId) ? query.marketId[0] : query.marketId);
|
||||||
|
|
||||||
|
const yesterday = Math.round(new Date().getTime() / 1000) - 24 * 3600;
|
||||||
|
const yTimestamp = new Date(yesterday * 1000).toISOString();
|
||||||
|
|
||||||
if (!marketId) {
|
if (!marketId) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
@ -37,12 +74,15 @@ const MarketPage = ({ id }: { id?: string }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LocalStorage.setItem('marketId', marketId);
|
||||||
return (
|
return (
|
||||||
<PageQueryContainer<Market, MarketVariables>
|
<PageQueryContainer<Market, MarketVariables>
|
||||||
query={MARKET_QUERY}
|
query={MARKET_QUERY}
|
||||||
options={{
|
options={{
|
||||||
variables: {
|
variables: {
|
||||||
marketId,
|
marketId,
|
||||||
|
interval: Interval.I1H,
|
||||||
|
since: yTimestamp,
|
||||||
},
|
},
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
}}
|
}}
|
||||||
|
145
apps/trading/pages/markets/__generated__/Market.ts
generated
145
apps/trading/pages/markets/__generated__/Market.ts
generated
@ -3,10 +3,112 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { Interval, MarketTradingMode, MarketState } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: Market
|
// GraphQL query operation: Market
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Market_market_data_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_data {
|
||||||
|
__typename: "MarketData";
|
||||||
|
/**
|
||||||
|
* market id of the associated mark price
|
||||||
|
*/
|
||||||
|
market: Market_market_data_market;
|
||||||
|
/**
|
||||||
|
* the mark price (actually an unsigned int)
|
||||||
|
*/
|
||||||
|
markPrice: string;
|
||||||
|
/**
|
||||||
|
* indicative volume if the auction ended now, 0 if not in auction mode
|
||||||
|
*/
|
||||||
|
indicativeVolume: string;
|
||||||
|
/**
|
||||||
|
* the aggregated volume being bid at the best bid price.
|
||||||
|
*/
|
||||||
|
bestBidVolume: string;
|
||||||
|
/**
|
||||||
|
* the aggregated volume being offered at the best offer price.
|
||||||
|
*/
|
||||||
|
bestOfferVolume: string;
|
||||||
|
/**
|
||||||
|
* the aggregated volume being offered at the best static bid price, excluding pegged orders
|
||||||
|
*/
|
||||||
|
bestStaticBidVolume: string;
|
||||||
|
/**
|
||||||
|
* the aggregated volume being offered at the best static offer price, excluding pegged orders.
|
||||||
|
*/
|
||||||
|
bestStaticOfferVolume: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_tradableInstrument_instrument_metadata {
|
||||||
|
__typename: "InstrumentMetadata";
|
||||||
|
/**
|
||||||
|
* An arbitrary list of tags to associated to associate to the Instrument (string list)
|
||||||
|
*/
|
||||||
|
tags: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_tradableInstrument_instrument {
|
||||||
|
__typename: "Instrument";
|
||||||
|
/**
|
||||||
|
* Full and fairly descriptive name for the instrument
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
/**
|
||||||
|
* Metadata for this instrument
|
||||||
|
*/
|
||||||
|
metadata: Market_market_tradableInstrument_instrument_metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_tradableInstrument {
|
||||||
|
__typename: "TradableInstrument";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a fully specified instrument.
|
||||||
|
*/
|
||||||
|
instrument: Market_market_tradableInstrument_instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_marketTimestamps {
|
||||||
|
__typename: "MarketTimestamps";
|
||||||
|
/**
|
||||||
|
* Time when the market is open and ready to accept trades
|
||||||
|
*/
|
||||||
|
open: string | null;
|
||||||
|
/**
|
||||||
|
* Time when the market is closed
|
||||||
|
*/
|
||||||
|
close: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Market_market_candles {
|
||||||
|
__typename: "Candle";
|
||||||
|
/**
|
||||||
|
* Open price (uint64)
|
||||||
|
*/
|
||||||
|
open: string;
|
||||||
|
/**
|
||||||
|
* Close price (uint64)
|
||||||
|
*/
|
||||||
|
close: string;
|
||||||
|
/**
|
||||||
|
* Volume price (uint64)
|
||||||
|
*/
|
||||||
|
volume: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Market_market {
|
export interface Market_market {
|
||||||
__typename: "Market";
|
__typename: "Market";
|
||||||
/**
|
/**
|
||||||
@ -17,6 +119,47 @@ export interface Market_market {
|
|||||||
* Market full name
|
* Market full name
|
||||||
*/
|
*/
|
||||||
name: string;
|
name: string;
|
||||||
|
/**
|
||||||
|
* Current mode of execution of the market
|
||||||
|
*/
|
||||||
|
tradingMode: MarketTradingMode;
|
||||||
|
/**
|
||||||
|
* Current state of the market
|
||||||
|
*/
|
||||||
|
state: MarketState;
|
||||||
|
/**
|
||||||
|
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||||
|
* number denominated in the currency of the Market. (uint64)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* Currency Balance decimalPlaces Real Balance
|
||||||
|
* GBP 100 0 GBP 100
|
||||||
|
* GBP 100 2 GBP 1.00
|
||||||
|
* GBP 100 4 GBP 0.01
|
||||||
|
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||||
|
*
|
||||||
|
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||||
|
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||||
|
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||||
|
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||||
|
*/
|
||||||
|
decimalPlaces: number;
|
||||||
|
/**
|
||||||
|
* marketData for the given market
|
||||||
|
*/
|
||||||
|
data: Market_market_data | null;
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a tradable instrument.
|
||||||
|
*/
|
||||||
|
tradableInstrument: Market_market_tradableInstrument;
|
||||||
|
/**
|
||||||
|
* timestamps for state changes in the market
|
||||||
|
*/
|
||||||
|
marketTimestamps: Market_market_marketTimestamps;
|
||||||
|
/**
|
||||||
|
* Candles on a market, for the 'last' n candles, at 'interval' seconds as specified by params
|
||||||
|
*/
|
||||||
|
candles: (Market_market_candles | null)[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Market {
|
export interface Market {
|
||||||
@ -28,4 +171,6 @@ export interface Market {
|
|||||||
|
|
||||||
export interface MarketVariables {
|
export interface MarketVariables {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
|
interval: Interval;
|
||||||
|
since: string;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { MarketsContainer } from '@vegaprotocol/market-list';
|
import { MarketsContainer } from '@vegaprotocol/market-list';
|
||||||
|
|
||||||
const Markets = () => {
|
const Markets = () => <MarketsContainer />;
|
||||||
return <MarketsContainer />;
|
|
||||||
};
|
|
||||||
|
|
||||||
Markets.getInitialProps = () => ({
|
Markets.getInitialProps = () => ({
|
||||||
page: 'markets',
|
page: 'markets',
|
||||||
|
@ -13,6 +13,9 @@ import { t } from '@vegaprotocol/react-helpers';
|
|||||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||||
|
import { SelectMarketDialog } from '@vegaprotocol/market-list';
|
||||||
|
import { ArrowDown, PriceCellChange } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { CandleClose } from '@vegaprotocol/types';
|
||||||
|
|
||||||
const TradingViews = {
|
const TradingViews = {
|
||||||
Candles: CandlesChartContainer,
|
Candles: CandlesChartContainer,
|
||||||
@ -31,6 +34,58 @@ interface TradeGridProps {
|
|||||||
market: Market_market;
|
market: Market_market;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TradeMarketHeader = ({ market }: TradeGridProps) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const candlesClose: string[] = (market?.candles || [])
|
||||||
|
.map((candle) => candle?.close)
|
||||||
|
.filter((c): c is CandleClose => c !== null);
|
||||||
|
const headerItemClassName = 'whitespace-nowrap flex flex-col';
|
||||||
|
const itemClassName =
|
||||||
|
'font-sans font-normal mb-0 text-dark/80 dark:text-white/80 text-ui-small';
|
||||||
|
const itemValueClassName =
|
||||||
|
'capitalize font-sans tracking-tighter text-black dark:text-white text-ui';
|
||||||
|
return (
|
||||||
|
<header className="w-full p-8">
|
||||||
|
<SelectMarketDialog dialogOpen={open} setDialogOpen={setOpen} />
|
||||||
|
<div className="flex flex-col md:flex-row gap-20 md:gap-64 ml-auto mr-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
className="shrink-0 dark:text-vega-yellow text-black text-h5 flex items-center gap-8 px-4 py-0 h-37 hover:bg-vega-yellow dark:hover:bg-white/20"
|
||||||
|
>
|
||||||
|
<span className="break-words text-left">{market.name}</span>
|
||||||
|
<ArrowDown color="yellow" borderX={8} borderTop={12} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-auto items-start gap-64 overflow-x-scroll whitespace-nowrap w-[400px]">
|
||||||
|
<div className={headerItemClassName}>
|
||||||
|
<span className={itemClassName}>Change (24h)</span>
|
||||||
|
<PriceCellChange
|
||||||
|
candles={candlesClose}
|
||||||
|
decimalPlaces={market.decimalPlaces}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={headerItemClassName}>
|
||||||
|
<span className={itemClassName}>Volume</span>
|
||||||
|
<span className={itemValueClassName}>
|
||||||
|
{market.data && market.data.indicativeVolume !== '0'
|
||||||
|
? market.data.indicativeVolume
|
||||||
|
: '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={headerItemClassName}>
|
||||||
|
<span className={itemClassName}>Trading mode</span>
|
||||||
|
<span className={itemValueClassName}>{market.tradingMode}</span>
|
||||||
|
</div>
|
||||||
|
<div className={headerItemClassName}>
|
||||||
|
<span className={itemClassName}>State</span>
|
||||||
|
<span className={itemValueClassName}>{market.state}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const TradeGrid = ({ market }: TradeGridProps) => {
|
export const TradeGrid = ({ market }: TradeGridProps) => {
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
'h-full max-h-full',
|
'h-full max-h-full',
|
||||||
@ -38,14 +93,12 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
|
|||||||
'bg-black-10 dark:bg-white-10',
|
'bg-black-10 dark:bg-white-10',
|
||||||
'text-ui'
|
'text-ui'
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<TradeMarketHeader market={market} />
|
||||||
<div className={wrapperClasses}>
|
<div className={wrapperClasses}>
|
||||||
<header className="col-start-1 col-end-2 row-start-1 row-end-1 p-8">
|
<TradeGridChild className="row-start-1 row-end-3">
|
||||||
<h1>
|
|
||||||
{t('Market')}: {market.name}
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
<TradeGridChild className="col-start-1 col-end-2">
|
|
||||||
<GridTabs group="chart">
|
<GridTabs group="chart">
|
||||||
<GridTab id="candles" name={t('Candles')}>
|
<GridTab id="candles" name={t('Candles')}>
|
||||||
<TradingViews.Candles marketId={market.id} />
|
<TradingViews.Candles marketId={market.id} />
|
||||||
@ -82,6 +135,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
|
|||||||
</GridTabs>
|
</GridTabs>
|
||||||
</TradeGridChild>
|
</TradeGridChild>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,11 +178,7 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||||
<header className="p-8">
|
<TradeMarketHeader market={market} />
|
||||||
<h1>
|
|
||||||
{t('Market')}: {market.name}
|
|
||||||
</h1>
|
|
||||||
</header>
|
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ width, height }) => (
|
{({ width, height }) => (
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './landing-dialog';
|
export * from './landing-dialog';
|
||||||
|
export * from './select-market-dialog';
|
||||||
export * from './select-market-list';
|
export * from './select-market-list';
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { SelectMarketDialog } from './select-market-dialog';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'next/link',
|
||||||
|
() =>
|
||||||
|
({ children }: { children: ReactNode }) =>
|
||||||
|
children
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('next/router', () => ({
|
||||||
|
useRouter() {
|
||||||
|
return {
|
||||||
|
route: '/',
|
||||||
|
pathname: '',
|
||||||
|
query: '',
|
||||||
|
asPath: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('SelectMarketDialog', () => {
|
||||||
|
it('should render select a market dialog', () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider>
|
||||||
|
<SelectMarketDialog dialogOpen={true} setDialogOpen={() => jest.fn()} />
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Select a market')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { MarketsContainer } from '../markets-container';
|
||||||
|
|
||||||
|
export interface SelectMarketListProps {
|
||||||
|
dialogOpen: boolean;
|
||||||
|
setDialogOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectMarketDialog = ({
|
||||||
|
dialogOpen,
|
||||||
|
setDialogOpen,
|
||||||
|
}: SelectMarketListProps) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
title={t('Select a market')}
|
||||||
|
intent={Intent.Prompt}
|
||||||
|
open={dialogOpen}
|
||||||
|
onChange={() => setDialogOpen(false)}
|
||||||
|
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
|
||||||
|
contentClassNames="w-full md:w-[1120px]"
|
||||||
|
>
|
||||||
|
<div className="h-[200px] w-full">
|
||||||
|
<MarketsContainer />
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
@ -3,19 +3,21 @@ import {
|
|||||||
PriceCell,
|
PriceCell,
|
||||||
t,
|
t,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type { CandleClose } from '@vegaprotocol/types';
|
||||||
import { PriceCellChange, Sparkline } from '@vegaprotocol/ui-toolkit';
|
import { PriceCellChange, Sparkline } from '@vegaprotocol/ui-toolkit';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { mapDataToMarketList } from '../../utils';
|
import { mapDataToMarketList } from '../../utils';
|
||||||
import type { MarketList } from '../markets-container/__generated__/MarketList';
|
import type { MarketList } from '../markets-container/__generated__/MarketList';
|
||||||
|
|
||||||
export interface SelectMarketListProps {
|
export interface SelectMarketListDataProps {
|
||||||
data: MarketList | undefined;
|
data: MarketList | undefined;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CandleClose = Required<string>;
|
export const SelectMarketList = ({
|
||||||
|
data,
|
||||||
export const SelectMarketList = ({ data, onSelect }: SelectMarketListProps) => {
|
onSelect,
|
||||||
|
}: SelectMarketListDataProps) => {
|
||||||
const thClassNames = (direction: 'left' | 'right') =>
|
const thClassNames = (direction: 'left' | 'right') =>
|
||||||
`px-8 text-${direction} font-sans font-normal text-ui-small leading-9 mb-0 text-dark/80 dark:text-white/80`;
|
`px-8 text-${direction} font-sans font-normal text-ui-small leading-9 mb-0 text-dark/80 dark:text-white/80`;
|
||||||
const tdClassNames =
|
const tdClassNames =
|
||||||
|
@ -13,7 +13,7 @@ import type {
|
|||||||
import { marketsDataProvider } from './markets-data-provider';
|
import { marketsDataProvider } from './markets-data-provider';
|
||||||
|
|
||||||
export const MarketsContainer = () => {
|
export const MarketsContainer = () => {
|
||||||
const { pathname, push } = useRouter();
|
const { push } = useRouter();
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
(delta: Markets_markets_data) => {
|
(delta: Markets_markets_data) => {
|
||||||
@ -57,7 +57,7 @@ export const MarketsContainer = () => {
|
|||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
data={data}
|
data={data}
|
||||||
onRowClicked={(id) =>
|
onRowClicked={(id) =>
|
||||||
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
push(`/markets/${id}?portfolio=orders&trade=orderbook`)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
|
@ -4,7 +4,7 @@ import type { Positions_party_positions } from './__generated__/Positions';
|
|||||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
|
||||||
const singleRow: Positions_party_positions = {
|
const singleRow: Positions_party_positions = {
|
||||||
realisedPNL: '5',
|
realisedPNL: '520000000',
|
||||||
openVolume: '100',
|
openVolume: '100',
|
||||||
unrealisedPNL: '895000',
|
unrealisedPNL: '895000',
|
||||||
averageEntryPrice: '1129935',
|
averageEntryPrice: '1129935',
|
||||||
@ -93,7 +93,7 @@ it('Correct formatting applied', async () => {
|
|||||||
'+100',
|
'+100',
|
||||||
'11.29935',
|
'11.29935',
|
||||||
'11.38885',
|
'11.38885',
|
||||||
'+5',
|
'+5,200.000',
|
||||||
];
|
];
|
||||||
cells.forEach((cell, i) => {
|
cells.forEach((cell, i) => {
|
||||||
expect(cell).toHaveTextContent(expectedValues[i]);
|
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||||
|
@ -131,8 +131,10 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
|||||||
'color-vega-red': ({ value }: { value: string }) =>
|
'color-vega-red': ({ value }: { value: string }) =>
|
||||||
Number(value) < 0,
|
Number(value) < 0,
|
||||||
}}
|
}}
|
||||||
valueFormatter={({ value }: ValueFormatterParams) =>
|
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||||
volumePrefix(value)
|
volumePrefix(
|
||||||
|
addDecimalsFormatNumber(value, data.market.decimalPlaces, 3)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
cellRenderer="PriceFlashCell"
|
cellRenderer="PriceFlashCell"
|
||||||
/>
|
/>
|
||||||
|
@ -47,7 +47,7 @@ interface GetDelta<SubscriptionData, Delta> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param subscriptionQuery query that will be used for subscription
|
* @param subscriptionQuery query that will be used for subscription
|
||||||
* @param update function that will be execued on each onNext, it should update data base on delta, it can restart data provider
|
* @param update function that will be executed on each onNext, it should update data base on delta, it can restart data provider
|
||||||
* @param getData transforms received query data to format that will be stored in data provider
|
* @param getData transforms received query data to format that will be stored in data provider
|
||||||
* @param getDelta transforms delta data to format that will be stored in data provider
|
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||||
* @param fetchPolicy
|
* @param fetchPolicy
|
||||||
@ -63,7 +63,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
): Subscribe<Data, Delta> {
|
): Subscribe<Data, Delta> {
|
||||||
// list of callbacks passed through subscribe call
|
// list of callbacks passed through subscribe call
|
||||||
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
||||||
// subscription is started before inital query, all deltas that will arrive before inital query response are put on queue
|
// subscription is started before initial query, all deltas that will arrive before initial query response are put on queue
|
||||||
const updateQueue: Delta[] = [];
|
const updateQueue: Delta[] = [];
|
||||||
|
|
||||||
let variables: OperationVariables | undefined = undefined;
|
let variables: OperationVariables | undefined = undefined;
|
||||||
@ -88,7 +88,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
callbacks.forEach((callback) => notify(callback, delta));
|
callbacks.forEach((callback) => notify(callback, delta));
|
||||||
};
|
};
|
||||||
|
|
||||||
const initalFetch = async () => {
|
const initialFetch = async () => {
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
});
|
});
|
||||||
data = getData(res.data);
|
data = getData(res.data);
|
||||||
// if there was some updates received from subscription during initial query loading apply them on just reveived data
|
// if there was some updates received from subscription during initial query loading apply them on just received data
|
||||||
if (data && updateQueue && updateQueue.length > 0) {
|
if (data && updateQueue && updateQueue.length > 0) {
|
||||||
data = produce(data, (draft) => {
|
data = produce(data, (draft) => {
|
||||||
while (updateQueue.length) {
|
while (updateQueue.length) {
|
||||||
@ -135,7 +135,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
} else {
|
} else {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = undefined;
|
error = undefined;
|
||||||
initalFetch();
|
initialFetch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
},
|
},
|
||||||
() => restart()
|
() => restart()
|
||||||
);
|
);
|
||||||
await initalFetch();
|
await initialFetch();
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
@ -243,7 +243,7 @@ const memoize = <Data, Delta>(
|
|||||||
/**
|
/**
|
||||||
* @param query Query<QueryData>
|
* @param query Query<QueryData>
|
||||||
* @param subscriptionQuery Query<SubscriptionData> query that will be used for subscription
|
* @param subscriptionQuery Query<SubscriptionData> query that will be used for subscription
|
||||||
* @param update Update<Data, Delta> function that will be execued on each onNext, it should update data base on delta, it can restart data provider
|
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can restart data provider
|
||||||
* @param getData transforms received query data to format that will be stored in data provider
|
* @param getData transforms received query data to format that will be stored in data provider
|
||||||
* @param getDelta transforms delta data to format that will be stored in data provider
|
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||||
* @param fetchPolicy
|
* @param fetchPolicy
|
||||||
|
@ -13,7 +13,7 @@ export const PriceCell = React.memo(
|
|||||||
return <span data-testid="price">-</span>;
|
return <span data-testid="price">-</span>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="font-mono relative" data-testid="price">
|
<span className="font-mono relative text-ui-small" data-testid="price">
|
||||||
{valueFormatted}
|
{valueFormatted}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
1
libs/types/src/candle.ts
Normal file
1
libs/types/src/candle.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type CandleClose = Required<string>;
|
@ -1 +1,2 @@
|
|||||||
export * from './__generated__/globalTypes';
|
export * from './__generated__/globalTypes';
|
||||||
|
export * from './candle';
|
||||||
|
@ -1,13 +1,38 @@
|
|||||||
export const ArrowUp = () => (
|
export interface ArrowStyleProps {
|
||||||
|
color?: string;
|
||||||
|
borderX?: number;
|
||||||
|
borderTop?: number;
|
||||||
|
borderBottom?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArrowUp = ({
|
||||||
|
color = 'green',
|
||||||
|
borderX = 4,
|
||||||
|
borderBottom = 4,
|
||||||
|
}: ArrowStyleProps) => (
|
||||||
<span
|
<span
|
||||||
data-testid="arrow-up"
|
data-testid="arrow-up"
|
||||||
className="w-0 h-0 border-x border-x-[4px] border-solid border-x-transparent border-b-[4px] border-b-green-dark dark:border-b-green"
|
style={{
|
||||||
|
borderLeft: `${borderX}px solid transparent`,
|
||||||
|
borderRight: `${borderX}px solid transparent`,
|
||||||
|
borderBottom: `${borderBottom}px solid`,
|
||||||
|
}}
|
||||||
|
className={`w-0 h-0 border-b-${color}-dark dark:border-b-${color}`}
|
||||||
></span>
|
></span>
|
||||||
);
|
);
|
||||||
export const ArrowDown = () => (
|
export const ArrowDown = ({
|
||||||
|
color = 'red',
|
||||||
|
borderX = 4,
|
||||||
|
borderTop = 4,
|
||||||
|
}: ArrowStyleProps) => (
|
||||||
<span
|
<span
|
||||||
data-testid="arrow-down"
|
data-testid="arrow-down"
|
||||||
className="w-0 h-0 border-x border-x-[4px] border-solid border-x-transparent border-t-[4px] border-t-red-dark dark:border-t-red"
|
style={{
|
||||||
|
borderLeft: `${borderX}px solid transparent`,
|
||||||
|
borderRight: `${borderX}px solid transparent`,
|
||||||
|
borderTop: `${borderTop}px solid`,
|
||||||
|
}}
|
||||||
|
className={`w-0 h-0 border-t-${color}-dark dark:border-t-${color}`}
|
||||||
></span>
|
></span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -215,12 +215,12 @@ export const AnchorButton = forwardRef<HTMLAnchorElement, AnchorButtonProps>(
|
|||||||
className,
|
className,
|
||||||
prependIconName,
|
prependIconName,
|
||||||
appendIconName,
|
appendIconName,
|
||||||
...prosp
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
<a ref={ref} className={classes(className, variant)} {...prosp}>
|
<a ref={ref} className={classes(className, variant)} {...props}>
|
||||||
{getContent(children, prependIconName, appendIconName)}
|
{getContent(children, prependIconName, appendIconName)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -12,6 +12,7 @@ interface DialogProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
intent?: Intent;
|
intent?: Intent;
|
||||||
titleClassNames?: string;
|
titleClassNames?: string;
|
||||||
|
contentClassNames?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
@ -21,13 +22,15 @@ export function Dialog({
|
|||||||
title,
|
title,
|
||||||
intent,
|
intent,
|
||||||
titleClassNames,
|
titleClassNames,
|
||||||
|
contentClassNames,
|
||||||
}: DialogProps) {
|
}: DialogProps) {
|
||||||
const contentClasses = classNames(
|
const contentClasses = classNames(
|
||||||
// Positions the modal in the center of screen
|
// Positions the modal in the center of screen
|
||||||
'z-20 fixed w-full md:w-[520px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
'z-20 fixed w-full md:w-[520px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||||
// Need to apply background and text colors again as content is rendered in a portal
|
// Need to apply background and text colors again as content is rendered in a portal
|
||||||
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
||||||
getIntentShadow(intent)
|
getIntentShadow(intent),
|
||||||
|
contentClassNames
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange(x)}>
|
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange(x)}>
|
||||||
|
Loading…
Reference in New Issue
Block a user