Compare commits

...
Author SHA1 Message Date
asiaznik 81bdd1fdca chore(explorer): move banner up 2023-03-20 14:28:43 +01:00
dexturr ca63a671c8 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 12:09:43 +00:00
Maciek 4165f3905f chore(static): update domains in network conf files (#3219) 2023-03-20 09:58:05 +00:00
dexturr a359ccb755 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 06:07:41 +00:00
dexturr e584737005 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-20 00:17:40 +00:00
dexturr b12c5ed39f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 18:08:07 +00:00
dexturr d3dbd64cf1 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 12:07:36 +00:00
dexturr 2df62bca61 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 06:07:48 +00:00
dexturr fd2a434b46 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-19 00:17:10 +00:00
dexturr 4e7a78f988 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 18:08:23 +00:00
dexturr 327619652a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 12:07:24 +00:00
dexturr d2339205cc chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 06:06:50 +00:00
dexturr 63df4cf465 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-18 00:13:54 +00:00
dexturr 5e34178291 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 18:06:54 +00:00
dexturr 0c44664aa5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 12:07:53 +00:00
Bartłomiej Głownia 7ef1d75529 chore(trading): remove red background from trades table (#3214) 2023-03-17 09:52:40 +00:00
Maciek 84795b2d6a chore(trading): split bottom panel into two parts (#3205) 2023-03-17 09:50:43 +00:00
dexturr 300019f108 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 06:09:09 +00:00
dexturr 2d0aebd3fe chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-17 00:14:43 +00:00
dexturr 9e86d2aff5 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 18:09:41 +00:00
daro-maj 0f7e76ae01 test(trading): increase coverage for 0004-ewal (#3210) 2023-03-16 13:29:40 +01:00
dexturr 56732d1503 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 12:08:59 +00:00
Joe Tsang 5c19263653 chore(governance): include slippage factor to proposals (#3204) 2023-03-16 10:41:18 +00:00
dexturr 0d911b3029 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 06:08:43 +00:00
Matthew RussellandEdd 9f059e7862 chore(explorer,governance): update be endpoints (#3183)
Co-authored-by: Edd <edd@vega.xyz>
2023-03-15 20:12:04 -07:00
dexturr 92ea93dadb chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-16 00:16:50 +00:00
daro-maj 6165aefad7 test(trading): fixed settlement asset displayed test (#3209) 2023-03-15 18:38:57 +00:00
dexturr c95369d914 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 18:08:52 +00:00
Maciek 5dbc5d7997 chore(trading): make loading masks in tabs consistent along the app (#3155) 2023-03-15 16:08:48 +01:00
dexturr 47b3d5b55f chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-03-15 12:07:50 +00:00
Maciek da7c0b84f7 chore(trading): display latency in block height progressing as warn or error (#3169) 2023-03-15 12:49:31 +01:00
42 changed files with 2162 additions and 1292 deletions
+3 -3
View File
@@ -2,11 +2,11 @@
NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
+1 -1
View File
@@ -71,8 +71,8 @@ function App() {
)}
>
<div>
<Header />
<MainnetSimAd />
<Header />
</div>
<div>
<Main />
@@ -10,7 +10,7 @@ export const Footer = () => {
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl'].includes(screenSize),
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
[screenSize]
);
@@ -2,6 +2,8 @@
"changes": {
"decimalPlaces": "5",
"positionDecimalPlaces": "5",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"lpPriceRange": "10",
"instrument": {
"name": "Token test market",
@@ -1,5 +1,7 @@
{
"lpPriceRange": "10",
"linearSlippageFactor": "0.001",
"quadraticSlippageFactor": "0",
"instrument": {
"code": "TEST.24h",
"future": {
@@ -110,10 +110,9 @@ Cypress.Commands.add('vega_wallet_teardown', function () {
}
});
cy.get(vegaWalletContainer).within(() => {
cy.get('[data-testid="associated-amount"]', { timeout: 30000 }).should(
'contain.text',
'0.00'
);
cy.get('[data-testid="vega-wallet-balance-unstaked"]', {
timeout: 30000,
}).should('contain.text', '0.00');
});
});
+1 -1
View File
@@ -2,7 +2,7 @@
NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
+1 -1
View File
@@ -1,3 +1,3 @@
{
"hosts": ["https://api.n04.d.vega.xyz/graphql"]
"hosts": ["https://api-n04.d.vega.rocks/graphql"]
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,3 +1,3 @@
{
"hosts": ["https://api.n00.mainnet-mirror.vega.xyz/graphql"]
"hosts": ["https://api-n00.mainnet-mirror.vega.rocks/graphql"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"hosts": ["https://api.n01.sandbox.vega.xyz/graphql"]
"hosts": ["https://api-n01.sandbox.vega.rocks/graphql"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"hosts": ["https://api.n00.stagnet1.vega.xyz/graphql"]
"hosts": ["https://api-n00.stagnet1.vega.rocks/graphql"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"hosts": ["https://api.stagnet3.vega.xyz/graphql"]
"hosts": ["https://api-stagnet3.vega.rocks/graphql"]
}
+7 -7
View File
@@ -1,11 +1,11 @@
{
"hosts": [
"https://api.n06.testnet.vega.xyz/graphql",
"https://api.n07.testnet.vega.xyz/graphql",
"https://api.n08.testnet.vega.xyz/graphql",
"https://api.n09.testnet.vega.xyz/graphql",
"https://api.n10.testnet.vega.xyz/graphql",
"https://api.n11.testnet.vega.xyz/graphql",
"https://api.n12.testnet.vega.xyz/graphql"
"https://api-n06.testnet.vega.rocks/graphql",
"https://api-n07.testnet.vega.rocks/graphql",
"https://api-n08.testnet.vega.rocks/graphql",
"https://api-n09.testnet.vega.rocks/graphql",
"https://api-n10.testnet.vega.rocks/graphql",
"https://api-n11.testnet.vega.rocks/graphql",
"https://api-n12.testnet.vega.rocks/graphql"
]
}
@@ -7,6 +7,10 @@ const externalLink = 'external-link';
const accordionContent = 'accordion-content';
describe('market info is displayed', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
});
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
@@ -216,6 +216,60 @@ describe('Market trading page', () => {
});
});
});
describe('market bottom panel', { tags: '@smoke' }, () => {
it('on xxl screen should be splitted out into two tables', () => {
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'active'
);
cy.getByTestId('tab-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.viewport(1801, 1000);
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'active'
);
cy.getByTestId('tab-orders').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'inactive');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('Fills').click();
cy.getByTestId('Collateral').click();
cy.getByTestId('tab-positions').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-orders').should(
'have.attr',
'data-state',
'inactive'
);
cy.getByTestId('tab-fills').should('have.attr', 'data-state', 'active');
cy.getByTestId('tab-accounts').should(
'have.attr',
'data-state',
'active'
);
});
});
});
describe('market states not accepting orders', { tags: '@smoke' }, function () {
@@ -0,0 +1,58 @@
import { connectEthereumWallet } from '../support/ethereum-wallet';
const connectEthWalletBtn = 'connect-eth-wallet-btn';
describe('ethereum wallet', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
// Using portfolio withdrawals tab is it requires Ethereum wallet connection
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist');
cy.getByTestId('Withdrawals').click();
});
it('can connect', () => {
// 0004-EWAL-001
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-MetaMask').click();
cy.getByTestId('web3-connector-list').should('not.exist');
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it('should see an option to cancel the attempted connection', () => {
// 0004-EWAL-003
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-WalletConnect').click();
cy.get('#walletconnect-qrcode-text').should('exist');
cy.get('#walletconnect-qrcode-close').click();
});
it('able to disconnect eth wallet', () => {
// 0004-EWAL-004
// 0004-EWAL-005
// 0004-EWAL-006
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
cy.getByTestId(connectEthWalletBtn).should('exist');
});
});
@@ -1,7 +1,8 @@
import { mockConnectWallet } from '@vegaprotocol/cypress';
import { connectEthereumWallet } from '../support/ethereum-wallet';
import {
mockConnectWallet,
mockConnectWalletWithUserError,
} from '@vegaprotocol/cypress';
const connectEthWalletBtn = 'connect-eth-wallet-btn';
const connectVegaBtn = 'connect-vega-wallet';
const manageVegaBtn = 'manage-vega-wallet';
const form = 'rest-connector-form';
@@ -127,6 +128,21 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
cy.getByTestId(manageVegaBtn).should('exist');
});
it('can not connect', () => {
// 0002-WCON-002
// 0002-WCON-005
// 0002-WCON-007
mockConnectWalletWithUserError();
cy.getByTestId(connectVegaBtn).click();
cy.getByTestId('connectors-list')
.find('[data-testid="connector-jsonRpc"]')
.click();
cy.getByTestId('dialog-content')
.should('contain.text', 'User error')
.and('contain.text', 'the user rejected the wallet connection');
});
it('can change selected public key and disconnect', () => {
// 0002-WCON-022
// 0002-WCON-023
@@ -164,39 +180,3 @@ describe('connect vega wallet', { tags: '@smoke' }, () => {
);
});
});
describe('ethereum wallet', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockWeb3Provider();
// Using portfolio withdrawals tab is it requires Ethereum wallet connection
cy.mockTradingPage();
cy.mockSubscription();
cy.setVegaWallet();
cy.visit('/#/portfolio');
cy.get('main[data-testid="/portfolio"]').should('exist');
cy.getByTestId('Withdrawals').click();
});
it('can connect', () => {
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
cy.getByTestId('web3-connector-list').should('exist');
cy.getByTestId('web3-connector-MetaMask').click();
cy.getByTestId('web3-connector-list').should('not.exist');
cy.getByTestId('tab-deposits').should('not.be.empty');
});
it('able to disconnect eth wallet', () => {
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
cy.getByTestId(connectEthWalletBtn).should('exist');
});
});
+6 -39
View File
@@ -1,9 +1,9 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
useDataProvider,
useScreenDimensions,
useThrottledDataProvider,
} from '@vegaprotocol/react-helpers';
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
@@ -61,7 +61,8 @@ export const MarketPage = () => {
const { marketId } = useParams();
const navigate = useNavigate();
const { w } = useWindowSize();
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const update = useGlobalStore((store) => store.update);
const lastMarketId = useGlobalStore((store) => store.marketId);
@@ -87,7 +88,7 @@ export const MarketPage = () => {
}, [update, lastMarketId, data?.id]);
const tradeView = useMemo(() => {
if (w > 960) {
if (largeScreen) {
return (
<TradeGrid
market={data}
@@ -105,7 +106,7 @@ export const MarketPage = () => {
onClickCollateral={() => navigate('/portfolio')}
/>
);
}, [w, data, onSelect, navigate]);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@@ -140,37 +141,3 @@ export const MarketPage = () => {
</AsyncRenderer>
);
};
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState(() => {
if (typeof window !== 'undefined') {
return {
w: window.innerWidth,
h: window.innerHeight,
};
}
// Something sensible for server rendered page
return {
w: 1200,
h: 900,
};
});
useEffect(() => {
const handleResize = debounce(({ target }) => {
setWindowSize({
w: target.innerWidth,
h: target.innerHeight,
});
}, 300);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return windowSize;
};
+195 -119
View File
@@ -8,7 +8,7 @@ import { TradesContainer } from '@vegaprotocol/trades';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { memo, useState } from 'react';
import { memo, useCallback, useState } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
@@ -29,6 +29,7 @@ import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -69,129 +70,204 @@ interface TradeGridProps {
pinnedAsset?: PinnedAsset;
}
const MainGrid = ({
marketId,
onSelect,
pinnedAsset,
}: {
interface BottomPanelProps {
marketId: string;
onSelect?: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
const onMarketClick = (marketId: string) => {
navigate(Links[Routes.MARKET](marketId), {
replace: true,
});
};
return (
<ResizableGrid vertical>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid proportionalLayout={false} minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize="50%"
>
<TradeGridChild>
<Tabs>
<Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} />
</Tab>
<Tab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={marketId} />
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<TradingViews.Liquidity marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={330}
minSize={300}
>
<TradeGridChild>
<Tabs>
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket
}
const MarketBottomPanel = memo(
({ marketId, pinnedAsset }: BottomPanelProps) => {
const { screenSize } = useScreenDimensions();
const navigate = useNavigate();
const onMarketClick = useCallback(
(marketId: string) => {
navigate(Links[Routes.MARKET](marketId), {
replace: true,
});
},
[navigate]
);
return 'xxxl' === screenSize ? (
<ResizableGrid proportionalLayout minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="50%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onClickCollateral={() => navigate('/portfolio')}
onMarketClick={onMarketClick}
/>
</Tab>
<Tab id="info" name={t('Info')}>
<TradingViews.Info
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onSelect={(id: string) => {
onSelect?.(id);
}}
onMarketClick={onMarketClick}
/>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={430}
minSize={200}
>
<TradeGridChild>
<Tabs>
<Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} />
</Tab>
<Tab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="25%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral
pinnedAsset={pinnedAsset}
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
);
};
const MainGridWrapped = memo(MainGrid);
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="50%"
minSize={50}
>
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions
onMarketClick={onMarketClick}
noBottomPlaceholder
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral
pinnedAsset={pinnedAsset}
noBottomPlaceholder
hideButtons
/>
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
) : (
<TradeGridChild>
<Tabs>
<Tab id="positions" name={t('Positions')}>
<VegaWalletContainer>
<TradingViews.Positions onMarketClick={onMarketClick} />
</VegaWalletContainer>
</Tab>
<Tab id="orders" name={t('Orders')}>
<VegaWalletContainer>
<TradingViews.Orders
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="fills" name={t('Fills')}>
<VegaWalletContainer>
<TradingViews.Fills
marketId={marketId}
onMarketClick={onMarketClick}
/>
</VegaWalletContainer>
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral pinnedAsset={pinnedAsset} hideButtons />
</VegaWalletContainer>
</Tab>
</Tabs>
</TradeGridChild>
);
}
);
MarketBottomPanel.displayName = 'MarketBottomPanel';
const MainGrid = memo(
({
marketId,
onSelect,
pinnedAsset,
}: {
marketId: string;
onSelect?: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
return (
<ResizableGrid vertical>
<ResizableGridPanel minSize={75} priority={LayoutPriority.High}>
<ResizableGrid proportionalLayout={false} minSize={200}>
<ResizableGridPanel
priority={LayoutPriority.High}
minSize={200}
preferredSize="50%"
>
<TradeGridChild>
<Tabs>
<Tab id="chart" name={t('Chart')}>
<TradingViews.Candles marketId={marketId} />
</Tab>
<Tab id="depth" name={t('Depth')}>
<TradingViews.Depth marketId={marketId} />
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<TradingViews.Liquidity marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={330}
minSize={300}
>
<TradeGridChild>
<Tabs>
<Tab id="ticket" name={t('Ticket')}>
<TradingViews.Ticket
marketId={marketId}
onClickCollateral={() => navigate('/portfolio')}
/>
</Tab>
<Tab id="info" name={t('Info')}>
<TradingViews.Info
marketId={marketId}
onSelect={(id: string) => {
onSelect?.(id);
}}
/>
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize={430}
minSize={200}
>
<TradeGridChild>
<Tabs>
<Tab id="orderbook" name={t('Orderbook')}>
<TradingViews.Orderbook marketId={marketId} />
</Tab>
<Tab id="trades" name={t('Trades')}>
<TradingViews.Trades marketId={marketId} />
</Tab>
</Tabs>
</TradeGridChild>
</ResizableGridPanel>
</ResizableGrid>
</ResizableGridPanel>
<ResizableGridPanel
priority={LayoutPriority.Low}
preferredSize="25%"
minSize={50}
>
<MarketBottomPanel marketId={marketId} pinnedAsset={pinnedAsset} />
</ResizableGridPanel>
</ResizableGrid>
);
}
);
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({
market,
@@ -201,7 +277,7 @@ export const TradeGrid = ({
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<TradeMarketHeader market={market} onSelect={onSelect} />
<MainGridWrapped
<MainGrid
marketId={market?.id || ''}
onSelect={onSelect}
pinnedAsset={pinnedAsset}
@@ -25,7 +25,8 @@ export const DepositsContainer = () => {
<div className="h-full relative">
<DepositsTable
rowData={data || []}
noRowsOverlayComponent={() => null}
suppressLoadingOverlay
suppressNoRowsOverlay
ref={gridRef}
{...bottomPlaceholderProps}
/>
@@ -24,7 +24,8 @@ export const WithdrawalsContainer = () => {
<WithdrawalsTable
data-testid="withdrawals-history"
rowData={data}
noRowsOverlayComponent={() => null}
suppressLoadingOverlay
suppressNoRowsOverlay
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@@ -12,9 +12,11 @@ import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = ({
pinnedAsset,
hideButtons,
noBottomPlaceholder,
}: {
pinnedAsset?: PinnedAsset;
hideButtons?: boolean;
noBottomPlaceholder?: boolean;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
@@ -46,6 +48,7 @@ export const AccountsContainer = ({
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
noBottomPlaceholder={noBottomPlaceholder}
/>
{!isReadOnly && !hideButtons && (
<div className="flex gap-2 justify-end p-2 px-[11px] absolute lg:fixed bottom-0 right-3 dark:bg-black/75 bg-white/75 rounded">
+33 -15
View File
@@ -1,20 +1,30 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
import { MockedProvider } from '@apollo/client/testing';
import { Intent } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest
.fn()
.mockImplementation(() => ({ VEGA_URL: 'https://vega-url.wtf' })),
}));
const mockSetNodeSwitcher = jest.fn();
jest.mock('../../stores', () => ({
...jest.requireActual('../../stores'),
useGlobalStore: () => mockSetNodeSwitcher,
}));
describe('NodeHealth', () => {
it('controls the node switcher dialog', async () => {
const mockOnClick = jest.fn();
render(
<NodeHealth
onClick={mockOnClick}
url={'https://api.n99.somenetwork.vega.xyz'}
blockHeight={100}
blockDiff={0}
/>
);
render(<NodeHealth />, { wrapper: MockedProvider });
await waitFor(() => {
expect(screen.getByRole('button')).toBeInTheDocument();
});
await userEvent.click(screen.getByRole('button'));
expect(mockOnClick).toHaveBeenCalled();
expect(mockSetNodeSwitcher).toHaveBeenCalled();
});
});
@@ -31,14 +41,22 @@ describe('NodeUrl', () => {
describe('HealthIndicator', () => {
const cases = [
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
{
intent: Intent.Success,
text: 'Operational',
classname: 'bg-vega-green-550',
},
{
intent: Intent.Warning,
text: '5 Blocks behind',
classname: 'bg-warning',
},
{ intent: Intent.Danger, text: 'Non operational', classname: 'bg-danger' },
];
it.each(cases)(
'renders correct text and indicator color for $diff block difference',
(elem) => {
render(<HealthIndicator blockDiff={elem.diff} />);
render(<HealthIndicator text={elem.text} intent={elem.intent} />);
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
expect(screen.getByText(elem.text)).toBeInTheDocument();
}
+21 -56
View File
@@ -1,60 +1,45 @@
import { useCallback } from 'react';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/i18n';
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Indicator } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useGlobalStore } from '../../stores';
export const Footer = () => {
const { VEGA_URL } = useEnvironment();
const setNodeSwitcher = useGlobalStore(
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
);
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
return (
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
{/* Pull left to align with top nav, due to button padding */}
<div className="-ml-2">
{VEGA_URL && (
<NodeHealth
url={VEGA_URL}
blockHeight={datanodeBlockHeight}
blockDiff={blockDiff}
onClick={() => setNodeSwitcher(true)}
/>
)}
<NodeHealth />
</div>
</footer>
);
};
interface NodeHealthProps {
url: string;
blockHeight: number | undefined;
blockDiff: number | null;
onClick: () => void;
}
export const NodeHealth = ({
url,
blockHeight,
blockDiff,
onClick,
}: NodeHealthProps) => {
return (
export const NodeHealth = () => {
const { VEGA_URL } = useEnvironment();
const setNodeSwitcher = useGlobalStore(
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
);
const { datanodeBlockHeight, text, intent } = useNodeHealth();
const onClick = useCallback(() => {
setNodeSwitcher(true);
}, [setNodeSwitcher]);
return VEGA_URL ? (
<FooterButton onClick={onClick} data-testid="node-health">
<FooterButtonPart>
<HealthIndicator blockDiff={blockDiff} />
<HealthIndicator text={text} intent={intent} />
</FooterButtonPart>
<FooterButtonPart>
<NodeUrl url={url} />
<NodeUrl url={VEGA_URL} />
</FooterButtonPart>
<FooterButtonPart>
<span title={t('Block height')}>{blockHeight}</span>
<span title={t('Block height')}>{datanodeBlockHeight}</span>
</FooterButtonPart>
</FooterButton>
);
) : null;
};
interface NodeUrlProps {
@@ -69,31 +54,11 @@ export const NodeUrl = ({ url }: NodeUrlProps) => {
};
interface HealthIndicatorProps {
blockDiff: number | null;
text: string;
intent: Intent;
}
// How many blocks behind the most advanced block that is
// deemed acceptable for "Good" status
const BLOCK_THRESHOLD = 3;
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
const online = useNavigatorOnline();
let intent = Intent.Success;
let text = 'Operational';
if (!online) {
text = t('Offline');
intent = Intent.Danger;
} else if (blockDiff === null) {
// Block height query failed and null was returned
text = t('Non operational');
intent = Intent.Danger;
} else if (blockDiff >= BLOCK_THRESHOLD) {
text = t(`${blockDiff} Blocks behind`);
intent = Intent.Warning;
}
export const HealthIndicator = ({ text, intent }: HealthIndicatorProps) => {
return (
<span title={t('Node health')}>
<Indicator variant={intent} />
+87 -51
View File
@@ -15,51 +15,29 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
}));
describe('AccountManager', () => {
beforeEach(() => {
mockedUseDataProvider
.mockImplementationOnce((args) => {
return {
data: [],
};
})
.mockImplementationOnce((args) => {
return {
data: [
{ asset: { id: 'a1' }, party: { id: 't1' } },
{ asset: { id: 'a2' }, party: { id: 't2' } },
],
};
});
});
it('change partyId should reload data provider', async () => {
const { rerender } = render(
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[0][0].variables.partyId
).toEqual('partyOne');
await act(() => {
rerender(
<AccountManager
partyId="partyTwo"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
describe('when rerender', () => {
beforeEach(() => {
mockedUseDataProvider
.mockImplementationOnce((args) => {
return {
data: [],
};
})
.mockImplementationOnce((args) => {
return {
data: [
{ asset: { id: 'a1' }, party: { id: 't1' } },
{ asset: { id: 'a2' }, party: { id: 't2' } },
],
};
});
});
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables.partyId
).toEqual('partyTwo');
});
it('update method should return proper result', async () => {
let rerenderer: (ui: React.ReactElement) => void;
await act(() => {
afterEach(() => {
jest.clearAllMocks();
});
it('change partyId should reload data provider', async () => {
const { rerender } = render(
<AccountManager
partyId="partyOne"
@@ -67,13 +45,67 @@ describe('AccountManager', () => {
isReadOnly={false}
/>
);
rerenderer = rerender;
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[0][0].variables
.partyId
).toEqual('partyOne');
await act(() => {
rerender(
<AccountManager
partyId="partyTwo"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
});
expect(
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables
.partyId
).toEqual('partyTwo');
});
await waitFor(() => {
expect(screen.getByText('No accounts')).toBeInTheDocument();
it('update method should return proper result', async () => {
let rerenderer: (ui: React.ReactElement) => void;
await act(() => {
const { rerender } = render(
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
rerenderer = rerender;
});
await waitFor(() => {
expect(screen.getByText('No accounts')).toBeInTheDocument();
});
await act(() => {
rerenderer(
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
isReadOnly={false}
/>
);
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
});
});
it('splash loading should be displayed', async () => {
mockedUseDataProvider.mockImplementation((args) => {
return {
loading: true,
data: null,
};
});
await act(() => {
rerenderer(
render(
<AccountManager
partyId="partyOne"
onClickAsset={jest.fn}
@@ -81,11 +113,15 @@ describe('AccountManager', () => {
/>
);
});
const container = document.querySelector('.ag-center-cols-container');
await waitFor(() => {
expect(container).toBeInTheDocument();
expect(
screen.getByText(
(content, element) =>
Boolean(
element?.className.endsWith('flex items-center justify-center')
) && content.startsWith('Loading')
)
).toBeInTheDocument();
});
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
});
});
+5 -1
View File
@@ -19,6 +19,7 @@ interface AccountManagerProps {
onClickDeposit?: (assetId?: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
noBottomPlaceholder?: boolean;
}
export const AccountManager = ({
@@ -28,6 +29,7 @@ export const AccountManager = ({
partyId,
isReadOnly,
pinnedAsset,
noBottomPlaceholder,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
@@ -45,6 +47,7 @@ export const AccountManager = ({
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
const getRowHeight = useCallback(
@@ -60,7 +63,8 @@ export const AccountManager = ({
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
isReadOnly={isReadOnly}
noRowsOverlayComponent={() => null}
suppressLoadingOverlay
suppressNoRowsOverlay
pinnedAsset={pinnedAsset}
getRowHeight={getRowHeight}
{...bottomPlaceholderProps}
+13 -7
View File
@@ -59,13 +59,19 @@ export function createClient({
const timestamp = r?.headers.get('x-block-timestamp');
if (blockHeight && timestamp) {
const state = useHeaderStore.getState();
useHeaderStore.setState({
...state,
[r.url]: {
blockHeight: Number(blockHeight),
timestamp: new Date(Number(timestamp.slice(0, -6))),
},
});
const urlState = state[r.url];
if (
!urlState?.blockHeight ||
urlState.blockHeight !== blockHeight
) {
useHeaderStore.setState({
...state,
[r.url]: {
blockHeight: Number(blockHeight),
timestamp: new Date(Number(timestamp.slice(0, -6))),
},
});
}
}
return response;
});
+4 -1
View File
@@ -46,7 +46,10 @@ addVegaWalletSubmitProposal();
addVegaWalletSubmitLiquidityProvision();
addImportNodeWallets();
export { mockConnectWallet } from './lib/commands/vega-wallet-connect';
export {
mockConnectWallet,
mockConnectWalletWithUserError,
} from './lib/commands/vega-wallet-connect';
export type { onMessage } from './lib/mock-ws';
export { aliasGQLQuery } from './lib/mock-gql';
export { aliasWalletQuery } from './lib/mock-rest';
@@ -35,6 +35,8 @@ function createNewMarketProposal(): ProposalSubmissionBody {
changes: {
decimalPlaces: '5',
positionDecimalPlaces: '5',
linearSlippageFactor: '0.001',
quadraticSlippageFactor: '0',
lpPriceRange: '10',
instrument: {
name: 'Test market 1',
@@ -1,4 +1,7 @@
import { aliasWalletConnectQuery } from '../mock-rest';
import {
aliasWalletConnectQuery,
aliasWalletConnectWithUserError,
} from '../mock-rest';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
@@ -20,6 +23,12 @@ export const mockConnectWallet = () => {
});
};
export const mockConnectWalletWithUserError = () => {
cy.mockWallet((req) => {
aliasWalletConnectWithUserError(req);
});
};
export function addVegaWalletConnect() {
Cypress.Commands.add('connectVegaWallet', (isMobile) => {
mockConnectWallet();
+20
View File
@@ -63,3 +63,23 @@ export const aliasWalletConnectQuery = (
});
}
};
export const aliasWalletConnectWithUserError = (
req: CyHttpMessages.IncomingHttpRequest
) => {
if (hasMethod(req, 'client.connect_wallet')) {
req.alias = 'client.connect_wallet';
req.reply({
statusCode: 400,
body: {
jsonrpc: '2.0',
error: {
code: 3001,
data: 'the user rejected the wallet connection',
message: 'User error',
},
id: '0',
},
});
}
};
@@ -5,6 +5,7 @@ import { MockedProvider } from '@apollo/react-testing';
import type { StatisticsQuery } from '../utils/__generated__/Node';
import { StatisticsDocument } from '../utils/__generated__/Node';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { Intent } from '@vegaprotocol/ui-toolkit';
const vegaUrl = 'https://foo.bar.com';
@@ -55,9 +56,24 @@ function setup(
describe('useNodeHealth', () => {
it.each([
{ core: 1, node: 1, expected: 0 },
{ core: 1, node: 5, expected: -4 },
{ core: 10, node: 5, expected: 5 },
{
core: 1,
node: 1,
expectedText: 'Operational',
expectedIntent: Intent.Success,
},
{
core: 1,
node: 5,
expectedText: 'Operational',
expectedIntent: Intent.Success,
},
{
core: 10,
node: 5,
expectedText: '5 Blocks behind',
expectedIntent: Intent.Warning,
},
])(
'provides difference core block $core and node block $node',
async (cases) => {
@@ -65,12 +81,12 @@ describe('useNodeHealth', () => {
blockHeight: cases.node,
timestamp: new Date(),
});
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.text).toEqual('Non operational');
expect(result.current.intent).toEqual(Intent.Danger);
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(cases.expected);
expect(result.current.coreBlockHeight).toEqual(cases.core);
expect(result.current.text).toEqual(cases.expectedText);
expect(result.current.intent).toEqual(cases.expectedIntent);
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
});
}
@@ -90,25 +106,64 @@ describe('useNodeHealth', () => {
blockHeight: 1,
timestamp: new Date(),
});
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.text).toEqual('Non operational');
expect(result.current.intent).toEqual(Intent.Danger);
expect(result.current.datanodeBlockHeight).toEqual(1);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.text).toEqual('Non operational');
expect(result.current.intent).toEqual(Intent.Danger);
expect(result.current.datanodeBlockHeight).toEqual(1);
});
});
it('returns 0 if no headers are found (waits until stats query resolves)', async () => {
const { result } = setup(createStatsMock(1), undefined);
expect(result.current.blockDiff).toEqual(null);
expect(result.current.coreBlockHeight).toEqual(undefined);
expect(result.current.text).toEqual('Non operational');
expect(result.current.intent).toEqual(Intent.Danger);
expect(result.current.datanodeBlockHeight).toEqual(undefined);
await waitFor(() => {
expect(result.current.blockDiff).toEqual(0);
expect(result.current.coreBlockHeight).toEqual(1);
expect(result.current.text).toEqual('Operational');
expect(result.current.intent).toEqual(Intent.Success);
expect(result.current.datanodeBlockHeight).toEqual(undefined);
});
});
it('Warning latency', async () => {
const now = 1678800900087;
const headerTimestamp = now - 4000;
const dateNow = new Date(now);
const dateHeaderTimestamp = new Date(headerTimestamp);
jest.useFakeTimers().setSystemTime(dateNow);
const { result } = setup(createStatsMock(2), {
blockHeight: 2,
timestamp: dateHeaderTimestamp,
});
await waitFor(() => {
expect(result.current.text).toEqual('Warning delay ( >3 sec): 4.05 sec');
expect(result.current.intent).toEqual(Intent.Warning);
expect(result.current.datanodeBlockHeight).toEqual(2);
});
});
it('Erroneous latency', async () => {
const now = 1678800900087;
const headerTimestamp = now - 11000;
const dateNow = new Date(now);
const dateHeaderTimestamp = new Date(headerTimestamp);
jest.useFakeTimers().setSystemTime(dateNow);
const { result } = setup(createStatsMock(2), {
blockHeight: 2,
timestamp: dateHeaderTimestamp,
});
await waitFor(() => {
expect(result.current.text).toEqual(
'Erroneous latency ( >10 sec): 11.05 sec'
);
expect(result.current.intent).toEqual(Intent.Danger);
expect(result.current.datanodeBlockHeight).toEqual(2);
});
jest.useRealTimers();
});
});
+48 -17
View File
@@ -2,30 +2,35 @@ import { useEffect, useMemo } from 'react';
import { useStatisticsQuery } from '../utils/__generated__/Node';
import { useHeaderStore } from '@vegaprotocol/apollo-client';
import { useEnvironment } from './use-environment';
import { fromNanoSeconds } from '@vegaprotocol/utils';
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
import { Intent } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
const POLL_INTERVAL = 1000;
const BLOCK_THRESHOLD = 3;
const ERROR_LATENCY = 10000;
const WARNING_LATENCY = 3000;
export const useNodeHealth = () => {
const online = useNavigatorOnline();
const url = useEnvironment((store) => store.VEGA_URL);
const headerStore = useHeaderStore();
const headers = url ? headerStore[url] : undefined;
const { data, error, loading, startPolling, stopPolling } =
useStatisticsQuery({
fetchPolicy: 'no-cache',
});
const { data, error, startPolling, stopPolling } = useStatisticsQuery({
fetchPolicy: 'no-cache',
});
const blockDiff = useMemo(() => {
if (!data?.statistics.blockHeight) {
return null;
}
if (!headers) {
if (!headers?.blockHeight) {
return 0;
}
return Number(data.statistics.blockHeight) - headers.blockHeight;
}, [data, headers]);
}, [data?.statistics.blockHeight, headers?.blockHeight]);
useEffect(() => {
if (error) {
@@ -38,17 +43,43 @@ export const useNodeHealth = () => {
}
}, [error, startPolling, stopPolling]);
const blockUpdateMsLatency = headers?.timestamp
? Date.now() - headers.timestamp.getTime()
: 0;
const [text, intent] = useMemo(() => {
let intent = Intent.Success;
let text = 'Operational';
if (!online) {
text = t('Offline');
intent = Intent.Danger;
} else if (blockDiff === null) {
// Block height query failed and null was returned
text = t('Non operational');
intent = Intent.Danger;
} else if (blockUpdateMsLatency > ERROR_LATENCY) {
text = t('Erroneous latency ( >%s sec): %s sec', [
(ERROR_LATENCY / 1000).toString(),
(blockUpdateMsLatency / 1000).toFixed(2),
]);
intent = Intent.Danger;
} else if (blockDiff >= BLOCK_THRESHOLD) {
text = t(`%s Blocks behind`, String(blockDiff));
intent = Intent.Warning;
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
text = t('Warning delay ( >%s sec): %s sec', [
(WARNING_LATENCY / 1000).toString(),
(blockUpdateMsLatency / 1000).toFixed(2),
]);
intent = Intent.Warning;
}
return [text, intent];
}, [online, blockDiff, blockUpdateMsLatency]);
return {
error,
loading,
coreBlockHeight: data?.statistics
? Number(data.statistics.blockHeight)
: undefined,
coreVegaTime: data?.statistics
? fromNanoSeconds(data?.statistics.vegaTime)
: undefined,
datanodeBlockHeight: headers?.blockHeight,
datanodeVegaTime: headers?.timestamp,
blockDiff,
text,
intent,
};
};
+15 -5
View File
@@ -1,7 +1,8 @@
import { useCallback, useRef } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from '../';
import { usePositionsData, PositionsTable } from '../';
import type { FilterChangedEvent } from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import * as Schema from '@vegaprotocol/types';
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
@@ -22,6 +23,7 @@ export const PositionsManager = ({
noBottomPlaceholder,
}: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = usePositionsData(
partyId,
gridRef,
@@ -66,8 +68,14 @@ export const PositionsManager = ({
const bottomPlaceholderProps = useBottomPlaceholder<Position>({
gridRef,
setId,
disabled: noBottomPlaceholder,
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data?.length]);
const onFilterChanged = useCallback((event: FilterChangedEvent) => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, []);
return (
<div className="h-full relative">
<PositionsTable
@@ -75,9 +83,11 @@ export const PositionsManager = ({
ref={gridRef}
onMarketClick={onMarketClick}
onClose={onClose}
noRowsOverlayComponent={() => null}
suppressLoadingOverlay
suppressNoRowsOverlay
isReadOnly={isReadOnly}
{...(noBottomPlaceholder ? null : bottomPlaceholderProps)}
onFilterChanged={onFilterChanged}
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@@ -85,7 +95,7 @@ export const PositionsManager = ({
error={error}
data={data}
noDataMessage={t('No positions')}
noDataCondition={(data) => !(data && data.length)}
noDataCondition={(data) => !dataCount}
reload={reload}
/>
</div>
+16 -1
View File
@@ -116,6 +116,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
value
)
}
minWidth={190}
/>
<AgGridColumn
headerName={t('Notional')}
@@ -141,6 +142,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={80}
/>
<AgGridColumn
headerName={t('Open volume')}
@@ -174,6 +176,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
);
}}
cellRenderer={OpenVolumeCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Mark price')}
@@ -213,8 +216,13 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Settlement asset')}
field="assetSymbol"
minWidth={100}
/>
<AgGridColumn headerName={t('Settlement asset')} field="assetSymbol" />
<AgGridColumn
headerName={t('Entry price')}
field="averageEntryPrice"
@@ -248,6 +256,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.marketDecimalPlaces
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Leverage')}
@@ -264,6 +273,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? undefined : formatNumber(value.toString(), 1)
}
minWidth={100}
/>
<AgGridColumn
headerName={t('Margin allocated')}
@@ -295,6 +305,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
data.decimals
);
}}
minWidth={100}
/>
<AgGridColumn
headerName={t('Realised PNL')}
@@ -321,6 +332,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Unrealised PNL')}
@@ -347,6 +359,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
'Unrealised profit is the current profit on your open position. Margin is still allocated to your position.'
)}
cellRenderer={PNLCell}
minWidth={100}
/>
<AgGridColumn
headerName={t('Updated')}
@@ -361,6 +374,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}
return getDateTimeFormat().format(new Date(value));
}}
minWidth={150}
/>
{onClose && !props.isReadOnly ? (
<AgGridColumn
@@ -375,6 +389,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
</ButtonLink>
) : null
}
minWidth={80}
/>
) : null}
</AgGrid>
@@ -11,11 +11,13 @@ const isFullWidthRow = (params: IsFullWidthRowParams) =>
interface Props<T> {
gridRef: RefObject<AgGridReact>;
setId?: (data: T) => T;
disabled?: boolean;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export const useBottomPlaceholder = <T extends {}>({
gridRef,
setId,
disabled,
}: Props<T>) => {
const onBodyScrollEnd = useCallback(() => {
const rowCont = gridRef.current?.api.getModel().getRowCount() ?? 0;
@@ -58,14 +60,17 @@ export const useBottomPlaceholder = <T extends {}>({
}, [gridRef, onBodyScrollEnd]);
return useMemo(
() => ({
onBodyScrollEnd,
rowClassRules: NO_HOVER_CSS_RULE,
isFullWidthRow,
fullWidthCellRenderer,
onSortChanged: onRowsChanged,
onFilterChange: onRowsChanged,
}),
[onBodyScrollEnd, onRowsChanged]
() =>
!disabled
? {
onBodyScrollEnd,
rowClassRules: NO_HOVER_CSS_RULE,
isFullWidthRow,
fullWidthCellRenderer,
onSortChanged: onRowsChanged,
onFilterChange: onRowsChanged,
}
: {},
[onBodyScrollEnd, onRowsChanged, disabled]
);
};
+13 -4
View File
@@ -1,10 +1,19 @@
import { useRef, useEffect, useState } from 'react';
const SERVER_SIDE_DIMENSIONS = {
width: 1200,
height: 900,
};
export const useResize = () => {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
const [windowSize, setWindowSize] = useState(
typeof window !== undefined
? {
width: window.innerWidth,
height: window.innerHeight,
}
: { ...SERVER_SIDE_DIMENSIONS }
);
const timeout = useRef(0);
@@ -1,29 +1,31 @@
import { useMemo } from 'react';
// @ts-ignore avoid adding declaration file
import { theme } from '@vegaprotocol/tailwindcss-config';
import { useResize } from './use-resize';
type Screen = keyof typeof theme.screens;
export type Screen = keyof typeof theme.screens;
interface Props {
isMobile: boolean;
screenSize: Screen;
width: number;
}
export const useScreenDimensions = (): Props => {
const { width } = useResize();
const isMobile = width < parseInt(theme.screens.md);
const screenSize = Object.entries(theme.screens).reduce(
(agg: Screen, entry) => {
if (width > parseInt(entry[1])) {
agg = entry[0] as Screen;
}
return agg;
},
'xs'
);
return useMemo(
() => ({
width,
isMobile: width < parseInt(theme.screens.md),
screenSize: Object.entries(theme.screens).reduce((agg: Screen, entry) => {
if (width > parseInt(entry[1])) {
agg = entry[0] as Screen;
}
return agg;
}, 'xs'),
isMobile,
screenSize,
}),
[width]
[isMobile, screenSize]
);
};
+1
View File
@@ -6,6 +6,7 @@ module.exports = {
lg: '960px',
xl: '1280px',
xxl: '1536px',
xxxl: '1800px',
},
colors: {
transparent: 'transparent',
+1 -1
View File
@@ -50,7 +50,7 @@ interface Props extends AgGridReactProps {
export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%', background: 'red' }}
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No trades')}
getRowId={({ data }) => data.id}
ref={ref}
@@ -98,6 +98,8 @@ interface ProposalNewMarketTerms {
decimalPlaces: string;
positionDecimalPlaces: string;
lpPriceRange: string;
linearSlippageFactor: string;
quadraticSlippageFactor: string;
instrument: {
name: string;
code: string;
@@ -130,6 +132,8 @@ interface ProposalUpdateMarketTerms {
updateMarket: {
marketId: string;
changes: {
linearSlippageFactor: string;
quadraticSlippageFactor: string;
instrument: {
code: string;
future: {