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:
m.ray 2022-06-06 19:19:56 +03:00 committed by GitHub
parent 29aa93dd3c
commit a65c52d7d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 507 additions and 124 deletions

View File

@ -1,6 +1,6 @@
export default class BasePage {
closeDialogBtn = 'dialog-close';
porfolioUrl = '/portfolio';
portfolioUrl = '/portfolio';
marketsUrl = '/markets';
assetSelectField = 'select[name="asset"]';
toAddressField = 'input[name="to"]';
@ -10,21 +10,22 @@ export default class BasePage {
dialogText = 'dialog-text';
closeDialog() {
cy.getByTestId(this.closeDialogBtn, { timeout: 8000 }).click({
cy.getByTestId(this.closeDialogBtn, { timeout: 8000 })?.click({
force: true,
});
}
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.getByTestId('portfolio');
}
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.getByTestId('markets');
}
verifyFormErrorDisplayed(expectedError: string, expectedNumErrors: number) {
@ -35,7 +36,7 @@ export default class BasePage {
);
}
updateTransactionform(args?: {
updateTransactionForm(args?: {
asset?: string;
to?: string;
amount?: string;

View File

@ -1,13 +1,14 @@
import BasePage from './base-page';
export default class MarketPage extends BasePage {
marketRowHeaderClassname = '.ag-header-cell-text';
marketRowHeaderClassname = 'div > span.ag-header-cell-text';
marketRowNameColumn = 'tradableInstrument.instrument.code';
marketRowSymbolColumn =
'tradableInstrument.instrument.product.settlementAsset.symbol';
marketRowPrices = 'flash-cell';
marketRowDescription = 'name';
marketStateColId = 'data';
openMarketMenu = 'arrow-down';
validateMarketsAreDisplayed() {
// We need this to ensure that ag-grid is fully rendered before asserting
@ -27,16 +28,12 @@ export default class MarketPage extends BasePage {
'Description',
];
cy.get(this.marketRowHeaderClassname)
.each(($marketHeader, index) => {
cy.wrap($marketHeader).should(
'have.text',
expectedMarketHeaders[index]
);
})
.then(($list) => {
cy.wrap($list).should('have.length', expectedMarketHeaders.length);
});
for (let index = 0; index < expectedMarketHeaders.length; index++) {
cy.get(this.marketRowHeaderClassname).should(
'contain.text',
expectedMarketHeaders[index]
);
}
cy.get(`[col-id='${this.marketRowNameColumn}']`).each(($marketName) => {
cy.wrap($marketName).should('not.be.empty');
@ -65,4 +62,8 @@ export default class MarketPage extends BasePage {
'portfolio=orders&trade=orderbook'
);
}
clickOpenMarketMenu() {
cy.getByTestId(this.openMarketMenu).click();
}
}

View File

@ -2,8 +2,10 @@ import { Given } from 'cypress-cucumber-preprocessor/steps';
import { hasOperationName } from '..';
import { generateMarketList } from '../mocks/generate-market-list';
import BasePage from '../pages/base-page';
import MarketPage from '../pages/markets-page';
const basePage = new BasePage();
const marketPage = new MarketPage();
Given('I am on the homepage', () => {
cy.mockGQL('MarketsList', (req) => {
@ -15,4 +17,5 @@ Given('I am on the homepage', () => {
});
cy.visit('/');
basePage.closeDialog();
marketPage.validateMarketsAreDisplayed();
});

View File

@ -30,7 +30,7 @@ Then('I can see the deposit form', () => {
});
When('I submit a deposit with empty fields', () => {
depositsPage.updateTransactionform();
depositsPage.updateTransactionForm();
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) => {
depositsPage.updateTransactionform({
depositsPage.updateTransactionForm({
asset: table.rowsHash().asset,
to: Cypress.env(table.rowsHash().to),
amount: table.rowsHash().amount,
@ -59,7 +59,7 @@ Then('Amount too small message shown', () => {
});
And('I enter a valid amount', () => {
depositsPage.updateTransactionform({ amount: '1' });
depositsPage.updateTransactionForm({ amount: '1' });
});
Then('Not approved message shown', () => {

View File

@ -19,6 +19,7 @@ const mockMarkets = () => {
Then('I navigate to markets page', () => {
mockMarkets();
marketsPage.navigateToMarkets();
marketsPage.clickOpenMarketMenu();
cy.wait('@Markets');
});

View File

@ -88,7 +88,7 @@ Given('I am on the trading page for an active market', () => {
cy.visit('/markets/market-id');
cy.wait('@Market');
cy.contains('Market: ACTIVE MARKET');
cy.contains('ACTIVE 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.wait('@Market');
cy.contains('Market: SUSPENDED MARKET');
cy.contains('SUSPENDED MARKET');
});
When('I click on {string} mocked market', (marketType) => {
@ -115,11 +115,11 @@ Then('trading page for {string} market is displayed', (marketType) => {
switch (marketType) {
case 'active':
cy.wait('@Market');
cy.contains('Market: ACTIVE MARKET');
cy.contains('ACTIVE MARKET');
break;
case 'suspended':
cy.wait('@Market');
cy.contains('Market: SUSPENDED MARKET');
cy.contains('SUSPENDED MARKET');
break;
}
tradingPage.clickOnTradesTab();

View File

@ -27,6 +27,7 @@ When('I connect to Vega Wallet', () => {
Cypress.env('TRADING_TEST_VEGA_WALLET_PASSPHRASE')
);
vegaWallet.clickConnectVegaWallet();
vegaWallet.validateWalletConnected();
});
When('I open wallet dialog', () => {

View File

@ -1,13 +1,16 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps';
import MarketPage from '../pages/markets-page';
import PortfolioPage from '../pages/portfolio-page';
import WithdrawalsPage from '../pages/withdrawals-page';
const marketPage = new MarketPage();
const portfolioPage = new PortfolioPage();
const withdrawalsPage = new WithdrawalsPage();
Given('I navigate to withdrawal page', () => {
cy.visit('/');
portfolioPage.closeDialog();
marketPage.validateMarketsAreDisplayed();
portfolioPage.navigateToPortfolio();
portfolioPage.navigateToWithdraw();
});
@ -26,14 +29,14 @@ When('click submit', () => {
});
When('I enter an invalid ethereum address', () => {
withdrawalsPage.updateTransactionform({
withdrawalsPage.updateTransactionForm({
to: '0x0dAAACaa868f87BB4666F918742141cAEAe893Fa',
});
withdrawalsPage.clickSubmit();
});
When('I select {string}', (selectedAsset) => {
withdrawalsPage.updateTransactionform({
withdrawalsPage.updateTransactionForm({
asset: selectedAsset,
});
});
@ -47,7 +50,7 @@ When('I click Use maximum', () => {
});
When('I enter the following details in withdrawal form', (table) => {
withdrawalsPage.updateTransactionform({
withdrawalsPage.updateTransactionForm({
asset: table.rowsHash().asset,
to: table.rowsHash().to,
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', () => {
withdrawalsPage.updateTransactionform({
withdrawalsPage.updateTransactionForm({
asset: Cypress.env('WITHDRAWAL_ASSET_ID'),
amount: '0.1',
});

View File

@ -20,11 +20,11 @@ export default class DealTicket {
);
if (isBuy == false) {
cy.getByTestId(this.sellOrder).click();
cy.getByTestId(this.sellOrder)?.click();
}
cy.getByTestId(this.orderSizeField).clear().type(orderSize);
cy.getByTestId(this.orderTypeDropDown).select(orderType);
cy.getByTestId(this.orderSizeField)?.clear().type(orderSize);
cy.getByTestId(this.orderTypeDropDown)?.select(orderType);
}
placeLimitOrder(
@ -33,10 +33,10 @@ export default class DealTicket {
orderPrice: string,
orderType: string
) {
cy.getByTestId(this.limitOrderType).click();
cy.getByTestId(this.limitOrderType)?.click();
if (isBuy == false) {
cy.getByTestId(this.sellOrder).click();
cy.getByTestId(this.sellOrder)?.click();
}
cy.getByTestId(this.orderSizeField).clear().type(orderSize);

View File

@ -50,6 +50,10 @@ export default class VegaWallet {
);
}
validateWalletConnected() {
cy.getByTestId(this.connectVegaBtn).should('contain.text', '…');
}
selectPublicKey() {
cy.getByTestId(this.selectPublicKeyBtn).click();
}

View File

@ -2,9 +2,38 @@ import { useRouter } from 'next/router';
import { Vega } from '../icons/vega';
import Link from 'next/link';
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 = () => {
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 (
<nav className="flex items-center">
<Link href="/" passHref={true}>
@ -12,10 +41,7 @@ export const Navbar = () => {
<Vega className="fill-black dark:fill-white" />
</a>
</Link>
{[
{ name: t('Trading'), path: '/markets' },
{ name: t('Portfolio'), path: '/portfolio' },
].map((route) => (
{navItems.map((route) => (
<NavLink key={route.path} {...route} />
))}
</nav>
@ -26,20 +52,30 @@ interface NavLinkProps {
name: string;
path: string;
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 isActive =
router.asPath === path || (!exact && router.asPath.startsWith(path));
const href = slug !== '' ? `${path}/${slug}` : path;
return (
<AnchorButton
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"
href={path}
data-testid={testId}
href={href}
onClick={(e) => {
e.preventDefault();
router.push(path);
router.push(href);
}}
>
{name}

View File

@ -1,4 +1,5 @@
import { gql, useQuery } from '@apollo/client';
import { LocalStorage } from '@vegaprotocol/react-helpers';
import { MarketTradingMode } from '@vegaprotocol/types';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
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).
const { data, error, loading } = useQuery<MarketsLanding>(MARKETS_QUERY);
const setLandingDialog = useGlobalStore((state) => state.setLandingDialog);
const lastSelectedMarketId = LocalStorage.getItem('marketId');
useEffect(() => {
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 (marketId) {
@ -50,7 +54,7 @@ export function Index() {
replace('/markets');
}
}
}, [data, replace, setLandingDialog]);
}, [data, lastSelectedMarketId, replace, setLandingDialog]);
return (
<AsyncRenderer data={data} loading={loading} error={error}>

View File

@ -1,21 +1,55 @@
import { gql } from '@apollo/client';
import type { Market, MarketVariables } from './__generated__/Market';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import debounce from 'lodash/debounce';
import { PageQueryContainer } from '../../components/page-query-container';
import { TradeGrid, TradePanels } from './trade-grid';
import { t } from '@vegaprotocol/react-helpers';
import { LocalStorage, t } from '@vegaprotocol/react-helpers';
import { useGlobalStore } from '../../stores';
import { LandingDialog } from '@vegaprotocol/market-list';
import type { Market, MarketVariables } from './__generated__/Market';
import { Interval } from '@vegaprotocol/types';
// Top level page query
const MARKET_QUERY = gql`
query Market($marketId: ID!) {
query Market($marketId: ID!, $interval: Interval!, $since: String!) {
market(id: $marketId) {
id
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 =
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) {
return (
<Splash>
@ -37,12 +74,15 @@ const MarketPage = ({ id }: { id?: string }) => {
);
}
LocalStorage.setItem('marketId', marketId);
return (
<PageQueryContainer<Market, MarketVariables>
query={MARKET_QUERY}
options={{
variables: {
marketId,
interval: Interval.I1H,
since: yTimestamp,
},
fetchPolicy: 'network-only',
}}

View File

@ -3,10 +3,112 @@
// @generated
// This file was automatically generated and should not be edited.
import { Interval, MarketTradingMode, MarketState } from "@vegaprotocol/types";
// ====================================================
// 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 {
__typename: "Market";
/**
@ -17,6 +119,47 @@ export interface Market_market {
* Market full name
*/
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 {
@ -28,4 +171,6 @@ export interface Market {
export interface MarketVariables {
marketId: string;
interval: Interval;
since: string;
}

View File

@ -1,8 +1,6 @@
import { MarketsContainer } from '@vegaprotocol/market-list';
const Markets = () => {
return <MarketsContainer />;
};
const Markets = () => <MarketsContainer />;
Markets.getInitialProps = () => ({
page: 'markets',

View File

@ -13,6 +13,9 @@ import { t } from '@vegaprotocol/react-helpers';
import { AccountsContainer } from '@vegaprotocol/accounts';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
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 = {
Candles: CandlesChartContainer,
@ -31,6 +34,58 @@ interface TradeGridProps {
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) => {
const wrapperClasses = classNames(
'h-full max-h-full',
@ -38,50 +93,49 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
'bg-black-10 dark:bg-white-10',
'text-ui'
);
return (
<div className={wrapperClasses}>
<header className="col-start-1 col-end-2 row-start-1 row-end-1 p-8">
<h1>
{t('Market')}: {market.name}
</h1>
</header>
<TradeGridChild className="col-start-1 col-end-2">
<GridTabs group="chart">
<GridTab id="candles" name={t('Candles')}>
<TradingViews.Candles marketId={market.id} />
</GridTab>
<GridTab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={market.id} />
</GridTab>
</GridTabs>
</TradeGridChild>
<TradeGridChild className="row-start-1 row-end-3">
<TradingViews.Ticket marketId={market.id} />
</TradeGridChild>
<TradeGridChild className="row-start-1 row-end-3">
<GridTabs group="trade">
<GridTab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={market.id} />
</GridTab>
<GridTab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={market.id} />
</GridTab>
</GridTabs>
</TradeGridChild>
<TradeGridChild className="col-span-3">
<GridTabs group="portfolio">
<GridTab id="orders" name={t('Orders')}>
<TradingViews.Orders />
</GridTab>
<GridTab id="positions" name={t('Positions')}>
<TradingViews.Positions />
</GridTab>
<GridTab id="accounts" name={t('Accounts')}>
<TradingViews.Accounts />
</GridTab>
</GridTabs>
</TradeGridChild>
</div>
<>
<TradeMarketHeader market={market} />
<div className={wrapperClasses}>
<TradeGridChild className="row-start-1 row-end-3">
<GridTabs group="chart">
<GridTab id="candles" name={t('Candles')}>
<TradingViews.Candles marketId={market.id} />
</GridTab>
<GridTab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={market.id} />
</GridTab>
</GridTabs>
</TradeGridChild>
<TradeGridChild className="row-start-1 row-end-3">
<TradingViews.Ticket marketId={market.id} />
</TradeGridChild>
<TradeGridChild className="row-start-1 row-end-3">
<GridTabs group="trade">
<GridTab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={market.id} />
</GridTab>
<GridTab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={market.id} />
</GridTab>
</GridTabs>
</TradeGridChild>
<TradeGridChild className="col-span-3">
<GridTabs group="portfolio">
<GridTab id="orders" name={t('Orders')}>
<TradingViews.Orders />
</GridTab>
<GridTab id="positions" name={t('Positions')}>
<TradingViews.Positions />
</GridTab>
<GridTab id="accounts" name={t('Accounts')}>
<TradingViews.Accounts />
</GridTab>
</GridTabs>
</TradeGridChild>
</div>
</>
);
};
@ -124,11 +178,7 @@ export const TradePanels = ({ market }: TradePanelsProps) => {
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<header className="p-8">
<h1>
{t('Market')}: {market.name}
</h1>
</header>
<TradeMarketHeader market={market} />
<div className="h-full">
<AutoSizer>
{({ width, height }) => (

View File

@ -1,2 +1,3 @@
export * from './landing-dialog';
export * from './select-market-dialog';
export * from './select-market-list';

View File

@ -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();
});
});

View File

@ -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>
);
};

View File

@ -3,19 +3,21 @@ import {
PriceCell,
t,
} from '@vegaprotocol/react-helpers';
import type { CandleClose } from '@vegaprotocol/types';
import { PriceCellChange, Sparkline } from '@vegaprotocol/ui-toolkit';
import Link from 'next/link';
import { mapDataToMarketList } from '../../utils';
import type { MarketList } from '../markets-container/__generated__/MarketList';
export interface SelectMarketListProps {
export interface SelectMarketListDataProps {
data: MarketList | undefined;
onSelect: (id: string) => void;
}
type CandleClose = Required<string>;
export const SelectMarketList = ({ data, onSelect }: SelectMarketListProps) => {
export const SelectMarketList = ({
data,
onSelect,
}: SelectMarketListDataProps) => {
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`;
const tdClassNames =

View File

@ -13,7 +13,7 @@ import type {
import { marketsDataProvider } from './markets-data-provider';
export const MarketsContainer = () => {
const { pathname, push } = useRouter();
const { push } = useRouter();
const gridRef = useRef<AgGridReact | null>(null);
const update = useCallback(
(delta: Markets_markets_data) => {
@ -57,7 +57,7 @@ export const MarketsContainer = () => {
ref={gridRef}
data={data}
onRowClicked={(id) =>
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
push(`/markets/${id}?portfolio=orders&trade=orderbook`)
}
/>
</AsyncRenderer>

View File

@ -4,7 +4,7 @@ import type { Positions_party_positions } from './__generated__/Positions';
import { MarketTradingMode } from '@vegaprotocol/types';
const singleRow: Positions_party_positions = {
realisedPNL: '5',
realisedPNL: '520000000',
openVolume: '100',
unrealisedPNL: '895000',
averageEntryPrice: '1129935',
@ -93,7 +93,7 @@ it('Correct formatting applied', async () => {
'+100',
'11.29935',
'11.38885',
'+5',
'+5,200.000',
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);

View File

@ -131,8 +131,10 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
'color-vega-red': ({ value }: { value: string }) =>
Number(value) < 0,
}}
valueFormatter={({ value }: ValueFormatterParams) =>
volumePrefix(value)
valueFormatter={({ value, data }: ValueFormatterParams) =>
volumePrefix(
addDecimalsFormatNumber(value, data.market.decimalPlaces, 3)
)
}
cellRenderer="PriceFlashCell"
/>

View File

@ -46,8 +46,8 @@ interface GetDelta<SubscriptionData, Delta> {
}
/**
* @param subscriptionQuery query that will beused 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 subscriptionQuery query that will be used for subscription
* @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 getDelta transforms delta data to format that will be stored in data provider
* @param fetchPolicy
@ -63,7 +63,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
): Subscribe<Data, Delta> {
// list of callbacks passed through subscribe call
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[] = [];
let variables: OperationVariables | undefined = undefined;
@ -88,7 +88,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
callbacks.forEach((callback) => notify(callback, delta));
};
const initalFetch = async () => {
const initialFetch = async () => {
if (!client) {
return;
}
@ -99,7 +99,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
fetchPolicy,
});
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) {
data = produce(data, (draft) => {
while (updateQueue.length) {
@ -135,7 +135,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
} else {
loading = true;
error = undefined;
initalFetch();
initialFetch();
}
};
@ -176,7 +176,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
},
() => restart()
);
await initalFetch();
await initialFetch();
};
const reset = () => {
@ -242,8 +242,8 @@ const memoize = <Data, Delta>(
/**
* @param query Query<QueryData>
* @param subscriptionQuery Query<SubscriptionData> query that will beused 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 subscriptionQuery Query<SubscriptionData> query that will be used for subscription
* @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 getDelta transforms delta data to format that will be stored in data provider
* @param fetchPolicy

View File

@ -13,7 +13,7 @@ export const PriceCell = React.memo(
return <span data-testid="price">-</span>;
}
return (
<span className="font-mono relative" data-testid="price">
<span className="font-mono relative text-ui-small" data-testid="price">
{valueFormatted}
</span>
);

1
libs/types/src/candle.ts Normal file
View File

@ -0,0 +1 @@
export type CandleClose = Required<string>;

View File

@ -1 +1,2 @@
export * from './__generated__/globalTypes';
export * from './candle';

View File

@ -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
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>
);
export const ArrowDown = () => (
export const ArrowDown = ({
color = 'red',
borderX = 4,
borderTop = 4,
}: ArrowStyleProps) => (
<span
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>
);

View File

@ -215,12 +215,12 @@ export const AnchorButton = forwardRef<HTMLAnchorElement, AnchorButtonProps>(
className,
prependIconName,
appendIconName,
...prosp
...props
},
ref
) => {
return (
<a ref={ref} className={classes(className, variant)} {...prosp}>
<a ref={ref} className={classes(className, variant)} {...props}>
{getContent(children, prependIconName, appendIconName)}
</a>
);

View File

@ -12,6 +12,7 @@ interface DialogProps {
title?: string;
intent?: Intent;
titleClassNames?: string;
contentClassNames?: string;
}
export function Dialog({
@ -21,13 +22,15 @@ export function Dialog({
title,
intent,
titleClassNames,
contentClassNames,
}: DialogProps) {
const contentClasses = classNames(
// 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%]',
// 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',
getIntentShadow(intent)
getIntentShadow(intent),
contentClassNames
);
return (
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange(x)}>