feat(trading): market selection changes (#3863)
This commit is contained in:
parent
34526d527e
commit
5280b79927
@ -1,20 +1,16 @@
|
||||
import { closeWelcomeDialog } from '../support/helpers';
|
||||
|
||||
const dialogContent = 'dialog-content';
|
||||
const nodeHealth = 'node-health';
|
||||
|
||||
describe('home', { tags: '@regression' }, () => {
|
||||
describe.skip('home', { tags: '@regression' }, () => {
|
||||
before(() => {
|
||||
cy.clearAllLocalStorage();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
closeWelcomeDialog();
|
||||
});
|
||||
|
||||
describe('footer', () => {
|
||||
it.skip('shows current block height', () => {
|
||||
closeWelcomeDialog();
|
||||
it('shows current block height', () => {
|
||||
// 0006-NETW-004
|
||||
// 0006-NETW-005
|
||||
// 0006-NETW-008
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { marketsDataQuery } from '@vegaprotocol/mock';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const selectMarketOverlay = 'select-market-list';
|
||||
const dialogContent = 'dialog-content';
|
||||
|
||||
const generateProposal = (code: string): ProposalListFieldsFragment => ({
|
||||
@ -110,77 +108,12 @@ describe('home', { tags: '@regression' }, () => {
|
||||
|
||||
cy.get('main[data-testid^="/markets/"]');
|
||||
|
||||
// Overlay should be shown
|
||||
cy.getByTestId(selectMarketOverlay).should('exist');
|
||||
cy.contains('Select a market to get started').should('be.visible');
|
||||
|
||||
// I expect the market overlay table to contain at least 3 rows (one header row)
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table tr')
|
||||
.then((row) => {
|
||||
expect(row.length >= 3).to.be.true;
|
||||
});
|
||||
|
||||
// each market shown in overlay table contains content under the last price and change fields
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table tr')
|
||||
.each(($element, index) => {
|
||||
if (index > 0) {
|
||||
// skip header row
|
||||
cy.root().within(() => {
|
||||
cy.getByTestId('price').should('not.be.empty');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
cy.getByTestId('welcome-notice-proposed-markets')
|
||||
.children('div.pt-1.flex.justify-between')
|
||||
.should('have.length', 3)
|
||||
.each((item) => {
|
||||
cy.wrap(item).getByTestId('external-link').should('exist');
|
||||
});
|
||||
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.getByTestId(selectMarketOverlay).should('not.exist');
|
||||
|
||||
// the choose market overlay is no longer showing
|
||||
cy.contains('Select a market to get started').should('not.exist');
|
||||
cy.contains('Loading...').should('not.exist');
|
||||
cy.url().should('eq', Cypress.config().baseUrl + '/#/markets/market-0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('market table should be properly rendered', () => {
|
||||
it('redirects to a default market with the landing dialog open', () => {
|
||||
const override = {
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
data: {
|
||||
markPrice: '46126900581221212121212121212121212121212121212',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
// @ts-ignore partial deep check failing
|
||||
const data = marketsDataQuery(override);
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'MarketsData', data);
|
||||
});
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table')
|
||||
.invoke('outerWidth')
|
||||
.then((value) => {
|
||||
expect(value).to.be.closeTo(554, 10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no markets found', () => {
|
||||
beforeEach(() => {
|
||||
cy.mockGQL((req) => {
|
||||
@ -206,40 +139,13 @@ describe('home', { tags: '@regression' }, () => {
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
});
|
||||
|
||||
it('redirects to a the empty market page and displays welcome notice', () => {
|
||||
cy.url().should('eq', Cypress.config().baseUrl + `/#/markets`);
|
||||
cy.getByTestId('welcome-notice-title').should(
|
||||
'contain.text',
|
||||
'Welcome to Console'
|
||||
);
|
||||
cy.getByTestId('welcome-notice-proposed-markets').should(
|
||||
'contain.text',
|
||||
'AAAZZZ'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('no proposal found', () => {
|
||||
it('there is a link to propose market', () => {
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'ProposalsList', {
|
||||
proposalsConnection: {
|
||||
__typename: 'ProposalsConnection',
|
||||
edges: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table tr')
|
||||
.then((row) => {
|
||||
expect(row.length >= 3).to.be.true;
|
||||
});
|
||||
cy.getByTestId('external-link')
|
||||
.contains('Propose a market')
|
||||
.should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
const selectMarketOverlay = 'select-market-list';
|
||||
const marketInfoBtn = 'Info';
|
||||
const marketInfoSubtitle = 'accordion-title';
|
||||
const marketSummaryBlock = 'header-summary';
|
||||
@ -14,45 +13,6 @@ const itemHeader = 'item-header';
|
||||
const itemValue = 'item-value';
|
||||
const marketListContent = 'popover-content';
|
||||
|
||||
describe(
|
||||
'Console - market list - live env',
|
||||
{ tags: '@live', testIsolation: true },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('shows the market list page', () => {
|
||||
cy.get('main', { timeout: 20000 });
|
||||
|
||||
// Overlay should be shown
|
||||
cy.getByTestId(selectMarketOverlay).should('exist');
|
||||
cy.contains('Select a market to get started').should('be.visible');
|
||||
|
||||
// I expect the market overlay table to contain at least one row
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table tr')
|
||||
.should('have.length.greaterThan', 1);
|
||||
|
||||
// each market shown in overlay table contains content under the last price and change fields
|
||||
cy.getByTestId(selectMarketOverlay)
|
||||
.get('table tr')
|
||||
.getByTestId('price')
|
||||
.should('not.be.empty');
|
||||
});
|
||||
|
||||
it('redirects to a default market', () => {
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.getByTestId(selectMarketOverlay).should('not.exist');
|
||||
|
||||
// the choose market overlay is no longer showing
|
||||
cy.contains('Select a market to get started').should('not.exist');
|
||||
cy.contains('Loading...').should('not.exist');
|
||||
cy.getByTestId('popover-trigger').should('not.be.empty');
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe(
|
||||
'Console - market info - live env',
|
||||
{ tags: '@live', testIsolation: true },
|
||||
|
@ -38,28 +38,28 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
// TODO: load data from mocks in. Using alias and wrap intermittently fails
|
||||
const data = [
|
||||
{
|
||||
code: 'AAPL.MF21',
|
||||
name: 'Apple Monthly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'BTCUSD.MF21',
|
||||
name: 'ACTIVE MARKET',
|
||||
markPrice: '46,126.90058',
|
||||
code: 'SOLUSD',
|
||||
markPrice: '84.41XYZalpha',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
},
|
||||
{
|
||||
code: 'ETHBTC.QM21',
|
||||
name: 'ETHBTC Quarterly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
markPrice: '46,126.90058tBTC',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
},
|
||||
{
|
||||
code: 'SOLUSD',
|
||||
name: 'SUSPENDED MARKET',
|
||||
markPrice: '84.41',
|
||||
code: 'BTCUSD.MF21',
|
||||
markPrice: '46,126.90058tDAI',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
},
|
||||
{
|
||||
code: 'AAPL.MF21',
|
||||
markPrice: '46,126.90058tUSDC',
|
||||
change: '+200.00%',
|
||||
vol: '324h vol',
|
||||
},
|
||||
];
|
||||
cy.getByTestId(list)
|
||||
@ -68,12 +68,13 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
const market = data[i];
|
||||
// 6001-MARK-021
|
||||
expect(item.find('h3').text()).equals(market.code);
|
||||
// 6001-MARK-022
|
||||
expect(item.find('h4').text()).equals(market.name);
|
||||
expect(
|
||||
item.find('[data-testid="market-selector-data-row"]').eq(0).text()
|
||||
).contains(market.vol);
|
||||
// 6001-MARK-024
|
||||
expect(item.find('[data-testid="market-item-price"]').text()).equals(
|
||||
market.markPrice
|
||||
);
|
||||
expect(
|
||||
item.find('[data-testid="market-selector-data-row"]').eq(1).text()
|
||||
).contains(market.markPrice);
|
||||
// 6001-MARK-023
|
||||
expect(item.find('[data-testid="market-item-change"]').text()).equals(
|
||||
market.change
|
||||
@ -96,8 +97,8 @@ describe('markets selector', { tags: '@smoke' }, () => {
|
||||
// 6001-MARK-29
|
||||
cy.getByTestId(searchInput).clear().type('btc');
|
||||
cy.getByTestId(list).find('a').should('have.length', 2);
|
||||
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
|
||||
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
|
||||
cy.getByTestId(list).find('a').eq(1).contains('BTCUSD.MF21');
|
||||
cy.getByTestId(list).find('a').eq(0).contains('ETHBTC.QM21');
|
||||
|
||||
cy.getByTestId(searchInput).clear();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
|
@ -3,8 +3,6 @@ import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
|
||||
import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
|
||||
const dialogCloseBtn = 'dialog-close';
|
||||
|
||||
describe('markets table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage().then(() => {
|
||||
@ -14,20 +12,18 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.wait('@MarketsCandles');
|
||||
cy.visit('/#/markets/all');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders markets correctly', () => {
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId('price').invoke('text').should('not.be.empty');
|
||||
cy.getByTestId('settlement-asset').should('not.be.empty');
|
||||
cy.getByTestId('price-change-percentage').should('not.be.empty');
|
||||
cy.getByTestId('price-change').should('not.be.empty');
|
||||
cy.getByTestId('sparkline-svg').should('be.visible');
|
||||
});
|
||||
|
||||
it('able to open and sort full market list - market page', () => {
|
||||
@ -37,9 +33,6 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
'ETHBTC.QM21',
|
||||
'SOLUSD',
|
||||
];
|
||||
cy.getByTestId('view-market-list-link')
|
||||
.should('have.attr', 'href', '#/markets/all')
|
||||
.click();
|
||||
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
|
||||
@ -51,10 +44,6 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
it('proposed markets tab should be rendered properly', () => {
|
||||
cy.getByTestId('view-market-list-link')
|
||||
.should('have.attr', 'href', '#/markets/all')
|
||||
.click();
|
||||
|
||||
cy.get('[data-testid="All markets"]').should(
|
||||
'have.attr',
|
||||
'data-state',
|
||||
@ -87,8 +76,8 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
|
||||
);
|
||||
});
|
||||
|
||||
it('proposed markets tab should be sorted properly', () => {
|
||||
cy.getByTestId('view-market-list-link').click();
|
||||
cy.get('[data-testid="Proposed markets"]').click();
|
||||
const marketColDefault = [
|
||||
'ETHUSD',
|
||||
@ -167,7 +156,7 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
|
||||
});
|
||||
|
||||
it('opening auction subsets should be properly displayed', () => {
|
||||
it.skip('opening auction subsets should be properly displayed', () => {
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||
@ -200,7 +189,6 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
cy.visit('#/markets/market-0');
|
||||
cy.url().should('contain', 'market-0');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
||||
cy.getByTestId('opening-auction-sub-status').should(
|
||||
'contain.text',
|
||||
|
@ -8,9 +8,6 @@ describe('Navbar', { tags: '@smoke' }, () => {
|
||||
cy.visit('/');
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.wait('@MarketsCandles');
|
||||
// close welcome dialog
|
||||
cy.getByTestId('dialog-close').click();
|
||||
});
|
||||
|
||||
const pages = [
|
||||
|
@ -1,12 +1,9 @@
|
||||
import { closeWelcomeDialog } from '../support/helpers';
|
||||
|
||||
describe('Settings page', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.clearLocalStorage().then(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
closeWelcomeDialog();
|
||||
cy.get('[aria-label="cog icon"]').click();
|
||||
});
|
||||
});
|
||||
|
@ -9,8 +9,3 @@ export const selectAsset = (assetIndex: number) => {
|
||||
// eslint-disable-next-line
|
||||
cy.wait(100);
|
||||
};
|
||||
|
||||
export const closeWelcomeDialog = () => {
|
||||
cy.getByTestId('select-market-list').should('exist');
|
||||
cy.getByTestId('dialog-close').click();
|
||||
};
|
||||
|
@ -85,7 +85,7 @@ const mockTradingPage = (
|
||||
trigger?: Schema.AuctionTrigger
|
||||
) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'NodeCheck', statisticsQuery());
|
||||
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
|
@ -9,7 +9,7 @@ import { useGlobalStore } from '../../stores';
|
||||
export const Home = () => {
|
||||
const navigate = useNavigate();
|
||||
// The default market selected in the platform behind the overlay
|
||||
// should be the oldest market that is currently trading in us 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 } = useDataProvider({
|
||||
dataProvider: marketsWithDataProvider,
|
||||
variables: undefined,
|
||||
|
@ -22,7 +22,19 @@ describe('MarketSelectorItem', () => {
|
||||
id: 'market-0',
|
||||
decimalPlaces: 2,
|
||||
// @ts-ignore fragment doesn't contain candles
|
||||
candles: [{ close: '5' }, { close: '10' }],
|
||||
candles: [
|
||||
{ close: '5', volume: '50' },
|
||||
{ close: '10', volume: '50' },
|
||||
],
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
settlementAsset: {
|
||||
symbol: 'SYM',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const marketData: MarketDataUpdateFieldsFragment = {
|
||||
__typename: 'ObservableMarketData',
|
||||
@ -83,6 +95,9 @@ describe('MarketSelectorItem', () => {
|
||||
};
|
||||
|
||||
it('renders market information', async () => {
|
||||
const symbol =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||
|
||||
renderJsx();
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
@ -91,7 +106,8 @@ describe('MarketSelectorItem', () => {
|
||||
|
||||
expect(link).toHaveClass('ring-1');
|
||||
|
||||
expect(screen.getByTestId('market-item-price')).toHaveTextContent('-');
|
||||
expect(screen.getByTitle('24h vol')).toHaveTextContent('100');
|
||||
expect(screen.getByTitle(symbol)).toHaveTextContent('-');
|
||||
|
||||
// candles are loaded immediately
|
||||
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
|
||||
@ -99,7 +115,7 @@ describe('MarketSelectorItem', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('market-item-price')).toHaveTextContent(
|
||||
expect(screen.getByTitle(symbol)).toHaveTextContent(
|
||||
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
|
||||
);
|
||||
});
|
||||
|
@ -7,8 +7,14 @@ import {
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import { calcCandleVolume } from '@vegaprotocol/markets';
|
||||
import { useMarketDataUpdateSubscription } from '@vegaprotocol/markets';
|
||||
import { Sparkline } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
MarketTradingMode,
|
||||
MarketTradingModeMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const MarketSelectorItem = ({
|
||||
market,
|
||||
@ -38,13 +44,6 @@ export const MarketSelectorItem = ({
|
||||
onSelect && onSelect(market.id);
|
||||
}}
|
||||
>
|
||||
<h3>{market.tradableInstrument.instrument.code}</h3>
|
||||
<h4
|
||||
title={market.tradableInstrument.instrument.name}
|
||||
className="text-sm text-vega-light-300 dark:text-vega-dark-300 text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{market.tradableInstrument.instrument.name}
|
||||
</h4>
|
||||
<MarketData market={market} />
|
||||
</Link>
|
||||
</div>
|
||||
@ -68,30 +67,84 @@ const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
|
||||
? addDecimalsFormatNumber(market.data.markPrice, market.decimalPlaces)
|
||||
: '-';
|
||||
|
||||
const marketTradingMode = marketData
|
||||
? marketData.marketTradingMode
|
||||
: market.tradingMode;
|
||||
|
||||
const mode = [
|
||||
MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
].includes(marketTradingMode)
|
||||
? MarketTradingModeMapping[marketTradingMode]
|
||||
: '';
|
||||
|
||||
const instrument = market.tradableInstrument.instrument;
|
||||
|
||||
const vol = market.candles ? calcCandleVolume(market.candles) : '0';
|
||||
const volume =
|
||||
vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, market.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
|
||||
return (
|
||||
<div className="flex flex-nowrap justify-between items-center mt-1">
|
||||
<div className="w-1/2">
|
||||
<div
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
data-testid="market-item-price"
|
||||
<>
|
||||
<div className="flex items-end gap-1 mb-1">
|
||||
<h3
|
||||
className={classNames(
|
||||
'overflow-hidden text-ellipsis whitespace-nowrap',
|
||||
{
|
||||
'w-1/2': mode, // make space for showing the trading mode
|
||||
}
|
||||
)}
|
||||
>
|
||||
{price}
|
||||
</div>
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</h3>
|
||||
{mode && (
|
||||
<p className="w-1/2 text-xs text-right text-vega-orange-500 dark:text-vega-orange-550">
|
||||
{mode}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DataRow value={volume} label={t('24h vol')} />
|
||||
<DataRow
|
||||
value={price}
|
||||
label={instrument.product.settlementAsset.symbol}
|
||||
/>
|
||||
<div className="relative">
|
||||
{market.candles && (
|
||||
<PriceChange candles={market.candles.map((c) => c.close)} />
|
||||
)}
|
||||
|
||||
<div
|
||||
// absolute so height is not larger than price change value
|
||||
className="absolute right-0 bottom-0 w-[120px]"
|
||||
>
|
||||
{market.candles && (
|
||||
<Sparkline
|
||||
width={120}
|
||||
height={20}
|
||||
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 max-w-[120px]">
|
||||
{market.candles ? (
|
||||
<Sparkline
|
||||
width={120}
|
||||
height={20}
|
||||
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DataRow = ({ value, label }: { value: string; label: string }) => {
|
||||
return (
|
||||
<div
|
||||
className="text-ellipsis whitespace-nowrap overflow-hidden leading-tight"
|
||||
data-testid="market-selector-data-row"
|
||||
>
|
||||
<span title={label} className="text-sm mr-1">
|
||||
{value}
|
||||
</span>
|
||||
<span className="text-xs text-vega-light-300 dark:text-vega-light-300">
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,8 +2,8 @@ import classNames from 'classnames';
|
||||
|
||||
// Make sure these match the available __typename properties on product
|
||||
export const Product = {
|
||||
Spot: 'Spot',
|
||||
Future: 'Future',
|
||||
Spot: 'Spot',
|
||||
Perpetual: 'Perpetual',
|
||||
} as const;
|
||||
|
||||
@ -12,8 +12,8 @@ export type ProductType = keyof typeof Product;
|
||||
const ProductTypeMapping: {
|
||||
[key in ProductType]: string;
|
||||
} = {
|
||||
[Product.Spot]: 'Spot',
|
||||
[Product.Future]: 'Futures',
|
||||
[Product.Spot]: 'Spot',
|
||||
[Product.Perpetual]: 'Perpetuals',
|
||||
};
|
||||
|
||||
|
@ -339,24 +339,31 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div className="border-b border-r border-default">
|
||||
<div className="flex gap-2 justify-between items-center px-4 py-2">
|
||||
<div className="h-full flex gap-2 justify-between items-end px-4 pt-1 pb-3">
|
||||
<HeaderTitle
|
||||
primaryContent={market?.tradableInstrument.instrument.code}
|
||||
secondaryContent={market?.tradableInstrument.instrument.name}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setSidebarOpen((x) => !x)}
|
||||
className="p-2"
|
||||
className="flex flex-col items-center text-xs w-12"
|
||||
data-testid="sidebar-toggle"
|
||||
>
|
||||
<span
|
||||
className={classNames('block', {
|
||||
'rotate-90 translate-x-1': !sidebarOpen,
|
||||
'-rotate-90 -translate-x-1': sidebarOpen,
|
||||
})}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
|
||||
</span>
|
||||
{sidebarOpen ? (
|
||||
<>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
|
||||
<span className="text-vega-light-300 dark:text-vega-dark-300">
|
||||
{t('Close')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} />
|
||||
<span className="text-vega-light-300 dark:text-vega-dark-300">
|
||||
{t('Markets')}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -315,39 +315,47 @@ describe('useMarketSelectorList', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts albphabetically', () => {
|
||||
it('sorts by state and volume by default', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'd',
|
||||
state: MarketState.STATE_PENDING,
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '200',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
createMarketFragment({
|
||||
state: MarketState.STATE_PENDING,
|
||||
id: 'market-3',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
volume: '100',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@ -364,10 +372,10 @@ describe('useMarketSelectorList', () => {
|
||||
assets: [],
|
||||
});
|
||||
expect(result.current.markets).toEqual([
|
||||
markets[2],
|
||||
markets[1],
|
||||
markets[3],
|
||||
markets[2],
|
||||
markets[0],
|
||||
markets[3],
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { useMemo } from 'react';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { calcCandleVolume, useMarketList } from '@vegaprotocol/markets';
|
||||
import { priceChangePercentage } from '@vegaprotocol/utils';
|
||||
import type { Filter } from './market-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
|
||||
// Used for sort order and filter
|
||||
const MARKET_TEMPLATE = [
|
||||
MarketState.STATE_ACTIVE,
|
||||
MarketState.STATE_SUSPENDED,
|
||||
MarketState.STATE_PENDING,
|
||||
];
|
||||
|
||||
export const useMarketSelectorList = ({
|
||||
product,
|
||||
assets,
|
||||
@ -46,7 +53,19 @@ export const useMarketSelectorList = ({
|
||||
});
|
||||
|
||||
if (sort === Sort.None) {
|
||||
return orderBy(markets, ['tradableInstrument.instrument.code'], ['asc']);
|
||||
// Sort by market state primarilly and AtoZ secondarilly
|
||||
return orderBy(
|
||||
markets,
|
||||
[
|
||||
(m) => MARKET_TEMPLATE.indexOf(m.state),
|
||||
(m) => {
|
||||
if (!m.candles?.length) return 0;
|
||||
const vol = calcCandleVolume(m.candles);
|
||||
return Number(vol || 0);
|
||||
},
|
||||
],
|
||||
['asc', 'desc']
|
||||
);
|
||||
}
|
||||
|
||||
if (sort === Sort.Gained || sort === Sort.Lost) {
|
||||
@ -78,9 +97,5 @@ export const useMarketSelectorList = ({
|
||||
};
|
||||
|
||||
export const isMarketActive = (state: MarketState) => {
|
||||
return [
|
||||
MarketState.STATE_ACTIVE,
|
||||
MarketState.STATE_SUSPENDED,
|
||||
MarketState.STATE_PENDING,
|
||||
].includes(state);
|
||||
return MARKET_TEMPLATE.includes(state);
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const THROTTLE_UPDATE_TIME = 500;
|
||||
export const RISK_ACCEPTED_KEY = 'vega_risk_accepted';
|
||||
export const MAINNET_WELCOME_HEADER = t(
|
||||
|
@ -73,10 +73,10 @@ export const HeaderTitle = ({
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-left" data-testid="header-title">
|
||||
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
|
||||
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap !leading-[1]">
|
||||
{primaryContent}
|
||||
</div>
|
||||
<div className="text-xs whitespace-nowrap text-neutral-500 dark:text-neutral-400">
|
||||
<div className="text-xs whitespace-nowrap text-vega-light-300 dark:text-vega-dark-300">
|
||||
{secondaryContent}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,2 +0,0 @@
|
||||
export * from './select-market-columns';
|
||||
export * from './select-market-table';
|
@ -1,344 +0,0 @@
|
||||
import type { RefObject, MouseEvent } from 'react';
|
||||
import {
|
||||
FeesCell,
|
||||
calcCandleHigh,
|
||||
calcCandleLow,
|
||||
calcCandleVolume,
|
||||
Last24hPriceChange,
|
||||
Last24hVolume,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { PriceCell } from '@vegaprotocol/datagrid';
|
||||
import { Link as UILink, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import isNil from 'lodash/isNil';
|
||||
import type { CandleClose } from '@vegaprotocol/types';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MarketMarkPrice } from '../market-mark-price';
|
||||
import { MarketTradingMode } from '../market-trading-mode';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
|
||||
const ellipsisClasses = 'whitespace-nowrap overflow-hidden text-ellipsis';
|
||||
export const cellClassNames = `py-1 first:text-left text-right ${ellipsisClasses}`;
|
||||
|
||||
const FeesInfo = () => {
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<span>
|
||||
{t(
|
||||
'Fees are paid by market takers on aggressive orders only. The fee displayed is made up of:'
|
||||
)}
|
||||
<ul className="list-disc ml-4">
|
||||
<li>{t('An infrastructure fee')}</li>
|
||||
<li>{t('A liquidity provision fee')}</li>
|
||||
<li>{t('A maker fee')}</li>
|
||||
</ul>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span>{t('Taker fee')}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export enum ColumnKind {
|
||||
Market,
|
||||
LastPrice,
|
||||
Change24,
|
||||
Asset,
|
||||
ProductType,
|
||||
Sparkline,
|
||||
High24,
|
||||
Low24,
|
||||
TradingMode,
|
||||
Volume,
|
||||
Fee,
|
||||
Position,
|
||||
FullName,
|
||||
}
|
||||
|
||||
export interface Column {
|
||||
kind: ColumnKind;
|
||||
value: string | React.ReactNode;
|
||||
className: string;
|
||||
onlyOnDetailed: boolean;
|
||||
dataTestId?: string;
|
||||
}
|
||||
|
||||
const headers: Column[] = [
|
||||
{
|
||||
kind: ColumnKind.Market,
|
||||
value: t('Market'),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.ProductType,
|
||||
value: t('Type'),
|
||||
className: 'py-2 text-left hidden sm:table-cell',
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.LastPrice,
|
||||
value: t('Last price'),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Change24,
|
||||
value: t('Change (24h)'),
|
||||
className: cellClassNames,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Sparkline,
|
||||
value: t(''),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Asset,
|
||||
value: t('Settlement asset'),
|
||||
className: `${cellClassNames} hidden sm:table-cell`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.High24,
|
||||
value: t('24h High'),
|
||||
className: `${cellClassNames} hidden xl:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Low24,
|
||||
value: t('24h Low'),
|
||||
className: `${cellClassNames} hidden xl:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Volume,
|
||||
value: t('24h Volume'),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.TradingMode,
|
||||
value: t('Trading mode'),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Fee,
|
||||
value: <FeesInfo />,
|
||||
className: `${cellClassNames} hidden xl:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const columnHeadersPositionMarkets: Column[] = [
|
||||
...headers,
|
||||
{
|
||||
kind: ColumnKind.Position,
|
||||
value: t('Position'),
|
||||
className: `${cellClassNames} hidden xxl:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const columnHeaders: Column[] = [
|
||||
...headers,
|
||||
{
|
||||
kind: ColumnKind.FullName,
|
||||
value: t('Full name'),
|
||||
className: `${cellClassNames} hidden xxl:block`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
];
|
||||
|
||||
export type OnCellClickHandler = (
|
||||
e: MouseEvent,
|
||||
kind: ColumnKind,
|
||||
value: string
|
||||
) => void;
|
||||
|
||||
export const columns = (
|
||||
market: MarketMaybeWithDataAndCandles,
|
||||
onSelect: (id: string, metaKey?: boolean) => void,
|
||||
onCellClick: OnCellClickHandler,
|
||||
inViewRoot?: RefObject<HTMLElement>
|
||||
) => {
|
||||
const candlesClose = market.candles
|
||||
?.map((candle) => candle?.close)
|
||||
.filter((c: string | undefined): c is CandleClose => !isNil(c));
|
||||
const candleLow = market.candles && calcCandleLow(market.candles);
|
||||
const candleHigh = market.candles && calcCandleHigh(market.candles);
|
||||
const candleVolume = market.candles && calcCandleVolume(market.candles);
|
||||
|
||||
const selectMarketColumns: Column[] = [
|
||||
{
|
||||
kind: ColumnKind.Market,
|
||||
value: (
|
||||
<Link
|
||||
to={Links[Routes.MARKET](market.id)}
|
||||
data-testid={`market-link-${market.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(market.id, e.metaKey || e.ctrlKey);
|
||||
}}
|
||||
>
|
||||
<UILink>{market.tradableInstrument.instrument.code}</UILink>
|
||||
</Link>
|
||||
),
|
||||
className: `${cellClassNames} max-w-[110px]`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.ProductType,
|
||||
value: market.tradableInstrument.instrument.product.__typename,
|
||||
className: `py-2 text-left hidden sm:table-cell max-w-[50px] ${ellipsisClasses}`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.LastPrice,
|
||||
value: (
|
||||
<MarketMarkPrice
|
||||
marketId={market.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
initialValue={market.data?.markPrice}
|
||||
inViewRoot={inViewRoot}
|
||||
asPriceCell
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} max-w-[100px]`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Change24,
|
||||
value: (
|
||||
<Last24hPriceChange
|
||||
marketId={market.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
inViewRoot={inViewRoot}
|
||||
initialValue={candlesClose}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} max-w-[150px]`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Sparkline,
|
||||
value: market.candles && (
|
||||
<Sparkline
|
||||
width={100}
|
||||
height={20}
|
||||
muted={false}
|
||||
data={candlesClose?.map((c: string) => Number(c)) || []}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell max-w-[80px]`,
|
||||
onlyOnDetailed: false && candlesClose,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Asset,
|
||||
value: (
|
||||
<button
|
||||
data-dialog-trigger
|
||||
className="inline underline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCellClick(
|
||||
e,
|
||||
ColumnKind.Asset,
|
||||
market.tradableInstrument.instrument.product.settlementAsset.id
|
||||
);
|
||||
}}
|
||||
>
|
||||
{market.tradableInstrument.instrument.product.settlementAsset.symbol}
|
||||
</button>
|
||||
),
|
||||
dataTestId: 'settlement-asset',
|
||||
className: `${cellClassNames} hidden sm:table-cell max-w-[100px]`,
|
||||
onlyOnDetailed: false,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.High24,
|
||||
value: candleHigh ? (
|
||||
<PriceCell
|
||||
value={Number(candleHigh)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
candleHigh.toString(),
|
||||
market.decimalPlaces,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Low24,
|
||||
value: candleLow ? (
|
||||
<PriceCell
|
||||
value={Number(candleLow)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
candleLow.toString(),
|
||||
market.decimalPlaces,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Volume,
|
||||
value: (
|
||||
<Last24hVolume
|
||||
marketId={market.id}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
initialValue={candleVolume}
|
||||
inViewRoot={inViewRoot}
|
||||
formatDecimals={2}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'market-volume',
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.TradingMode,
|
||||
value: (
|
||||
<MarketTradingMode
|
||||
marketId={market?.id}
|
||||
inViewRoot={inViewRoot}
|
||||
initialTradingMode={market.tradingMode}
|
||||
initialTrigger={market.data?.trigger}
|
||||
/>
|
||||
),
|
||||
className: `${cellClassNames} hidden lg:table-cell`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'trading-mode-col',
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.Fee,
|
||||
value: <FeesCell feeFactors={market.fees.factors} />,
|
||||
className: `${cellClassNames} hidden xl:table-cell font-mono`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'taker-fee',
|
||||
},
|
||||
{
|
||||
kind: ColumnKind.FullName,
|
||||
value: market.tradableInstrument.instrument.name,
|
||||
className: `${cellClassNames} hidden xxl:block`,
|
||||
onlyOnDetailed: true,
|
||||
dataTestId: 'market-name',
|
||||
},
|
||||
];
|
||||
return selectMarketColumns;
|
||||
};
|
@ -1,63 +0,0 @@
|
||||
import { columnHeaders } from './select-market-columns';
|
||||
import classNames from 'classnames';
|
||||
import type { Column } from './select-market-columns';
|
||||
|
||||
export const SelectMarketTableHeader = ({
|
||||
detailed = false,
|
||||
headers = columnHeaders,
|
||||
}) => {
|
||||
return (
|
||||
<tr className="sticky top-0 z-10 border-b border-default bg-inherit">
|
||||
{headers.map(({ kind, value, className, onlyOnDetailed }) => {
|
||||
const thClass = classNames(
|
||||
'font-normal text-neutral-500 dark:text-neutral-400',
|
||||
className
|
||||
);
|
||||
|
||||
if (!onlyOnDetailed || detailed === onlyOnDetailed) {
|
||||
return (
|
||||
<th key={kind} className={thClass}>
|
||||
{value}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export const SelectMarketTableRow = ({
|
||||
detailed = false,
|
||||
columns,
|
||||
onSelect,
|
||||
marketId,
|
||||
}: {
|
||||
detailed?: boolean;
|
||||
columns: Column[];
|
||||
onSelect: (id: string, metaKey?: boolean) => void;
|
||||
marketId: string;
|
||||
}) => {
|
||||
return (
|
||||
<tr
|
||||
className={`hover:bg-neutral-200 dark:hover:bg-neutral-700 cursor-pointer relative h-[34px]`}
|
||||
onClick={(ev) => {
|
||||
onSelect(marketId, ev.metaKey || ev.ctrlKey);
|
||||
}}
|
||||
data-testid={`market-link-${marketId}`}
|
||||
>
|
||||
{columns.map(({ kind, value, className, dataTestId, onlyOnDetailed }) => {
|
||||
if (!onlyOnDetailed || detailed === onlyOnDetailed) {
|
||||
const tdClass = classNames(className);
|
||||
return (
|
||||
<td key={kind} data-testid={dataTestId} className={tdClass}>
|
||||
{value}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import * as constants from '../constants';
|
||||
|
||||
export const WelcomeDialogHeader = () => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const header =
|
||||
VEGA_ENV === Networks.MAINNET
|
||||
? constants.MAINNET_WELCOME_HEADER
|
||||
: constants.TESTNET_WELCOME_HEADER;
|
||||
return <h1 className="mb-6 p-4 text-center text-2xl">{header}</h1>;
|
||||
};
|
@ -8,7 +8,6 @@ import { activeMarketsProvider } from '@vegaprotocol/markets';
|
||||
import * as constants from '../constants';
|
||||
import { RiskNoticeDialog } from './risk-notice-dialog';
|
||||
import { WelcomeNoticeDialog } from './welcome-notice-dialog';
|
||||
import { WelcomeLandingDialog } from './welcome-landing-dialog';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { Networks } from '@vegaprotocol/environment';
|
||||
@ -43,6 +42,7 @@ export const WelcomeDialog = () => {
|
||||
shouldDisplayWelcomeDialog: isRiskDialogNeeded,
|
||||
});
|
||||
}, [update, isRiskDialogNeeded]);
|
||||
|
||||
if (isRiskDialogNeeded) {
|
||||
dialogContent = (
|
||||
<RiskNoticeDialog onClose={onCloseDialog} network={VEGA_ENV} />
|
||||
@ -52,9 +52,6 @@ export const WelcomeDialog = () => {
|
||||
} else if (isWelcomeDialogNeeded && data?.length === 0) {
|
||||
dialogContent = <WelcomeNoticeDialog />;
|
||||
onClose = onCloseDialog;
|
||||
} else if (isWelcomeDialogNeeded && (data?.length || 0) > 0) {
|
||||
dialogContent = <WelcomeLandingDialog onClose={onCloseDialog} />;
|
||||
onClose = onCloseDialog;
|
||||
} else {
|
||||
dialogContent = null as React.ReactNode;
|
||||
}
|
||||
|
@ -1,258 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type {
|
||||
MarketMaybeWithCandles,
|
||||
MarketMaybeWithData,
|
||||
MarketData,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { SelectMarketLandingTable } from './welcome-landing-dialog';
|
||||
const mockMarketClickHandler = jest.fn();
|
||||
jest.mock('../../lib/hooks/use-market-click-handler', () => ({
|
||||
useMarketClickHandler: () => mockMarketClickHandler,
|
||||
}));
|
||||
|
||||
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
|
||||
type PartialMarket = Partial<
|
||||
Omit<Market, 'data'> & { data: Partial<MarketData> }
|
||||
>;
|
||||
|
||||
const MARKET_A: PartialMarket = {
|
||||
__typename: 'Market',
|
||||
id: '1',
|
||||
decimalPlaces: 2,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
id: '1',
|
||||
code: 'ABCDEF',
|
||||
name: 'ABCDEF 1-Day',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'ABCDEF',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-ABC',
|
||||
name: 'asset-ABC',
|
||||
decimals: 2,
|
||||
symbol: 'ABC',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
data: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
data: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
tradingTerminationProperty: 'trading-termination-property',
|
||||
settlementDataProperty: 'settlement-data-property',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
__typename: 'InstrumentMetadata',
|
||||
tags: ['ABC', 'DEF'],
|
||||
},
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
__typename: 'FeeFactors',
|
||||
infrastructureFee: '0.01',
|
||||
liquidityFee: '0.01',
|
||||
makerFee: '0.01',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '1',
|
||||
},
|
||||
markPrice: '90',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
|
||||
marketState: Schema.MarketState.STATE_PENDING,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
indicativeVolume: '1000',
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '10',
|
||||
close: '80',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:49:00Z',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '10',
|
||||
low: '1',
|
||||
open: '1',
|
||||
close: '100',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:50:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const MARKET_B: PartialMarket = {
|
||||
__typename: 'Market',
|
||||
id: '2',
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 0,
|
||||
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
id: '2',
|
||||
code: 'XYZ',
|
||||
name: 'XYZ 1-Day',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'XYZ',
|
||||
settlementAsset: {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-XYZ',
|
||||
name: 'asset-XYZ',
|
||||
decimals: 2,
|
||||
symbol: 'XYZ',
|
||||
},
|
||||
dataSourceSpecForTradingTermination: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
data: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecForSettlementData: {
|
||||
__typename: 'DataSourceSpec',
|
||||
id: 'oracleId',
|
||||
data: {
|
||||
__typename: 'DataSourceDefinition',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceDefinitionExternal',
|
||||
sourceType: {
|
||||
__typename: 'DataSourceSpecConfiguration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dataSourceSpecBinding: {
|
||||
__typename: 'DataSourceSpecToFutureBinding',
|
||||
tradingTerminationProperty: 'trading-termination-property',
|
||||
settlementDataProperty: 'settlement-data-property',
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
__typename: 'InstrumentMetadata',
|
||||
tags: ['XYZ'],
|
||||
},
|
||||
},
|
||||
},
|
||||
fees: {
|
||||
__typename: 'Fees',
|
||||
factors: {
|
||||
__typename: 'FeeFactors',
|
||||
infrastructureFee: '0.01',
|
||||
liquidityFee: '0.01',
|
||||
makerFee: '0.01',
|
||||
},
|
||||
},
|
||||
data: {
|
||||
__typename: 'MarketData',
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
id: '2',
|
||||
},
|
||||
markPrice: '123.123',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
|
||||
marketState: Schema.MarketState.STATE_PENDING,
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
indicativeVolume: '2000',
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
high: '100',
|
||||
low: '10',
|
||||
open: '10',
|
||||
close: '80',
|
||||
volume: '1000',
|
||||
periodStart: '2022-11-01T15:49:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('WelcomeLandingDialog', () => {
|
||||
it('should call onSelect callback on SelectMarketLandingTable', () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SelectMarketLandingTable
|
||||
markets={[MARKET_A as Market, MARKET_B as Market]}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-2`)[0]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not call onClose when metaKey is held', () => {
|
||||
const onClose = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SelectMarketLandingTable
|
||||
markets={[MARKET_A as Market, MARKET_B as Market]}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
{ wrapper: MockedProvider }
|
||||
);
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0], {
|
||||
metaKey: true,
|
||||
});
|
||||
expect(mockMarketClickHandler).toHaveBeenCalled();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
@ -1,130 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { Link as UILink, TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
import type { OnCellClickHandler } from '../select-market';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||
import {
|
||||
ColumnKind,
|
||||
columns,
|
||||
SelectMarketTableHeader,
|
||||
SelectMarketTableRow,
|
||||
} from '../select-market';
|
||||
import { WelcomeDialogHeader } from './welcome-dialog-header';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ProposedMarkets } from './proposed-markets';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { TelemetryApproval } from './telemetry-approval';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
export const SelectMarketLandingTable = ({
|
||||
markets,
|
||||
onClose,
|
||||
}: {
|
||||
markets: MarketMaybeWithDataAndCandles[] | null;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const onSelect = useMarketClickHandler();
|
||||
const onSelectMarket = useCallback(
|
||||
(id: string, metaKey?: boolean) => {
|
||||
onSelect(id, metaKey);
|
||||
if (!metaKey) {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onSelect, onClose]
|
||||
);
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const onCellClick = useCallback<OnCellClickHandler>(
|
||||
(e, kind, value) => {
|
||||
if (value && kind === ColumnKind.Asset) {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
||||
}
|
||||
},
|
||||
[openAssetDetailsDialog]
|
||||
);
|
||||
const showProposed = (markets?.length || 0) <= 5;
|
||||
return (
|
||||
<>
|
||||
<TinyScroll
|
||||
className="max-h-[60vh] overflow-x-auto -mr-4 pr-4"
|
||||
data-testid="select-market-list"
|
||||
>
|
||||
<p className="text-neutral-500 dark:text-neutral-400 mb-4">
|
||||
{t('Select a market to get started...')}
|
||||
</p>
|
||||
<table className="text-sm relative h-full min-w-full whitespace-nowrap">
|
||||
<thead className="sticky top-0 z-10 bg-white dark:bg-black">
|
||||
<SelectMarketTableHeader />
|
||||
</thead>
|
||||
<tbody>
|
||||
{markets?.map((market, i) => (
|
||||
<SelectMarketTableRow
|
||||
marketId={market.id}
|
||||
key={i}
|
||||
detailed={false}
|
||||
onSelect={onSelectMarket}
|
||||
columns={columns(market, onSelectMarket, onCellClick)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TinyScroll>
|
||||
<div className="mt-4 text-md">
|
||||
<Link
|
||||
to={Links[Routes.MARKETS]()}
|
||||
data-testid="view-market-list-link"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<UILink className="text-sm underline">
|
||||
{'Or view full market list'}
|
||||
</UILink>
|
||||
</Link>
|
||||
</div>
|
||||
{showProposed && <ProposedMarkets />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface LandingDialogContainerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WelcomeLandingDialog = ({
|
||||
onClose,
|
||||
}: LandingDialogContainerProps) => {
|
||||
const { data, loading, error } = useMarketList();
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const isMainnet = VEGA_ENV === Networks.MAINNET;
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<p className="my-8">{t('Failed to load markets')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<p className="my-8">{t('Loading...')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WelcomeDialogHeader />
|
||||
<SelectMarketLandingTable markets={data} onClose={onClose} />
|
||||
{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.'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -92,10 +92,7 @@ export const SparklineView = ({
|
||||
return (
|
||||
<svg
|
||||
data-testid="sparkline-svg"
|
||||
className={classNames(
|
||||
'pt-px pr-0 w-full overflow-visible p-2',
|
||||
className
|
||||
)}
|
||||
className={classNames('w-full', className)}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
|
Loading…
Reference in New Issue
Block a user