Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a327ff360 |
@@ -6,10 +6,18 @@ context('Home Page', function () {
|
||||
describe('Stats page', { tags: '@smoke' }, function () {
|
||||
const statsValue = '[data-testid="stats-value"]';
|
||||
|
||||
it('Should show connected environment', function () {
|
||||
const deployedEnv = Cypress.env('environment').toUpperCase();
|
||||
cy.get('[data-testid="stats-environment"]').should(
|
||||
'have.text',
|
||||
`/ ${deployedEnv}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should show connected environment stats', function () {
|
||||
const statTitles = {
|
||||
0: 'Status',
|
||||
1: 'Block height',
|
||||
1: 'Height',
|
||||
2: 'Uptime',
|
||||
3: 'Total nodes',
|
||||
4: 'Total staked',
|
||||
@@ -28,27 +36,27 @@ context('Home Page', function () {
|
||||
|
||||
cy.get('[data-testid="stats-title"]')
|
||||
.each(($list, index) => {
|
||||
cy.wrap($list).should('contain.text', statTitles[index]);
|
||||
cy.wrap($list).should('have.text', statTitles[index]);
|
||||
})
|
||||
.then(($list) => {
|
||||
cy.wrap($list).should('have.length', 16);
|
||||
});
|
||||
|
||||
cy.get(statsValue).eq(0).should('contain.text', 'CONNECTED');
|
||||
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
|
||||
cy.get(statsValue).eq(1).should('not.be.empty');
|
||||
cy.get(statsValue)
|
||||
.eq(2)
|
||||
.invoke('text')
|
||||
.should('match', /\d+d \d+h \d+m \d+s/i);
|
||||
cy.get(statsValue).eq(3).should('contain.text', '2');
|
||||
cy.get(statsValue).eq(3).should('have.text', '2');
|
||||
cy.get(statsValue)
|
||||
.eq(4)
|
||||
.invoke('text')
|
||||
.should('match', /\d+\.\d\d(?!\d)/i);
|
||||
cy.get(statsValue).eq(5).should('contain.text', '0');
|
||||
cy.get(statsValue).eq(6).should('contain.text', '0');
|
||||
cy.get(statsValue).eq(7).should('contain.text', '0');
|
||||
cy.get(statsValue).eq(8).should('contain.text', '0');
|
||||
cy.get(statsValue).eq(5).should('have.text', '0');
|
||||
cy.get(statsValue).eq(6).should('have.text', '0');
|
||||
cy.get(statsValue).eq(7).should('have.text', '0');
|
||||
cy.get(statsValue).eq(8).should('have.text', '0');
|
||||
cy.get(statsValue).eq(9).should('not.be.empty');
|
||||
cy.get(statsValue).eq(10).should('not.be.empty');
|
||||
cy.get(statsValue).eq(11).should('not.be.empty');
|
||||
@@ -78,4 +86,75 @@ context('Home Page', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Git info', function () {
|
||||
it('git info is rendered on the footer of the page', function () {
|
||||
cy.getByTestId('git-info').within(() => {
|
||||
cy.getByTestId('git-network-data').within(() => {
|
||||
cy.contains('Reading network data from').should('be.visible');
|
||||
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
|
||||
cy.getByTestId('link').should('be.visible');
|
||||
});
|
||||
|
||||
cy.getByTestId('git-eth-data').within(() => {
|
||||
cy.contains('Reading Ethereum data from').should('be.visible');
|
||||
cy.get('span').should('have.text', Cypress.env('ethUrl'));
|
||||
});
|
||||
|
||||
cy.getByTestId('git-commit-hash').within(() => {
|
||||
cy.contains('Version/commit hash:').should('be.visible');
|
||||
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Search bar', function () {
|
||||
it('Successful search for specific id by block id', function () {
|
||||
const blockId = '973624';
|
||||
search(blockId);
|
||||
cy.url().should('include', blockId);
|
||||
});
|
||||
|
||||
it('Successful search for specific id by tx hash', function () {
|
||||
const txHash =
|
||||
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
|
||||
search(txHash);
|
||||
cy.url().should('include', txHash);
|
||||
});
|
||||
|
||||
it('Successful search for specific id by tx id', function () {
|
||||
const txId =
|
||||
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
|
||||
search(txId);
|
||||
cy.url().should('include', txId);
|
||||
});
|
||||
|
||||
it('Error message displayed when invalid search by wrong string length', function () {
|
||||
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
|
||||
validateSearchError("Something doesn't look right");
|
||||
});
|
||||
|
||||
it('Error message displayed when invalid search by invalid hash', function () {
|
||||
search(
|
||||
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
|
||||
);
|
||||
validateSearchError('Transaction is not hexadecimal');
|
||||
});
|
||||
|
||||
it('Error message displayed when searching empty field', function () {
|
||||
cy.get('[data-testid="search"]').clear();
|
||||
cy.get('[data-testid="search-button"]').click();
|
||||
validateSearchError('Search required');
|
||||
});
|
||||
|
||||
function search(searchTxt) {
|
||||
cy.get('[data-testid="search"]').clear().type(searchTxt);
|
||||
cy.get('[data-testid="search-button"]').click();
|
||||
}
|
||||
|
||||
function validateSearchError(expectedError) {
|
||||
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.rocks/
|
||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
||||
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.rocks
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.xyz
|
||||
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.xyz/rest
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
|
||||
|
||||
@@ -1,21 +1,87 @@
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Footer } from './components/footer/footer';
|
||||
import {
|
||||
AnnouncementBanner,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes/router-config';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
|
||||
const splashLoading = (
|
||||
<Splash>
|
||||
<Loader />
|
||||
</Splash>
|
||||
);
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MainnetSimAd = () => {
|
||||
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
|
||||
|
||||
// Return an empty div so that the grid layout in _app.page.ts
|
||||
// renders correctly
|
||||
if (!shouldDisplayBanner) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBanner>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
|
||||
<button
|
||||
className="flex items-center"
|
||||
onClick={() => setShouldDisplayBanner(false)}
|
||||
>
|
||||
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
|
||||
</button>
|
||||
<div>
|
||||
<span className="pr-4">Mainnet sim 3 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1500px] min-h-[100vh]',
|
||||
'mx-auto my-0',
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Header />
|
||||
<MainnetSimAd />
|
||||
</div>
|
||||
<div>
|
||||
<Main />
|
||||
</div>
|
||||
<div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Footer = () => {
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
() => ['lg', 'xl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ jest.mock('../search', () => ({
|
||||
}));
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter initialEntries={['/txs']}>
|
||||
<MemoryRouter>
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -24,7 +24,6 @@ describe('Header', () => {
|
||||
|
||||
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
|
||||
});
|
||||
|
||||
it('should render search', () => {
|
||||
render(renderComponent());
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { matchPath, useLocation, useMatch } from 'react-router-dom';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ThemeSwitcher,
|
||||
Navigation,
|
||||
@@ -13,26 +13,23 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import { isNavigable } from '../../routes/router-config';
|
||||
import { routerConfig } from '../../routes/router-config';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import { useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import { Search } from '../search';
|
||||
|
||||
const routeToNavigationItem = (r: Navigable) => (
|
||||
<NavigationItem key={r.handle.name}>
|
||||
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink>
|
||||
<NavigationItem key={r.name}>
|
||||
<NavigationLink to={r.path}>{r.text}</NavigationLink>
|
||||
</NavigationItem>
|
||||
);
|
||||
|
||||
export const Header = () => {
|
||||
const isHome = Boolean(useMatch(Routes.HOME));
|
||||
const pages = routerConfig[0].children || [];
|
||||
const mainItems = compact(
|
||||
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
|
||||
pages.find((r) => r.path === n)
|
||||
routerConfig.find((r) => r.path === n)
|
||||
)
|
||||
).filter(isNavigable);
|
||||
);
|
||||
|
||||
const groupedItems = compact(
|
||||
[
|
||||
@@ -42,8 +39,8 @@ export const Header = () => {
|
||||
Routes.GOVERNANCE,
|
||||
Routes.NETWORK_PARAMETERS,
|
||||
Routes.GENESIS,
|
||||
].map((n) => pages.find((r) => r.path === n))
|
||||
).filter(isNavigable);
|
||||
].map((n) => routerConfig.find((r) => r.path === n))
|
||||
);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -70,7 +67,7 @@ export const Header = () => {
|
||||
actions={
|
||||
<>
|
||||
<ThemeSwitcher />
|
||||
{!isHome && <Search />}
|
||||
<Search />
|
||||
</>
|
||||
}
|
||||
onResize={(width, el) => {
|
||||
@@ -93,7 +90,7 @@ export const Header = () => {
|
||||
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
|
||||
>
|
||||
{mainItems.map(routeToNavigationItem)}
|
||||
{groupedItems && groupedItems.length > 0 && (
|
||||
{groupedItems && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||
{t('Other')}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AppRouter } from '../../routes';
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<main className="p-4">
|
||||
<AppRouter />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -1,52 +1,143 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
|
||||
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
|
||||
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
InstrumentInfoPanel,
|
||||
KeyDetailsInfoPanel,
|
||||
LiquidityPriceRangeInfoPanel,
|
||||
MetadataInfoPanel,
|
||||
OracleInfoPanel,
|
||||
RiskFactorsInfoPanel,
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
} from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
MarketStateMapping,
|
||||
MarketTradingModeMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
const quoteUnit = market?.tradableInstrument.instrument.product.quoteName;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
const keyDetails = {
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
tradingMode: market.tradingMode,
|
||||
state: MarketStateMapping[market.state],
|
||||
};
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
keyDetails.tradingMode &&
|
||||
MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Instrument'),
|
||||
content: <InstrumentInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType:
|
||||
market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
|
||||
content: asset ? (
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={false}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Metadata'),
|
||||
content: <MetadataInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk model'),
|
||||
content: <RiskModelInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk parameters'),
|
||||
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk factors'),
|
||||
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(trigger, i) => ({
|
||||
@@ -67,10 +158,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{ referencePrice: trigger.referencePrice }}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
decimalPlaces={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
@@ -78,26 +166,64 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
{
|
||||
title: t('Liquidity monitoring parameters'),
|
||||
content: (
|
||||
<LiquidityMonitoringParametersInfoPanel
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
market={market}
|
||||
data={{
|
||||
triggeringRatio:
|
||||
market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity'),
|
||||
content: <LiquidityInfoPanel market={market} noBorder={false} />,
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel noBorder={false} market={market}>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
||||
}
|
||||
>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
@@ -110,7 +236,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</Link>
|
||||
</OracleInfoPanel>
|
||||
</MarketInfoTable>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -118,7 +244,7 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
|
||||
return (
|
||||
<>
|
||||
{panels.map((p) => (
|
||||
<div key={p.title} className="mb-3">
|
||||
<div className="mb-3">
|
||||
<h2 className="font-alpha calt text-xl">{p.title}</h2>
|
||||
{p.content}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface RouteErrorBoundaryProps {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
export class RouteErrorBoundary extends React.Component<
|
||||
RouteErrorBoundaryProps,
|
||||
{ hasError: boolean }
|
||||
> {
|
||||
constructor(props: RouteErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
override componentDidCatch(error: Error) {
|
||||
console.log(`Error caught in App error boundary ${error.message}`, error);
|
||||
}
|
||||
|
||||
override render() {
|
||||
if (this.state.hasError) {
|
||||
return <h1>{t('Something went wrong')}</h1>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,189 @@
|
||||
import {
|
||||
determineType,
|
||||
detectTypeByFetching,
|
||||
detectTypeFromQuery,
|
||||
getSearchType,
|
||||
isBlock,
|
||||
isHexadecimal,
|
||||
isNetworkParty,
|
||||
isNonHex,
|
||||
SearchTypes,
|
||||
isHash,
|
||||
toHex,
|
||||
toNonHex,
|
||||
} from './detect-search';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('Detect Search', () => {
|
||||
it.each([
|
||||
['0000000000000000000000000000000000000000000000000000000000000000', true],
|
||||
['0000000000000000000000000000000000000000000000000000000000000001', true],
|
||||
[
|
||||
'LOOONG0000000000000000000000000000000000000000000000000000000000000000',
|
||||
false,
|
||||
],
|
||||
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
|
||||
['something else', false],
|
||||
])("should detect that it's a hash", (input, expected) => {
|
||||
expect(isHash(input)).toBe(expected);
|
||||
it("should detect that it's a hexadecimal", () => {
|
||||
const expected = true;
|
||||
const testString =
|
||||
'0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
||||
const actual = isHexadecimal(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect that it's not hexadecimal", () => {
|
||||
const expected = true;
|
||||
const testString =
|
||||
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
|
||||
const actual = isNonHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect that it's a network party", () => {
|
||||
expect(isNetworkParty('network')).toBe(true);
|
||||
expect(isNetworkParty('web')).toBe(false);
|
||||
const expected = true;
|
||||
const testString = 'network';
|
||||
const actual = isNetworkParty(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect that it's a block", () => {
|
||||
expect(isBlock('123')).toBe(true);
|
||||
expect(isBlock('x123')).toBe(false);
|
||||
const expected = true;
|
||||
const testString = '3188';
|
||||
const actual = isBlock(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
SearchTypes.Transaction,
|
||||
],
|
||||
[
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
SearchTypes.Party,
|
||||
],
|
||||
['123', SearchTypes.Block],
|
||||
['network', SearchTypes.Party],
|
||||
['something else', SearchTypes.Unknown],
|
||||
])(
|
||||
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
|
||||
async (input, type) => {
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok:
|
||||
input ===
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: input,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await determineType(input);
|
||||
expect(result).toBe(type);
|
||||
}
|
||||
);
|
||||
it('should convert from non-hex to hex', () => {
|
||||
const expected = '0x123';
|
||||
const testString = '123';
|
||||
const actual = toHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should convert from hex to non-hex', () => {
|
||||
const expected = '123';
|
||||
const testString = '0x123';
|
||||
const actual = toNonHex(testString);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a hexadecimal", () => {
|
||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
const testString =
|
||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a non hex", () => {
|
||||
const expected = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
const testString =
|
||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a network party", () => {
|
||||
const expected = [SearchTypes.Party];
|
||||
const testString = 'network';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("should detect type client side from query if it's a block (number)", () => {
|
||||
const expected = [SearchTypes.Block];
|
||||
const testString = '23432';
|
||||
const actual = detectTypeFromQuery(testString);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
|
||||
const query = '0xabc';
|
||||
const type = SearchTypes.Transaction;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: query,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await detectTypeByFetching(query);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
|
||||
);
|
||||
expect(result).toBe(type);
|
||||
});
|
||||
|
||||
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
|
||||
const query = 'abc';
|
||||
const type = SearchTypes.Party;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await detectTypeByFetching(query);
|
||||
expect(result).toBe(type);
|
||||
});
|
||||
|
||||
it('getSearchType should return party from fetch response', async () => {
|
||||
const query =
|
||||
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const expected = SearchTypes.Party;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: false,
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return transaction from fetch response', async () => {
|
||||
const query =
|
||||
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
|
||||
const expected = SearchTypes.Transaction;
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: query,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return undefined from transaction response', async () => {
|
||||
const query = 'u';
|
||||
const expected = undefined;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return block if query is number', async () => {
|
||||
const query = '123';
|
||||
const expected = SearchTypes.Block;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('getSearchType should return party if query is network', async () => {
|
||||
const query = 'network';
|
||||
const expected = SearchTypes.Party;
|
||||
const result = await getSearchType(query);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
|
||||
|
||||
@@ -7,20 +6,15 @@ export enum SearchTypes {
|
||||
Party = 'party',
|
||||
Block = 'block',
|
||||
Order = 'order',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export const HASH_LENGTH = 64;
|
||||
|
||||
export const isHash = (value: string) =>
|
||||
/[0-9a-fA-F]+/.test(remove0x(value)) &&
|
||||
remove0x(value).length === HASH_LENGTH;
|
||||
export const TX_LENGTH = 64;
|
||||
|
||||
export const isHexadecimal = (search: string) =>
|
||||
search.startsWith('0x') && search.length === 2 + HASH_LENGTH;
|
||||
search.startsWith('0x') && search.length === 2 + TX_LENGTH;
|
||||
|
||||
export const isNonHex = (search: string) =>
|
||||
!search.startsWith('0x') && search.length === HASH_LENGTH;
|
||||
!search.startsWith('0x') && search.length === TX_LENGTH;
|
||||
|
||||
export const isBlock = (search: string) => !Number.isNaN(Number(search));
|
||||
|
||||
@@ -29,44 +23,121 @@ export const isNetworkParty = (search: string) => search === 'network';
|
||||
export const toHex = (query: string) =>
|
||||
isHexadecimal(query) ? query : `0x${query}`;
|
||||
|
||||
export const toNonHex = remove0x;
|
||||
export const toNonHex = (query: string) =>
|
||||
isNonHex(query) ? query : `${query.replace('0x', '')}`;
|
||||
|
||||
/**
|
||||
* Determine the type of the given query
|
||||
*/
|
||||
export const determineType = async (query: string): Promise<SearchTypes> => {
|
||||
const value = query.toLowerCase();
|
||||
if (isHash(value)) {
|
||||
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction`
|
||||
if (await isTransactionHash(value)) {
|
||||
return SearchTypes.Transaction;
|
||||
} else {
|
||||
return SearchTypes.Party;
|
||||
}
|
||||
} else if (isNetworkParty(value)) {
|
||||
return SearchTypes.Party;
|
||||
} else if (isBlock(value)) {
|
||||
return SearchTypes.Block;
|
||||
export const detectTypeFromQuery = (
|
||||
query: string
|
||||
): SearchTypes[] | undefined => {
|
||||
const i = query.toLowerCase();
|
||||
|
||||
if (isHexadecimal(i) || isNonHex(i)) {
|
||||
return [SearchTypes.Party, SearchTypes.Transaction];
|
||||
} else if (isNetworkParty(i)) {
|
||||
return [SearchTypes.Party];
|
||||
} else if (isBlock(i)) {
|
||||
return [SearchTypes.Block];
|
||||
}
|
||||
return SearchTypes.Unknown;
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if given input is a transaction hash by querying the transactions
|
||||
* endpoint
|
||||
*/
|
||||
export const isTransactionHash = async (input: string): Promise<boolean> => {
|
||||
const hash = remove0x(input);
|
||||
export const detectTypeByFetching = async (
|
||||
query: string
|
||||
): Promise<SearchTypes | undefined> => {
|
||||
const hash = toNonHex(query);
|
||||
const request = await fetch(
|
||||
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
||||
);
|
||||
|
||||
if (request?.ok) {
|
||||
const body: BlockExplorerTransaction = await request.json();
|
||||
|
||||
if (body?.transaction) {
|
||||
return true;
|
||||
return SearchTypes.Transaction;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return SearchTypes.Party;
|
||||
};
|
||||
|
||||
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
|
||||
// export const detectTypeByFetching = async (
|
||||
// query: string,
|
||||
// type: SearchTypes
|
||||
// ): Promise<SearchTypes | undefined> => {
|
||||
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
|
||||
//
|
||||
// if (!TYPES.includes(type)) {
|
||||
// throw new Error('Search type provided not recognised');
|
||||
// }
|
||||
//
|
||||
// if (type === SearchTypes.Transaction) {
|
||||
// const hash = toNonHex(query);
|
||||
// const request = await fetch(
|
||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
|
||||
// );
|
||||
//
|
||||
// if (request?.ok) {
|
||||
// const body: BlockExplorerTransaction = await request.json();
|
||||
//
|
||||
// if (body?.transaction) {
|
||||
// return SearchTypes.Transaction;
|
||||
// }
|
||||
// }
|
||||
// } else if (type === SearchTypes.Party) {
|
||||
// const party = toNonHex(query);
|
||||
//
|
||||
// const request = await fetch(
|
||||
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
|
||||
// );
|
||||
//
|
||||
// if (request.ok) {
|
||||
// const body: BlockExplorerTransactions = await request.json();
|
||||
//
|
||||
// if (body?.transactions?.length) {
|
||||
// return SearchTypes.Party;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return undefined;
|
||||
// };
|
||||
|
||||
// export const getSearchType = async (
|
||||
// query: string
|
||||
// ): Promise<SearchTypes | undefined> => {
|
||||
// const searchTypes = detectTypeFromQuery(query);
|
||||
// const hasResults = searchTypes?.length;
|
||||
//
|
||||
// if (hasResults) {
|
||||
// if (hasResults > 1) {
|
||||
// const promises = searchTypes.map((type) =>
|
||||
// detectTypeByFetching(query, type)
|
||||
// );
|
||||
// const results = await Promise.all(promises);
|
||||
// return results.find((result) => result !== undefined);
|
||||
// }
|
||||
//
|
||||
// return searchTypes[0];
|
||||
// }
|
||||
//
|
||||
// return undefined;
|
||||
// };
|
||||
|
||||
export const getSearchType = async (
|
||||
query: string
|
||||
): Promise<SearchTypes | undefined> => {
|
||||
const searchTypes = detectTypeFromQuery(query);
|
||||
const hasResults = searchTypes?.length;
|
||||
|
||||
if (hasResults) {
|
||||
if (hasResults > 1) {
|
||||
return await detectTypeByFetching(query);
|
||||
}
|
||||
|
||||
return searchTypes[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -1,82 +1,157 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { SearchForm } from './search';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { Search } from './search';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { SearchTypes, getSearchType } from './detect-search';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
const mockedNavigate = jest.fn();
|
||||
const mockGetSearchType = getSearchType as jest.MockedFunction<
|
||||
typeof getSearchType
|
||||
>;
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
jest.mock('./detect-search', () => ({
|
||||
...jest.requireActual('./detect-search'),
|
||||
getSearchType: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mockedNavigate.mockClear();
|
||||
});
|
||||
|
||||
const renderComponent = () =>
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<SearchForm />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Search />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
describe('SearchForm', () => {
|
||||
const getInputs = () => ({
|
||||
input: screen.getByTestId('search'),
|
||||
button: screen.getByTestId('search-button'),
|
||||
});
|
||||
|
||||
describe('Search', () => {
|
||||
it('should render search input and button', () => {
|
||||
renderComponent();
|
||||
render(renderComponent());
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('search-button')).toHaveTextContent('Search');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
Routes.TX,
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
],
|
||||
[
|
||||
Routes.PARTIES,
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
],
|
||||
[Routes.BLOCKS, '123'],
|
||||
[undefined, 'something else'],
|
||||
])('should redirect to %s', async (route, input) => {
|
||||
// @ts-ignore issue related to polyfill
|
||||
fetch.mockImplementation(
|
||||
jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok:
|
||||
input ===
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
transaction: {
|
||||
hash: input,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
it('should render error if input is not known', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
fireEvent.change(input, { target: { value: 'asd' } });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Transaction type is not recognised'
|
||||
);
|
||||
renderComponent();
|
||||
await act(async () => {
|
||||
fireEvent.change(screen.getByTestId('search'), {
|
||||
target: {
|
||||
value: input,
|
||||
},
|
||||
});
|
||||
fireEvent.click(screen.getByTestId('search-button'));
|
||||
});
|
||||
|
||||
it('should render error if no input is given', async () => {
|
||||
render(renderComponent());
|
||||
const { button } = getInputs();
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(await screen.findByTestId('search-error')).toHaveTextContent(
|
||||
'Search query required'
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to transactions page', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledTimes(route ? 1 : 0);
|
||||
if (route) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(mockedNavigate).toBeCalledWith(`${route}/${input}`);
|
||||
}
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to transactions page without proceeding 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Transaction);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.TX}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to parties page', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'0x1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to parties page without proceeding 0x', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Party);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value:
|
||||
'1234567890123456789012345678901234567890123456789012345678901234',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
`${Routes.PARTIES}/0x1234567890123456789012345678901234567890123456789012345678901234`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to blocks page if passed a number', async () => {
|
||||
render(renderComponent());
|
||||
const { button, input } = getInputs();
|
||||
mockGetSearchType.mockResolvedValue(SearchTypes.Block);
|
||||
fireEvent.change(input, {
|
||||
target: {
|
||||
value: '123',
|
||||
},
|
||||
});
|
||||
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => {
|
||||
expect(mockedNavigate).toBeCalledWith(`${Routes.BLOCKS}/123`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getSearchType, SearchTypes, toHex } from './detect-search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { determineType, isBlock, isHash, SearchTypes } from './detect-search';
|
||||
|
||||
interface FormFields {
|
||||
search: string;
|
||||
@@ -31,26 +30,101 @@ const MagnifyingGlass = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
const Clear = () => (
|
||||
<svg
|
||||
className="w-3 h-3"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.3748 1.37478L1.37478 11.3748L0.625244 10.6252L10.6252 0.625244L11.3748 1.37478Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.37478 0.625244L11.3748 10.6252L10.6252 11.3748L0.625244 1.37478L1.37478 0.625244Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const Search = () => {
|
||||
const searchForm = <SearchForm />;
|
||||
const { register, handleSubmit } = useForm<FormFields>();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (fields: FormFields) => {
|
||||
setError(null);
|
||||
|
||||
const query = fields.search;
|
||||
|
||||
if (!query) {
|
||||
return setError(new Error(t('Search query required')));
|
||||
}
|
||||
|
||||
const result = await getSearchType(query);
|
||||
const urlAsHex = toHex(query);
|
||||
const unrecognisedError = new Error(
|
||||
t('Transaction type is not recognised')
|
||||
);
|
||||
|
||||
if (result) {
|
||||
switch (result) {
|
||||
case SearchTypes.Party:
|
||||
return navigate(`${Routes.PARTIES}/${urlAsHex}`);
|
||||
case SearchTypes.Transaction:
|
||||
return navigate(`${Routes.TX}/${urlAsHex}`);
|
||||
case SearchTypes.Block:
|
||||
return navigate(`${Routes.BLOCKS}/${Number(query)}`);
|
||||
default:
|
||||
return setError(unrecognisedError);
|
||||
}
|
||||
}
|
||||
|
||||
return setError(unrecognisedError);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const searchForm = (
|
||||
<form className="block min-w-[290px]" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex relative items-stretch gap-2 text-xs">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute top-[50%] translate-y-[-50%] left-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
<Input
|
||||
{...register('search')}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className={classNames(
|
||||
'peer',
|
||||
'pl-8 py-2 text-xs',
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200'
|
||||
)}
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
placeholder={t('Enter block number, public key or transaction hash')}
|
||||
/>
|
||||
{error?.message && (
|
||||
<div
|
||||
className={classNames(
|
||||
'hidden peer-focus:block',
|
||||
'bg-white dark:bg-black',
|
||||
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
|
||||
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
|
||||
)}
|
||||
>
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="text-xs"
|
||||
>
|
||||
{error.message}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="hidden [.search-dropdown_&]:block"
|
||||
type="submit"
|
||||
size="xs"
|
||||
data-testid="search-button"
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
const searchTrigger = (
|
||||
<DropdownMenu.Root>
|
||||
@@ -80,135 +154,10 @@ export const Search = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="hidden [.nav-search-full_&]:block min-w-[290px]">
|
||||
{searchForm}
|
||||
</div>
|
||||
<div className="hidden [.nav-search-full_&]:block">{searchForm}</div>
|
||||
<div className="hidden [.nav-search-compact_&]:block">
|
||||
{searchTrigger}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchForm = () => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState,
|
||||
watch,
|
||||
} = useForm<FormFields>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (fields: FormFields) => {
|
||||
clearErrors();
|
||||
const type = await determineType(fields.search);
|
||||
if (type) {
|
||||
switch (type) {
|
||||
case SearchTypes.Party:
|
||||
return navigate(`${Routes.PARTIES}/${remove0x(fields.search)}`);
|
||||
case SearchTypes.Transaction:
|
||||
return navigate(`${Routes.TX}/${remove0x(fields.search)}`);
|
||||
case SearchTypes.Block:
|
||||
return navigate(`${Routes.BLOCKS}/${Number(fields.search)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setError('search', new Error(t('The search term is not a valid query')));
|
||||
},
|
||||
[clearErrors, navigate, setError]
|
||||
);
|
||||
|
||||
const searchQuery = watch('search', '');
|
||||
|
||||
return (
|
||||
<form className="block min-w-[200px]" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex relative items-stretch gap-2 text-xs">
|
||||
<div className="relative w-full">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute top-[50%] translate-y-[-50%] left-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<MagnifyingGlass />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setValue('search', '');
|
||||
clearErrors();
|
||||
}}
|
||||
className={classNames(
|
||||
{ hidden: searchQuery.length === 0 },
|
||||
'absolute top-[50%] translate-y-[-50%] right-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
>
|
||||
<Clear />
|
||||
</button>
|
||||
<Input
|
||||
{...register('search', {
|
||||
required: t('Search query is required'),
|
||||
validate: (value) =>
|
||||
isHash(value) ||
|
||||
isBlock(value) ||
|
||||
t('Search query has to be a number or a 64 character hash'),
|
||||
onBlur: () => clearErrors('search'),
|
||||
})}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className={classNames(
|
||||
'pl-8 py-2 text-xs',
|
||||
{ 'pr-8': searchQuery.length > 1 },
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200',
|
||||
{
|
||||
'border-vega-pink dark:border-vega-pink': Boolean(
|
||||
formState.errors.search
|
||||
),
|
||||
}
|
||||
)}
|
||||
hasError={Boolean(formState.errors.search)}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
'Enter block number, public key or transaction hash'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{formState.errors.search && (
|
||||
<div
|
||||
className={classNames(
|
||||
'[nav_&]:border [nav_&]:rounded [nav_&]:border-vega-light-300 [nav_&]:dark:border-vega-light-300',
|
||||
'[.search-dropdown_&]:border [.search-dropdown_&]:rounded [.search-dropdown_&]:border-vega-light-300 [.search-dropdown_&]:dark:border-vega-light-300',
|
||||
'bg-white dark:bg-black',
|
||||
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
|
||||
)}
|
||||
>
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="text-xs"
|
||||
>
|
||||
{formState.errors.search.message}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
size="xs"
|
||||
data-testid="search-button"
|
||||
className="[nav_&]:hidden"
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { StatsManager } from '@vegaprotocol/network-stats';
|
||||
import { SearchForm } from '../../components/search';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
const Home = () => {
|
||||
const classnames = 'mt-4 mb-4';
|
||||
const classnames = 'mt-4 grid grid-cols-1 lg:grid-cols-2 lg:gap-4';
|
||||
|
||||
useDocumentTitle();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="p-20 max-sm:py-10 max-sm:px-0">
|
||||
<SearchForm />
|
||||
</div>
|
||||
<div className="px-20 max-sm:px-0">
|
||||
<StatsManager className={classnames} />
|
||||
</div>
|
||||
<StatsManager className={classnames} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import { RouteErrorBoundary } from '../components/router-error-boundary';
|
||||
|
||||
import routerConfig from './router-config';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface RouteChildProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const AppRouter = () => {
|
||||
const routes = useRoutes(routerConfig);
|
||||
|
||||
const splashLoading = (
|
||||
<Splash>
|
||||
<Loader />
|
||||
</Splash>
|
||||
);
|
||||
|
||||
return (
|
||||
<RouteErrorBoundary>
|
||||
<React.Suspense fallback={splashLoading}>{routes}</React.Suspense>
|
||||
</RouteErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -1,187 +0,0 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
AnnouncementBanner,
|
||||
BackgroundVideo,
|
||||
BreadcrumbsContainer,
|
||||
ButtonLink,
|
||||
ExternalLink,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Link,
|
||||
Outlet,
|
||||
useMatch,
|
||||
useRouteError,
|
||||
} from 'react-router-dom';
|
||||
import { Footer } from '../components/footer/footer';
|
||||
import { Header } from '../components/header';
|
||||
import { Routes } from './route-names';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MainnetSimAd = () => {
|
||||
const [shouldDisplayBanner, setShouldDisplayBanner] = useState<boolean>(true);
|
||||
|
||||
// Return an empty div so that the grid layout in _app.page.ts
|
||||
// renders correctly
|
||||
if (!shouldDisplayBanner) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnnouncementBanner>
|
||||
<div className="grid grid-cols-[auto_1fr] gap-4 font-alpha calt uppercase text-center text-lg text-white">
|
||||
<button
|
||||
className="flex items-center"
|
||||
onClick={() => setShouldDisplayBanner(false)}
|
||||
>
|
||||
<Icon name="cross" className="w-6 h-6" ariaLabel="dismiss" />
|
||||
</button>
|
||||
<div>
|
||||
<span className="pr-4">Mainnet sim 3 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
);
|
||||
};
|
||||
|
||||
export const Layout = () => {
|
||||
const isHome = Boolean(useMatch(Routes.HOME));
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1500px] min-h-[100vh]',
|
||||
'mx-auto my-0',
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<MainnetSimAd />
|
||||
<Header />
|
||||
</div>
|
||||
<div>
|
||||
<main className="p-4">
|
||||
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<div>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorBoundary = () => {
|
||||
const error = useRouteError();
|
||||
|
||||
const errorTitle = isRouteErrorResponse(error)
|
||||
? `${error.status} ${error.statusText}`
|
||||
: t('Something went wrong');
|
||||
|
||||
const errorMessage = isRouteErrorResponse(error)
|
||||
? error.error?.message
|
||||
: (error as Error).message || JSON.stringify(error);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BackgroundVideo className="brightness-50" />
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[620px] p-2 mt-[10vh]',
|
||||
'mx-auto my-0',
|
||||
'antialiased text-white',
|
||||
'overflow-hidden relative',
|
||||
'flex flex-col gap-2'
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-4">
|
||||
<div>{GHOST}</div>
|
||||
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
|
||||
{errorTitle}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-sm mt-10 overflow-auto break-all font-mono">
|
||||
{errorMessage}
|
||||
</div>
|
||||
<div>
|
||||
<ButtonLink onClick={() => window.location.reload()}>
|
||||
{t('Try refreshing')}
|
||||
</ButtonLink>{' '}
|
||||
{t('or go back to')}{' '}
|
||||
<Link className="underline" to="/">
|
||||
{t('Home')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
viewBox="0 0 56 85"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M41 0.5H3V60.5H41V0.5Z" fill="white" />
|
||||
<path d="M15 18.5H13V20.5H15V18.5Z" fill="black" />
|
||||
<path d="M17 20.5H15V22.5H17V20.5Z" fill="black" />
|
||||
<path d="M19 18.5H17V20.5H19V18.5Z" fill="black" />
|
||||
<path d="M15 22.5H13V24.5H15V22.5Z" fill="black" />
|
||||
<path d="M19 22.5H17V24.5H19V22.5Z" fill="black" />
|
||||
<path d="M29 28.5H15V30.5H29V28.5Z" fill="black" />
|
||||
<path d="M27 18.5H25V20.5H27V18.5Z" fill="black" />
|
||||
<path d="M29 20.5H27V22.5H29V20.5Z" fill="black" />
|
||||
<path d="M31 18.5H29V20.5H31V18.5Z" fill="black" />
|
||||
<path d="M27 22.5H25V24.5H27V22.5Z" fill="black" />
|
||||
<path d="M31 22.5H29V24.5H31V22.5Z" fill="black" />
|
||||
<path d="M31 26.5H29V28.5H31V26.5Z" fill="black" />
|
||||
<path d="M19 60.5H17V84.5H19V60.5Z" fill="black" />
|
||||
<path d="M27 60.5H25V84.5H27V60.5Z" fill="black" />
|
||||
<path
|
||||
d="M3 42.5V58.64V60.5V64.5H21V60.5H23V64.5H41V60.5V58.64V42.5H3Z"
|
||||
fill="#FF077F"
|
||||
/>
|
||||
<path d="M35 46.5H41V42.5H3V46.5H31H35Z" fill="#CB0666" />
|
||||
<path d="M3 32.32V29.5L0 32.5V60.5H2V33.33L3 32.32Z" fill="black" />
|
||||
<path d="M41 31.8V29.49L54.79 21.53L55.79 23.26L41 31.8Z" fill="black" />
|
||||
<path d="M36 54.5H35V55.5H36V54.5Z" fill="black" />
|
||||
<path d="M35 53.5H34V54.5H35V53.5Z" fill="black" />
|
||||
<path d="M34 48.5H33V53.5H34V48.5Z" fill="black" />
|
||||
<path d="M38 48.5H37V52.5H38V48.5Z" fill="black" />
|
||||
<path d="M37 53.5H36V54.5H37V53.5Z" fill="black" />
|
||||
<path d="M39 52.5H38V53.5H39V52.5Z" fill="black" />
|
||||
<path
|
||||
d="M55.7901 23.27L53.0601 22.54L45.1001 8.75L46.8301 7.75L55.7901 23.27Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
|
||||
import { marketInfoProvider } from '@vegaprotocol/market-info';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
|
||||
export const MarketPage = () => {
|
||||
@@ -17,7 +17,7 @@ export const MarketPage = () => {
|
||||
const { marketId } = useParams<{ marketId: string }>();
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketInfoWithDataProvider,
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: marketId || '',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Oracle } from './oracles/id';
|
||||
import Party from './parties';
|
||||
import { Parties } from './parties/home';
|
||||
import { Party as PartySingle } from './parties/id';
|
||||
import Txs from './txs';
|
||||
import { ValidatorsPage } from './validators';
|
||||
import Genesis from './genesis';
|
||||
import { Block } from './blocks/id';
|
||||
@@ -19,55 +20,23 @@ import flags from '../config/flags';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
import type { Params, RouteObject } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ErrorBoundary, Layout } from './layout';
|
||||
import compact from 'lodash/compact';
|
||||
import { AssetLink, MarketLink } from '../components/links';
|
||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
handle: {
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
export const isNavigable = (item: RouteObject): item is Navigable =>
|
||||
(item as Navigable).path !== undefined &&
|
||||
(item as Navigable).handle !== undefined &&
|
||||
(item as Navigable).handle.name !== undefined &&
|
||||
(item as Navigable).handle.text !== undefined;
|
||||
|
||||
export type Breadcrumbable = {
|
||||
handle: { breadcrumb: (data?: Params<string>) => ReactNode | string };
|
||||
};
|
||||
export const isBreadcrumbable = (item: RouteObject): item is Breadcrumbable =>
|
||||
(item as Breadcrumbable).handle !== undefined &&
|
||||
(item as Breadcrumbable).handle.breadcrumb !== undefined;
|
||||
|
||||
type RouteItem =
|
||||
| RouteObject
|
||||
| (RouteObject & Navigable)
|
||||
| (RouteObject & Breadcrumbable);
|
||||
type Route = RouteItem & {
|
||||
children?: RouteItem[];
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
type Route = RouteObject & Navigable;
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
element: <Party />,
|
||||
handle: {
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -76,13 +45,6 @@ const partiesRoutes: Route[] = flags.parties
|
||||
{
|
||||
path: ':party',
|
||||
element: <PartySingle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -93,11 +55,8 @@ const assetsRoutes: Route[] = flags.assets
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
handle: {
|
||||
name: t('Assets'),
|
||||
text: t('Assets'),
|
||||
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
|
||||
},
|
||||
text: t('Assets'),
|
||||
name: t('Assets'),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -106,11 +65,6 @@ const assetsRoutes: Route[] = flags.assets
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<AssetLink assetId={params.assetId as string} />
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -121,13 +75,8 @@ const genesisRoutes: Route[] = flags.genesis
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
handle: {
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
|
||||
),
|
||||
},
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
element: <Genesis />,
|
||||
},
|
||||
]
|
||||
@@ -137,13 +86,8 @@ const governanceRoutes: Route[] = flags.governance
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
handle: {
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
|
||||
),
|
||||
},
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
element: <Proposals />,
|
||||
},
|
||||
]
|
||||
@@ -153,11 +97,8 @@ const marketsRoutes: Route[] = flags.markets
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
handle: {
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
|
||||
},
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -166,11 +107,6 @@ const marketsRoutes: Route[] = flags.markets
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<MarketLink id={params.marketId as string} />
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -181,15 +117,8 @@ const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
handle: {
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.NETWORK_PARAMETERS}>
|
||||
{t('Network Parameters')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
element: <NetworkParameters />,
|
||||
},
|
||||
]
|
||||
@@ -199,133 +128,80 @@ const validators: Route[] = flags.validators
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
handle: {
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
|
||||
),
|
||||
},
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const linkTo = (...segments: (string | undefined)[]) =>
|
||||
compact(segments).join('/');
|
||||
|
||||
export const routerConfig: Route[] = [
|
||||
const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
element: <Layout />,
|
||||
handle: {
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
|
||||
},
|
||||
errorElement: <ErrorBoundary />,
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
element: <Home />,
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
element: <Txs />,
|
||||
children: [
|
||||
{
|
||||
path: 'pending',
|
||||
element: <PendingTxs />,
|
||||
},
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />,
|
||||
element: <Blocks />,
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
handle: {
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'pending',
|
||||
element: <PendingTxs />,
|
||||
handle: {
|
||||
breadcrumb: () => (
|
||||
<Link to={linkTo(Routes.TX, 'pending')}>
|
||||
{t('Pending transactions')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.TX, params.txHash)}>
|
||||
{truncateMiddle(remove0x(params.txHash as string))}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
handle: {
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
|
||||
},
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Blocks />,
|
||||
},
|
||||
{
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.BLOCKS, params.block)}>
|
||||
{params.block}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
handle: {
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
|
||||
},
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.ORACLES, params.id)}>
|
||||
{truncateMiddle(params.id as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
},
|
||||
],
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
];
|
||||
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
export default routerConfig;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
@@ -7,6 +8,9 @@ import { TxDetails } from './tx-details';
|
||||
import type { BlockExplorerTransaction } from '../../../routes/types/block-explorer-response';
|
||||
import { toNonHex } from '../../../components/search/detect-search';
|
||||
import { PageHeader } from '../../../components/page-header';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
|
||||
const Tx = () => {
|
||||
@@ -31,6 +35,17 @@ const Tx = () => {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Link
|
||||
className="font-normal underline underline-offset-4 block mb-5"
|
||||
to={`/${Routes.TX}`}
|
||||
>
|
||||
<Icon
|
||||
className="text-vega-light-150 dark:text-vega-light-150"
|
||||
name={IconNames.CHEVRON_LEFT}
|
||||
/>
|
||||
All Transactions
|
||||
</Link>
|
||||
|
||||
<PageHeader
|
||||
title="transaction"
|
||||
truncateStart={5}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { PageTitle } from '../../components/page-helpers/page-title';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
EtherscanLink,
|
||||
ContractAddressLink,
|
||||
DApp,
|
||||
TOKEN_VALIDATOR,
|
||||
useLinks,
|
||||
@@ -224,7 +224,7 @@ export const ValidatorsPage = () => {
|
||||
<KeyValueTableRow>
|
||||
<div>{t('Ethereum address')}</div>
|
||||
<div className="break-all text-xs">
|
||||
<EtherscanLink address={v.ethereumAddress} />{' '}
|
||||
<ContractAddressLink address={v.ethereumAddress} />{' '}
|
||||
<CopyWithTooltip text={v.ethereumAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import './styles.css';
|
||||
|
||||
import App from './app/app';
|
||||
@@ -9,6 +10,8 @@ const root = rootElement && createRoot(rootElement);
|
||||
|
||||
root?.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
"changes": {
|
||||
"decimalPlaces": "5",
|
||||
"positionDecimalPlaces": "5",
|
||||
"linearSlippageFactor": "0.001",
|
||||
"quadraticSlippageFactor": "0",
|
||||
"lpPriceRange": "10",
|
||||
"instrument": {
|
||||
"name": "Token test market",
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"lpPriceRange": "10",
|
||||
"linearSlippageFactor": "0.001",
|
||||
"quadraticSlippageFactor": "0",
|
||||
"instrument": {
|
||||
"code": "TEST.24h",
|
||||
"future": {
|
||||
|
||||
@@ -110,9 +110,10 @@ Cypress.Commands.add('vega_wallet_teardown', function () {
|
||||
}
|
||||
});
|
||||
cy.get(vegaWalletContainer).within(() => {
|
||||
cy.get('[data-testid="vega-wallet-balance-unstaked"]', {
|
||||
timeout: 30000,
|
||||
}).should('contain.text', '0.00');
|
||||
cy.get('[data-testid="associated-amount"]', { timeout: 30000 }).should(
|
||||
'contain.text',
|
||||
'0.00'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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.rocks/
|
||||
NX_VEGA_REST=https://api.validators-testnet.vega.xyz/
|
||||
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
|
||||
|
||||
@@ -21,9 +21,9 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
|
||||
<>
|
||||
<AnnouncementBanner>
|
||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||
<span className="pr-4">Wait no longer, SIM3 is here!</span>
|
||||
<ExternalLink href="https://fairground.wtf/sim3">
|
||||
Learn more
|
||||
<span className="pr-4">Mainnet sim 3 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Come help stress test the network
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
|
||||
+2
-7
@@ -2,7 +2,7 @@ import type { Dispatch, SetStateAction } from 'react';
|
||||
import { forwardRef, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
@@ -87,12 +87,7 @@ const TopThirdCellRenderer = (
|
||||
<div className="mb-4">
|
||||
<Button
|
||||
data-testid="show-all-validators"
|
||||
rightIcon={
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
className="fill-current mr-2 align-text-top"
|
||||
/>
|
||||
}
|
||||
rightIcon="arrow-right"
|
||||
className="inline-flex items-center"
|
||||
>
|
||||
{t('Reveal top validators')}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
Link,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
@@ -10,7 +11,7 @@ import { useParams } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { TrancheItem } from '../redemption/tranche-item';
|
||||
import Routes from '../routes';
|
||||
import { TrancheLabel } from './tranche-label';
|
||||
@@ -18,6 +19,7 @@ import { useTranches } from '../../lib/tranches/tranches-store';
|
||||
|
||||
export const Tranche = () => {
|
||||
const tranches = useTranches((state) => state.tranches);
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const { t } = useTranslation();
|
||||
const { trancheId } = useParams<{ trancheId: string; address: string }>();
|
||||
const { chainId } = useWeb3React();
|
||||
@@ -57,15 +59,25 @@ export const Tranche = () => {
|
||||
</KeyValueTableRow>
|
||||
{tranche.users.map((user) => (
|
||||
<KeyValueTableRow key={user}>
|
||||
<EtherscanLink address={user} data-testid="link" />
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
{
|
||||
<Link
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
href={`${ETHERSCAN_URL}/address/${user}`}
|
||||
target="_blank"
|
||||
>
|
||||
{user}
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
<RouterLink
|
||||
className="underline"
|
||||
title={t('View vesting information')}
|
||||
to={`${Routes.REDEEM}/${user}`}
|
||||
data-testid="redeem-link"
|
||||
>
|
||||
{t('View vesting information')}
|
||||
</RouterLink>
|
||||
}
|
||||
</KeyValueTableRow>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
import { Indicator } from '../indicator';
|
||||
import type { AuctionTrigger } from '@vegaprotocol/types';
|
||||
|
||||
export const Status = ({
|
||||
tradingMode,
|
||||
@@ -32,8 +31,7 @@ export const Status = ({
|
||||
};
|
||||
|
||||
const status = getStatus();
|
||||
const tooltipDescription =
|
||||
tradingMode && getTooltipDescription(tradingMode, trigger);
|
||||
const tooltipDescription = t(getTooltipDescription(status));
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -54,43 +52,24 @@ export const Status = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getTooltipDescription = (
|
||||
status: Schema.MarketTradingMode,
|
||||
trigger?: Schema.AuctionTrigger
|
||||
) => {
|
||||
const getTooltipDescription = (status: string) => {
|
||||
let tooltipDescription = '';
|
||||
switch (status) {
|
||||
case Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS:
|
||||
return t(
|
||||
'This is the standard trading mode where trades are executed whenever orders are received'
|
||||
);
|
||||
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION:
|
||||
return getMonitoringDescriptionTooltip(trigger);
|
||||
case Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION:
|
||||
return t(
|
||||
'This is a new market in an opening auction to determine a fair mid-price before starting continuous trading.'
|
||||
);
|
||||
case Schema.MarketTradingModeMapping.TRADING_MODE_CONTINUOUS:
|
||||
tooltipDescription =
|
||||
'This is the standard trading mode where trades are executed whenever orders are received';
|
||||
break;
|
||||
case `${Schema.MarketTradingModeMapping.TRADING_MODE_MONITORING_AUCTION} - ${Schema.AuctionTriggerMapping.AUCTION_TRIGGER_LIQUIDITY}`:
|
||||
tooltipDescription =
|
||||
'This market is in auction until it reaches sufficient liquidity';
|
||||
break;
|
||||
case Schema.MarketTradingModeMapping.TRADING_MODE_OPENING_AUCTION:
|
||||
tooltipDescription =
|
||||
'This is a new market in an opening auction to determine a fair mid-price before starting continuous trading.';
|
||||
break;
|
||||
default:
|
||||
return '';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const getMonitoringDescriptionTooltip = (trigger?: AuctionTrigger) => {
|
||||
switch (trigger) {
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET:
|
||||
return t(
|
||||
`This market is in auction until it reaches sufficient liquidity.`
|
||||
);
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS:
|
||||
return t(
|
||||
`This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.`
|
||||
);
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE:
|
||||
return t(`This market is in auction due to high price volatility.`);
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING:
|
||||
return t(
|
||||
`This is a new market in an opening auction to determine a fair mid-price before starting continuous trading`
|
||||
);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
return tooltipDescription;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api-n04.d.vega.rocks/graphql"]
|
||||
"hosts": ["https://api.n04.d.vega.xyz/graphql"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api-n00.mainnet-mirror.vega.rocks/graphql"]
|
||||
"hosts": ["https://api.n00.mainnet-mirror.vega.xyz/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api-n01.sandbox.vega.rocks/graphql"]
|
||||
"hosts": ["https://api.n01.sandbox.vega.xyz/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api-n00.stagnet1.vega.rocks/graphql"]
|
||||
"hosts": ["https://api.n00.stagnet1.vega.xyz/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"hosts": ["https://api-stagnet3.vega.rocks/graphql"]
|
||||
"hosts": ["https://api.stagnet3.vega.xyz/graphql"]
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"hosts": [
|
||||
"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"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@ const externalLink = 'external-link';
|
||||
const accordionContent = 'accordion-content';
|
||||
|
||||
describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
});
|
||||
|
||||
before(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
@@ -75,8 +71,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
validateMarketDataRow(3, 'Quote Name', 'BTC');
|
||||
});
|
||||
|
||||
// need to check why data are not visible
|
||||
it.skip('settlement asset displayed', () => {
|
||||
it('settlement asset displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Settlement asset').click();
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT');
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('Market proposal notification', { tags: '@smoke' }, () => {
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(
|
||||
@@ -62,7 +62,7 @@ describe('Market trading page', () => {
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -148,7 +148,7 @@ describe('Market trading page', () => {
|
||||
cy.getByTestId(itemHeader).should('have.text', 'Trading mode');
|
||||
cy.getByTestId(itemValue).should(
|
||||
'have.text',
|
||||
'Monitoring auction - liquidity (target not met)'
|
||||
'Monitoring auction - liquidity'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -216,60 +216,6 @@ 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 () {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
|
||||
@@ -161,7 +161,7 @@ describe(
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -230,7 +230,7 @@ describe(
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -299,7 +299,7 @@ describe(
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
@@ -585,7 +585,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_SUSPENDED,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY
|
||||
);
|
||||
const accounts = accountsQuery();
|
||||
cy.mockGQL((req) => {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
+39
-19
@@ -1,8 +1,7 @@
|
||||
import {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from '@vegaprotocol/cypress';
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
import { connectEthereumWallet } from '../support/ethereum-wallet';
|
||||
|
||||
const connectEthWalletBtn = 'connect-eth-wallet-btn';
|
||||
const connectVegaBtn = 'connect-vega-wallet';
|
||||
const manageVegaBtn = 'manage-vega-wallet';
|
||||
const form = 'rest-connector-form';
|
||||
@@ -128,21 +127,6 @@ 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
|
||||
@@ -180,3 +164,39 @@ 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');
|
||||
});
|
||||
});
|
||||
@@ -9,4 +9,3 @@ NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -8,4 +8,3 @@ NX_VEGA_TOKEN_URL=https://token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -9,4 +9,3 @@ NX_VEGA_TOKEN_URL=https://mainnet-mirror.token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -7,4 +7,3 @@ NX_VEGA_NETWORKS={\"DEVNET\":\"https://dev.token.vega.xyz\",\"STAGNET3\":\"https
|
||||
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -9,4 +9,3 @@ NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -9,4 +9,3 @@ NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -9,4 +9,3 @@ NX_VEGA_TOKEN_URL=https://token.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
@@ -9,4 +9,4 @@ NX_VEGA_TOKEN_URL=https://validator-testnet.governance.fairground.wtf
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ export const LiquidityViewContainer = ({
|
||||
<div className="break-word">{marketId}</div>
|
||||
</HeaderStat>
|
||||
</Header>
|
||||
<Tabs defaultValue={getActiveDefaultId()}>
|
||||
<Tabs active={getActiveDefaultId()}>
|
||||
<Tab
|
||||
id={LiquidityTabs.MyLiquidityProvision}
|
||||
name={t('My liquidity provision')}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
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,8 +61,7 @@ export const MarketPage = () => {
|
||||
const { marketId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
|
||||
const { w } = useWindowSize();
|
||||
const update = useGlobalStore((store) => store.update);
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
@@ -88,7 +87,7 @@ export const MarketPage = () => {
|
||||
}, [update, lastMarketId, data?.id]);
|
||||
|
||||
const tradeView = useMemo(() => {
|
||||
if (largeScreen) {
|
||||
if (w > 960) {
|
||||
return (
|
||||
<TradeGrid
|
||||
market={data}
|
||||
@@ -106,7 +105,7 @@ export const MarketPage = () => {
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
);
|
||||
}, [largeScreen, data, onSelect, navigate]);
|
||||
}, [w, data, onSelect, navigate]);
|
||||
if (!data && marketId) {
|
||||
return (
|
||||
<Splash>
|
||||
@@ -141,3 +140,37 @@ 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;
|
||||
};
|
||||
|
||||
@@ -8,13 +8,13 @@ import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode, ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import {
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
Tabs,
|
||||
ResizableGrid,
|
||||
ResizableGridPanel,
|
||||
Splash,
|
||||
@@ -29,7 +29,6 @@ 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
|
||||
@@ -70,204 +69,129 @@ interface TradeGridProps {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
interface BottomPanelProps {
|
||||
const MainGrid = ({
|
||||
marketId,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: {
|
||||
marketId: string;
|
||||
onSelect?: (marketId: string) => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
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 storageKey="console-trade-grid-orders">
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Orders
|
||||
}) => {
|
||||
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
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Fills
|
||||
</Tab>
|
||||
<Tab id="info" name={t('Info')}>
|
||||
<TradingViews.Info
|
||||
marketId={marketId}
|
||||
onMarketClick={onMarketClick}
|
||||
onSelect={(id: string) => {
|
||||
onSelect?.(id);
|
||||
}}
|
||||
/>
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</TradeGridChild>
|
||||
</ResizableGridPanel>
|
||||
<ResizableGridPanel
|
||||
priority={LayoutPriority.Low}
|
||||
preferredSize="50%"
|
||||
minSize={50}
|
||||
>
|
||||
<TradeGridChild>
|
||||
<Tabs storageKey="console-trade-grid-positions">
|
||||
<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 storageKey="console-trade-grid-positions">
|
||||
<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 storageKey="console-trade-grid-chart">
|
||||
<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 storageKey="console-trade-grid-ticket">
|
||||
<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 storageKey="console-trade-grid-orderbook">
|
||||
<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';
|
||||
</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);
|
||||
|
||||
export const TradeGrid = ({
|
||||
market,
|
||||
@@ -277,7 +201,7 @@ export const TradeGrid = ({
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<MainGrid
|
||||
<MainGridWrapped
|
||||
marketId={market?.id || ''}
|
||||
onSelect={onSelect}
|
||||
pinnedAsset={pinnedAsset}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit';
|
||||
import { Tabs, Tab } from '@vegaprotocol/ui-toolkit';
|
||||
import { Markets } from './markets';
|
||||
import { Proposed } from './proposed';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
@@ -14,7 +14,7 @@ export const MarketsPage = () => {
|
||||
updateTitle(titlefy(['Markets']));
|
||||
}, [updateTitle]);
|
||||
return (
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tabs>
|
||||
<Tab id="all-markets" name={t('All markets')}>
|
||||
<Markets />
|
||||
</Tab>
|
||||
|
||||
@@ -25,8 +25,7 @@ export const DepositsContainer = () => {
|
||||
<div className="h-full relative">
|
||||
<DepositsTable
|
||||
rowData={data || []}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
noRowsOverlayComponent={() => null}
|
||||
ref={gridRef}
|
||||
{...bottomPlaceholderProps}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,7 @@ import { titlefy } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
||||
import {
|
||||
ResizableGridPanel,
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { ResizableGridPanel, Tab, Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import { WithdrawalsContainer } from './withdrawals-container';
|
||||
import { FillsContainer } from '@vegaprotocol/fills';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -45,7 +41,7 @@ export const Portfolio = () => {
|
||||
<ResizableGrid vertical>
|
||||
<ResizableGridPanel minSize={75}>
|
||||
<PortfolioGridChild>
|
||||
<Tabs storageKey="console-portfolio-account-history">
|
||||
<Tabs>
|
||||
<Tab id="account-history" name={t('Account history')}>
|
||||
<VegaWalletContainer>
|
||||
<AccountHistoryContainer />
|
||||
@@ -83,7 +79,7 @@ export const Portfolio = () => {
|
||||
minSize={50}
|
||||
>
|
||||
<PortfolioGridChild>
|
||||
<Tabs storageKey="console-portfolio-collateral">
|
||||
<Tabs>
|
||||
<Tab id="collateral" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<AccountsContainer />
|
||||
|
||||
@@ -24,8 +24,7 @@ export const WithdrawalsContainer = () => {
|
||||
<WithdrawalsTable
|
||||
data-testid="withdrawals-history"
|
||||
rowData={data}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
noRowsOverlayComponent={() => null}
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
<AsyncRenderer
|
||||
|
||||
@@ -12,11 +12,9 @@ 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();
|
||||
@@ -48,7 +46,6 @@ 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">
|
||||
|
||||
@@ -1,38 +1,20 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } 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',
|
||||
VEGA_INCIDENT_URL: 'https://blog.vega.community',
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockSetNodeSwitcher = jest.fn();
|
||||
jest.mock('../../stores', () => ({
|
||||
...jest.requireActual('../../stores'),
|
||||
useGlobalStore: () => mockSetNodeSwitcher,
|
||||
}));
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
it('controls the node switcher dialog', async () => {
|
||||
render(<NodeHealth />, { wrapper: MockedProvider });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||
});
|
||||
const mockOnClick = jest.fn();
|
||||
render(
|
||||
<NodeHealth
|
||||
onClick={mockOnClick}
|
||||
url={'https://api.n99.somenetwork.vega.xyz'}
|
||||
blockHeight={100}
|
||||
blockDiff={0}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(mockSetNodeSwitcher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('External link to blog should be present', () => {
|
||||
render(<NodeHealth />, { wrapper: MockedProvider });
|
||||
expect(
|
||||
screen.getByRole('link', { name: /^Mainnet status & incidents/ })
|
||||
).toBeInTheDocument();
|
||||
expect(mockOnClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,22 +31,14 @@ describe('NodeUrl', () => {
|
||||
|
||||
describe('HealthIndicator', () => {
|
||||
const cases = [
|
||||
{
|
||||
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' },
|
||||
{ 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' },
|
||||
];
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
render(<HealthIndicator text={elem.text} intent={elem.intent} />);
|
||||
render(<HealthIndicator blockDiff={elem.diff} />);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||
}
|
||||
|
||||
@@ -1,61 +1,59 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { Indicator, Intent } 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">
|
||||
<NodeHealth />
|
||||
{VEGA_URL && (
|
||||
<NodeHealth
|
||||
url={VEGA_URL}
|
||||
blockHeight={datanodeBlockHeight}
|
||||
blockDiff={blockDiff}
|
||||
onClick={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
interface NodeHealthProps {
|
||||
url: string;
|
||||
blockHeight: number | undefined;
|
||||
blockDiff: number | null;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const NodeHealth = () => {
|
||||
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { datanodeBlockHeight, text, intent } = useNodeHealth();
|
||||
const onClick = useCallback(() => {
|
||||
setNodeSwitcher(true);
|
||||
}, [setNodeSwitcher]);
|
||||
const incidentsLink = VEGA_INCIDENT_URL && (
|
||||
<ExternalLink className="ml-1" href={VEGA_INCIDENT_URL}>
|
||||
{t('Mainnet status & incidents')}
|
||||
</ExternalLink>
|
||||
);
|
||||
export const NodeHealth = ({
|
||||
url,
|
||||
blockHeight,
|
||||
blockDiff,
|
||||
onClick,
|
||||
}: NodeHealthProps) => {
|
||||
return (
|
||||
<>
|
||||
{VEGA_URL && (
|
||||
<FooterButton onClick={onClick} data-testid="node-health">
|
||||
<FooterButtonPart>
|
||||
<HealthIndicator text={text} intent={intent} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<NodeUrl url={VEGA_URL} />
|
||||
</FooterButtonPart>
|
||||
{/* create a monospace effect - avoiding jumps of width */}
|
||||
<FooterButtonPart
|
||||
width={`${
|
||||
datanodeBlockHeight
|
||||
? String(datanodeBlockHeight).length + 'ch'
|
||||
: 'auto'
|
||||
}`}
|
||||
>
|
||||
<span title={t('Block height')}>{datanodeBlockHeight}</span>
|
||||
</FooterButtonPart>
|
||||
</FooterButton>
|
||||
)}
|
||||
{incidentsLink}
|
||||
</>
|
||||
<FooterButton onClick={onClick} data-testid="node-health">
|
||||
<FooterButtonPart>
|
||||
<HealthIndicator blockDiff={blockDiff} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<NodeUrl url={url} />
|
||||
</FooterButtonPart>
|
||||
<FooterButtonPart>
|
||||
<span title={t('Block height')}>{blockHeight}</span>
|
||||
</FooterButtonPart>
|
||||
</FooterButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -71,11 +69,31 @@ export const NodeUrl = ({ url }: NodeUrlProps) => {
|
||||
};
|
||||
|
||||
interface HealthIndicatorProps {
|
||||
text: string;
|
||||
intent: Intent;
|
||||
blockDiff: number | null;
|
||||
}
|
||||
|
||||
export const HealthIndicator = ({ text, intent }: HealthIndicatorProps) => {
|
||||
// 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;
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={t('Node health')}>
|
||||
<Indicator variant={intent} />
|
||||
@@ -95,16 +113,9 @@ const FooterButton = (props: FooterButtonProps) => {
|
||||
return <button {...props} className={buttonClasses} />;
|
||||
};
|
||||
|
||||
const FooterButtonPart = ({
|
||||
width = 'auto',
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
width?: string;
|
||||
}) => {
|
||||
const FooterButtonPart = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<span
|
||||
style={{ width }}
|
||||
className={classNames(
|
||||
'relative inline-block mr-2 last:mr-0 pr-2 last:pr-0',
|
||||
'last:after:hidden',
|
||||
|
||||
@@ -93,8 +93,7 @@ export const MarketLiquiditySupplied = ({
|
||||
percentage.gte(100) &&
|
||||
market?.marketTradingMode ===
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
market.trigger ===
|
||||
AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
|
||||
market.trigger === AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
|
||||
|
||||
const description = marketId ? (
|
||||
<section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { ETHERSCAN_TX, useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -8,7 +8,7 @@ import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { Panel } from '@vegaprotocol/ui-toolkit';
|
||||
import { CLOSE_AFTER } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent, ProgressBar } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
@@ -103,7 +103,7 @@ const EthTxPendingToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
<>
|
||||
<ToastHeading>{t('Awaiting confirmation')}</ToastHeading>
|
||||
<p>{t('Please wait for your transaction to be confirmed.')}</p>
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
@@ -126,12 +126,26 @@ const EthTxErrorToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EtherscanLink = ({ tx }: EthTxToastContentProps) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
return tx.txHash ? (
|
||||
<p className="break-all">
|
||||
<ExternalLink
|
||||
href={etherscanLink(ETHERSCAN_TX.replace(':hash', tx.txHash))}
|
||||
rel="noreferrer"
|
||||
>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
) : null;
|
||||
};
|
||||
|
||||
const EthTxConfirmedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
return (
|
||||
<>
|
||||
<ToastHeading>{t('Transaction confirmed')}</ToastHeading>
|
||||
<p>{t('Your transaction has been confirmed.')}</p>
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
@@ -148,7 +162,7 @@ const EthTxCompletedToastContent = ({ tx }: EthTxToastContentProps) => {
|
||||
{t('Your transaction has been completed.')}{' '}
|
||||
{isDeposit && t('Waiting for deposit confirmation.')}
|
||||
</p>
|
||||
{tx.txHash && <EtherscanLink tx={tx.txHash} />}
|
||||
<EtherscanLink tx={tx} />
|
||||
<EthTransactionDetails tx={tx} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,29 +15,51 @@ jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||
}));
|
||||
|
||||
describe('AccountManager', () => {
|
||||
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' } },
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
mockedUseDataProvider
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [],
|
||||
};
|
||||
})
|
||||
.mockImplementationOnce((args) => {
|
||||
return {
|
||||
data: [
|
||||
{ asset: { id: 'a1' }, party: { id: 't1' } },
|
||||
{ asset: { id: 'a2' }, party: { id: 't2' } },
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables.partyId
|
||||
).toEqual('partyTwo');
|
||||
});
|
||||
|
||||
it('change partyId should reload data provider', async () => {
|
||||
it('update method should return proper result', async () => {
|
||||
let rerenderer: (ui: React.ReactElement) => void;
|
||||
await act(() => {
|
||||
const { rerender } = render(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
@@ -45,67 +67,13 @@ describe('AccountManager', () => {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(
|
||||
(helpers.useDataProvider as jest.Mock).mock.calls[1][0].variables
|
||||
.partyId
|
||||
).toEqual('partyTwo');
|
||||
rerenderer = rerender;
|
||||
});
|
||||
|
||||
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 waitFor(() => {
|
||||
expect(screen.getByText('No accounts')).toBeInTheDocument();
|
||||
});
|
||||
await act(() => {
|
||||
render(
|
||||
rerenderer(
|
||||
<AccountManager
|
||||
partyId="partyOne"
|
||||
onClickAsset={jest.fn}
|
||||
@@ -113,15 +81,11 @@ describe('AccountManager', () => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const container = document.querySelector('.ag-center-cols-container');
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
(content, element) =>
|
||||
Boolean(
|
||||
element?.className.endsWith('flex items-center justify-center')
|
||||
) && content.startsWith('Loading')
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
expect(getAllByRole(container as HTMLDivElement, 'row')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,6 @@ interface AccountManagerProps {
|
||||
onClickDeposit?: (assetId?: string) => void;
|
||||
isReadOnly: boolean;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
noBottomPlaceholder?: boolean;
|
||||
}
|
||||
|
||||
export const AccountManager = ({
|
||||
@@ -29,7 +28,6 @@ export const AccountManager = ({
|
||||
partyId,
|
||||
isReadOnly,
|
||||
pinnedAsset,
|
||||
noBottomPlaceholder,
|
||||
}: AccountManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
@@ -47,7 +45,6 @@ export const AccountManager = ({
|
||||
const bottomPlaceholderProps = useBottomPlaceholder<AccountFields>({
|
||||
gridRef,
|
||||
setId,
|
||||
disabled: noBottomPlaceholder,
|
||||
});
|
||||
|
||||
const getRowHeight = useCallback(
|
||||
@@ -63,8 +60,7 @@ export const AccountManager = ({
|
||||
onClickDeposit={onClickDeposit}
|
||||
onClickWithdraw={onClickWithdraw}
|
||||
isReadOnly={isReadOnly}
|
||||
suppressLoadingOverlay
|
||||
suppressNoRowsOverlay
|
||||
noRowsOverlayComponent={() => null}
|
||||
pinnedAsset={pinnedAsset}
|
||||
getRowHeight={getRowHeight}
|
||||
{...bottomPlaceholderProps}
|
||||
|
||||
@@ -59,19 +59,13 @@ export function createClient({
|
||||
const timestamp = r?.headers.get('x-block-timestamp');
|
||||
if (blockHeight && timestamp) {
|
||||
const state = useHeaderStore.getState();
|
||||
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))),
|
||||
},
|
||||
});
|
||||
}
|
||||
useHeaderStore.setState({
|
||||
...state,
|
||||
[r.url]: {
|
||||
blockHeight: Number(blockHeight),
|
||||
timestamp: new Date(Number(timestamp.slice(0, -6))),
|
||||
},
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { ContractAddressLink } from '@vegaprotocol/environment';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
@@ -108,7 +108,7 @@ export const rows: Rows = [
|
||||
|
||||
return (
|
||||
<>
|
||||
<EtherscanLink address={asset.source.contractAddress} />{' '}
|
||||
<ContractAddressLink address={asset.source.contractAddress} />{' '}
|
||||
<CopyWithTooltip text={asset.source.contractAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<Icon size={3} name="duplicate" />
|
||||
|
||||
@@ -46,10 +46,7 @@ addVegaWalletSubmitProposal();
|
||||
addVegaWalletSubmitLiquidityProvision();
|
||||
addImportNodeWallets();
|
||||
|
||||
export {
|
||||
mockConnectWallet,
|
||||
mockConnectWalletWithUserError,
|
||||
} from './lib/commands/vega-wallet-connect';
|
||||
export { mockConnectWallet } 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,8 +35,6 @@ function createNewMarketProposal(): ProposalSubmissionBody {
|
||||
changes: {
|
||||
decimalPlaces: '5',
|
||||
positionDecimalPlaces: '5',
|
||||
linearSlippageFactor: '0.001',
|
||||
quadraticSlippageFactor: '0',
|
||||
lpPriceRange: '10',
|
||||
instrument: {
|
||||
name: 'Test market 1',
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
aliasWalletConnectQuery,
|
||||
aliasWalletConnectWithUserError,
|
||||
} from '../mock-rest';
|
||||
import { aliasWalletConnectQuery } from '../mock-rest';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -23,12 +20,6 @@ export const mockConnectWallet = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const mockConnectWalletWithUserError = () => {
|
||||
cy.mockWallet((req) => {
|
||||
aliasWalletConnectWithUserError(req);
|
||||
});
|
||||
};
|
||||
|
||||
export function addVegaWalletConnect() {
|
||||
Cypress.Commands.add('connectVegaWallet', (isMobile) => {
|
||||
mockConnectWallet();
|
||||
|
||||
@@ -63,23 +63,3 @@ 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -30,12 +30,9 @@ export const compileGridData = (
|
||||
): { label: ReactNode; value?: ReactNode }[] => {
|
||||
const grid: SimpleGridProps['grid'] = [];
|
||||
const isLiquidityMonitoringAuction =
|
||||
(marketData?.marketTradingMode ===
|
||||
marketData?.marketTradingMode ===
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||
marketData?.trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET) ||
|
||||
marketData?.trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
|
||||
marketData?.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
|
||||
|
||||
const formatStake = (value: string) => {
|
||||
const formattedValue = addDecimalsFormatNumber(
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as Schema from '@vegaprotocol/types';
|
||||
import { ExternalLink, SimpleGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { compileGridData } from './compile-grid-data';
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
type TradingModeTooltipProps = {
|
||||
marketId?: string;
|
||||
@@ -114,39 +115,23 @@ export const TradingModeTooltip = ({
|
||||
}
|
||||
case Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION: {
|
||||
switch (trigger) {
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET: {
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY: {
|
||||
const notEnoughLiquidity = new BigNumber(
|
||||
marketData.suppliedStake || 0
|
||||
).isLessThan(marketData.targetStake || 0);
|
||||
return (
|
||||
<section data-testid="trading-mode-tooltip">
|
||||
<p className={classNames({ 'mb-4': Boolean(compiledGrid) })}>
|
||||
<span className="mb-2">
|
||||
{t(
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)}
|
||||
</span>
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={
|
||||
createDocsLinks(VEGA_DOCS_URL)
|
||||
.AUCTION_TYPE_LIQUIDITY_MONITORING
|
||||
}
|
||||
>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
{compiledGrid && <SimpleGrid grid={compiledGrid} />}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
case Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS: {
|
||||
return (
|
||||
<section data-testid="trading-mode-tooltip">
|
||||
<p className={classNames({ 'mb-4': Boolean(compiledGrid) })}>
|
||||
<span className="mb-2">
|
||||
{t(
|
||||
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</span>
|
||||
{notEnoughLiquidity &&
|
||||
t(
|
||||
'This market is in auction until it reaches sufficient liquidity.'
|
||||
)}
|
||||
{!notEnoughLiquidity &&
|
||||
t(
|
||||
'This market may have sufficient liquidity but there are not enough priced limit orders in the order book, which are required to deploy liquidity commitment pegged orders.'
|
||||
)}
|
||||
</span>{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={
|
||||
|
||||
@@ -13,10 +13,7 @@ export const validateTimeInForce = (
|
||||
const isPriceTrigger =
|
||||
trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE;
|
||||
const isLiquidityTrigger =
|
||||
trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET ||
|
||||
trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
|
||||
trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
|
||||
|
||||
if (isMarketInAuction(marketTradingMode)) {
|
||||
if (
|
||||
|
||||
@@ -17,10 +17,7 @@ export const validateType = (
|
||||
const isPriceTrigger =
|
||||
trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE;
|
||||
const isLiquidityTrigger =
|
||||
trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET ||
|
||||
trigger ===
|
||||
Schema.AuctionTrigger.AUCTION_TRIGGER_UNABLE_TO_DEPLOY_LP_ORDERS;
|
||||
trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
|
||||
|
||||
if (isMonitoringAuction && isPriceTrigger) {
|
||||
return MarketModeValidationType.PriceMonitoringAuction;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
@@ -126,10 +126,14 @@ const ApprovalTxFeedback = ({
|
||||
selectedAsset: Asset;
|
||||
allowance?: BigNumber;
|
||||
}) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
|
||||
if (!tx) return null;
|
||||
|
||||
const txLink = tx.txHash && (
|
||||
<EtherscanLink tx={tx.txHash}>{t('View on Etherscan')}</EtherscanLink>
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</ExternalLink>
|
||||
);
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
@@ -15,13 +16,14 @@ import type {
|
||||
TypedDataAgGrid,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { DepositFieldsFragment } from './__generated__/Deposit';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { DepositStatusMapping } from '@vegaprotocol/types';
|
||||
|
||||
export const DepositsTable = forwardRef<
|
||||
AgGridReact,
|
||||
TypedDataAgGrid<DepositFieldsFragment>
|
||||
>((props, ref) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
return (
|
||||
<AgGrid
|
||||
ref={ref}
|
||||
@@ -75,9 +77,14 @@ export const DepositsTable = forwardRef<
|
||||
if (!data) return null;
|
||||
if (!value) return '-';
|
||||
return (
|
||||
<EtherscanLink tx={value} data-testid="etherscan-link">
|
||||
<Link
|
||||
title={t('View transaction on Etherscan')}
|
||||
href={`${ETHERSCAN_URL}/tx/${value}`}
|
||||
data-testid="etherscan-link"
|
||||
target="_blank"
|
||||
>
|
||||
{truncateByChars(value)}
|
||||
</EtherscanLink>
|
||||
</Link>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import { getFaucetError } from './get-faucet-error';
|
||||
|
||||
@@ -19,6 +19,7 @@ export const FaucetNotification = ({
|
||||
selectedAsset,
|
||||
faucetTxId,
|
||||
}: FaucetNotificationProps) => {
|
||||
const { ETHERSCAN_URL } = useEnvironment();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||
});
|
||||
@@ -78,9 +79,9 @@ export const FaucetNotification = ({
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
<p>
|
||||
<EtherscanLink tx={tx.txHash}>
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</EtherscanLink>
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
@@ -106,9 +107,9 @@ export const FaucetNotification = ({
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
<p>
|
||||
<EtherscanLink tx={tx.txHash}>
|
||||
<ExternalLink href={`${ETHERSCAN_URL}/tx/${tx.txHash}`}>
|
||||
{t('View on Etherscan')}
|
||||
</EtherscanLink>
|
||||
</ExternalLink>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEtherscanLink } from '../hooks';
|
||||
|
||||
export const ContractAddressLink = ({ address }: { address: string }) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const href = etherscanLink(`/address/${address}`);
|
||||
return (
|
||||
<Link href={href} target="_blank" title={t('View on etherscan')}>
|
||||
{address}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { ETHERSCAN_ADDRESS, ETHERSCAN_TX, useEtherscanLink } from '../hooks';
|
||||
|
||||
export const EtherscanLink = ({
|
||||
address,
|
||||
tx,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
address?: string;
|
||||
tx?: string;
|
||||
} & ComponentProps<typeof ExternalLink>) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
let href = '';
|
||||
|
||||
if ((!address && !tx) || (address && tx)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (address) {
|
||||
href = etherscanLink(ETHERSCAN_ADDRESS.replace(':hash', address));
|
||||
}
|
||||
if (tx) {
|
||||
href = etherscanLink(ETHERSCAN_TX.replace(':hash', tx));
|
||||
}
|
||||
|
||||
return (
|
||||
<ExternalLink
|
||||
href={href}
|
||||
title={t('View on Etherscan (opens in a new tab)')}
|
||||
{...props}
|
||||
>
|
||||
{children || address || tx}
|
||||
</ExternalLink>
|
||||
);
|
||||
};
|
||||
@@ -2,4 +2,4 @@ export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
export * from './node-guard';
|
||||
export * from './node-switcher';
|
||||
export * from './etherscan-link';
|
||||
export * from './contract-address-link';
|
||||
|
||||
@@ -285,7 +285,6 @@ function compileEnvVars() {
|
||||
GIT_BRANCH: process.env['GIT_COMMIT_BRANCH'],
|
||||
GIT_COMMIT_HASH: process.env['GIT_COMMIT_HASH'],
|
||||
GIT_ORIGIN_URL: process.env['GIT_ORIGIN_URL'],
|
||||
VEGA_INCIDENT_URL: process.env['NX_VEGA_INCIDENT_URL'],
|
||||
};
|
||||
|
||||
return env;
|
||||
|
||||
@@ -103,7 +103,6 @@ export const TOKEN_VALIDATOR = '/validators/:id';
|
||||
export const EXPLORER_TX = '/txs/:hash';
|
||||
|
||||
// Etherscan pages
|
||||
export const ETHERSCAN_ADDRESS = '/address/:hash';
|
||||
export const ETHERSCAN_TX = '/tx/:hash';
|
||||
|
||||
// Console pages
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
|
||||
@@ -56,24 +55,9 @@ function setup(
|
||||
|
||||
describe('useNodeHealth', () => {
|
||||
it.each([
|
||||
{
|
||||
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,
|
||||
},
|
||||
{ core: 1, node: 1, expected: 0 },
|
||||
{ core: 1, node: 5, expected: -4 },
|
||||
{ core: 10, node: 5, expected: 5 },
|
||||
])(
|
||||
'provides difference core block $core and node block $node',
|
||||
async (cases) => {
|
||||
@@ -81,12 +65,12 @@ describe('useNodeHealth', () => {
|
||||
blockHeight: cases.node,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||
await waitFor(() => {
|
||||
expect(result.current.text).toEqual(cases.expectedText);
|
||||
expect(result.current.intent).toEqual(cases.expectedIntent);
|
||||
expect(result.current.blockDiff).toEqual(cases.expected);
|
||||
expect(result.current.coreBlockHeight).toEqual(cases.core);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||
});
|
||||
}
|
||||
@@ -106,64 +90,25 @@ describe('useNodeHealth', () => {
|
||||
blockHeight: 1,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(1);
|
||||
await waitFor(() => {
|
||||
expect(result.current.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
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.text).toEqual('Non operational');
|
||||
expect(result.current.intent).toEqual(Intent.Danger);
|
||||
expect(result.current.blockDiff).toEqual(null);
|
||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
||||
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
||||
await waitFor(() => {
|
||||
expect(result.current.text).toEqual('Operational');
|
||||
expect(result.current.intent).toEqual(Intent.Success);
|
||||
expect(result.current.blockDiff).toEqual(0);
|
||||
expect(result.current.coreBlockHeight).toEqual(1);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,35 +2,30 @@ import { useEffect, useMemo } from 'react';
|
||||
import { useStatisticsQuery } from '../utils/__generated__/Node';
|
||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||
import { useEnvironment } from './use-environment';
|
||||
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { fromNanoSeconds } from '@vegaprotocol/utils';
|
||||
|
||||
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, startPolling, stopPolling } = useStatisticsQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
const { data, error, loading, startPolling, stopPolling } =
|
||||
useStatisticsQuery({
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const blockDiff = useMemo(() => {
|
||||
if (!data?.statistics.blockHeight) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!headers?.blockHeight) {
|
||||
if (!headers) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(data.statistics.blockHeight) - headers.blockHeight;
|
||||
}, [data?.statistics.blockHeight, headers?.blockHeight]);
|
||||
}, [data, headers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
@@ -43,43 +38,17 @@ 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,
|
||||
text,
|
||||
intent,
|
||||
datanodeVegaTime: headers?.timestamp,
|
||||
blockDiff,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ const schemaObject = {
|
||||
MAINTENANCE_PAGE: z.optional(z.boolean()),
|
||||
ETH_LOCAL_PROVIDER_URL: z.optional(z.string()),
|
||||
ETH_WALLET_MNEMONIC: z.optional(z.string()),
|
||||
VEGA_INCIDENT_URL: z.optional(z.string()),
|
||||
};
|
||||
|
||||
// combine schema above with custom rule to ensure either
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('updateLevels', () => {
|
||||
|
||||
const updates = [{ price: '132', volume: '200' }];
|
||||
|
||||
expect(updateLevels(priceLevels, updates, 2, 0, false)).toEqual([
|
||||
expect(updateLevels(priceLevels, updates, 2, 0, true)).toEqual([
|
||||
{ price: 1.35, volume: 200 },
|
||||
{ price: 1.32, volume: 200 },
|
||||
{ price: 1.28, volume: 100 },
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import reverse from 'lodash/reverse';
|
||||
|
||||
interface PriceLevel {
|
||||
price: number;
|
||||
@@ -43,9 +41,9 @@ export const updateLevels = (
|
||||
updates: RawPriceLevel[],
|
||||
decimalPlaces: number,
|
||||
positionDecimalPlaces: number,
|
||||
ascending = true
|
||||
reverse = false
|
||||
) => {
|
||||
uniqBy(reverse(updates || []), 'price').forEach((update) => {
|
||||
updates.forEach((update) => {
|
||||
const updateLevel = parseLevel(
|
||||
update,
|
||||
decimalPlaces,
|
||||
@@ -65,9 +63,9 @@ export const updateLevels = (
|
||||
}
|
||||
} else if (update.volume !== '0') {
|
||||
index = levels.findIndex((level) =>
|
||||
ascending
|
||||
? level.price > updateLevel.price
|
||||
: level.price < updateLevel.price
|
||||
reverse
|
||||
? level.price < updateLevel.price
|
||||
: level.price > updateLevel.price
|
||||
);
|
||||
if (index !== -1) {
|
||||
levels.splice(index, 0, updateLevel);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
|
||||
import type { MarketData } from '@vegaprotocol/market-list';
|
||||
import type {
|
||||
MarketDepthQuery,
|
||||
MarketDepthUpdateSubscription,
|
||||
PriceLevelFieldsFragment,
|
||||
} from './__generated__/MarketDepth';
|
||||
@@ -23,18 +22,6 @@ interface DepthChartManagerProps {
|
||||
const formatMidPrice = (midPrice: string, decimalPlaces: number) =>
|
||||
Number(addDecimal(midPrice, decimalPlaces));
|
||||
|
||||
const getMidPrice = (
|
||||
sell: PriceLevelFieldsFragment[] | null | undefined,
|
||||
buy: PriceLevelFieldsFragment[] | null | undefined,
|
||||
decimalPlaces: number
|
||||
) =>
|
||||
buy?.length && sell?.length
|
||||
? formatMidPrice(
|
||||
((BigInt(buy[0].price) + BigInt(sell[0].price)) / BigInt(2)).toString(),
|
||||
decimalPlaces
|
||||
)
|
||||
: undefined;
|
||||
|
||||
type DepthData = Pick<DepthChartProps, 'data' | 'midPrice'>;
|
||||
|
||||
export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
@@ -43,7 +30,6 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
const [depthData, setDepthData] = useState<DepthData | null>(null);
|
||||
const dataRef = useRef<DepthData | null>(null);
|
||||
const marketDataRef = useRef<MarketData | null>(null);
|
||||
const rawDataRef = useRef<MarketDepthQuery['market'] | null>(null);
|
||||
const deltaRef = useRef<{
|
||||
sell: PriceLevelFieldsFragment[];
|
||||
buy: PriceLevelFieldsFragment[];
|
||||
@@ -70,28 +56,28 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
}
|
||||
dataRef.current = {
|
||||
...dataRef.current,
|
||||
midPrice: getMidPrice(
|
||||
rawDataRef.current?.depth.sell,
|
||||
rawDataRef.current?.depth.buy,
|
||||
market.decimalPlaces
|
||||
),
|
||||
midPrice: marketDataRef.current?.staticMidPrice
|
||||
? formatMidPrice(
|
||||
marketDataRef.current?.staticMidPrice,
|
||||
market.decimalPlaces
|
||||
)
|
||||
: undefined,
|
||||
data: {
|
||||
buy: deltaRef.current.buy.length
|
||||
buy: deltaRef.current.buy
|
||||
? updateLevels(
|
||||
dataRef.current.data.buy,
|
||||
deltaRef.current.buy,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
false
|
||||
true
|
||||
)
|
||||
: dataRef.current.data.buy,
|
||||
sell: deltaRef.current.sell.length
|
||||
sell: deltaRef.current.sell
|
||||
? updateLevels(
|
||||
dataRef.current.data.sell,
|
||||
deltaRef.current.sell,
|
||||
market.decimalPlaces,
|
||||
market.positionDecimalPlaces,
|
||||
true
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: dataRef.current.data.sell,
|
||||
},
|
||||
@@ -99,23 +85,16 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
setDepthData(dataRef.current);
|
||||
}, 250),
|
||||
}, 1000),
|
||||
[market]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
}, [marketId]);
|
||||
|
||||
// Apply updates to the table
|
||||
const update = useCallback(
|
||||
({
|
||||
delta: deltas,
|
||||
data: rawData,
|
||||
}: {
|
||||
delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'] | null;
|
||||
data: NonNullable<MarketDepthQuery['market']> | null;
|
||||
delta?: MarketDepthUpdateSubscription['marketsDepthUpdate'];
|
||||
}) => {
|
||||
if (!dataRef.current) {
|
||||
return false;
|
||||
@@ -130,7 +109,6 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
if (delta.buy) {
|
||||
deltaRef.current.buy.push(...delta.buy);
|
||||
}
|
||||
rawDataRef.current = rawData;
|
||||
updateDepthData();
|
||||
}
|
||||
return true;
|
||||
@@ -138,11 +116,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
[marketId, updateDepthData]
|
||||
);
|
||||
|
||||
const { data, error, loading } = useDataProvider<
|
||||
NonNullable<MarketDepthQuery['market']> | null,
|
||||
MarketDepthUpdateSubscription['marketsDepthUpdate'],
|
||||
{ marketId: string }
|
||||
>({
|
||||
const { data, error, loading } = useDataProvider({
|
||||
dataProvider: marketDepthProvider,
|
||||
update,
|
||||
variables,
|
||||
@@ -181,11 +155,9 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
return;
|
||||
}
|
||||
dataRef.current = {
|
||||
midPrice: getMidPrice(
|
||||
data.depth.sell,
|
||||
data.depth.buy,
|
||||
market.decimalPlaces
|
||||
),
|
||||
midPrice: marketData.staticMidPrice
|
||||
? formatMidPrice(marketData.staticMidPrice, market.decimalPlaces)
|
||||
: undefined,
|
||||
data: {
|
||||
buy:
|
||||
data.depth.buy?.map((priceLevel) =>
|
||||
@@ -205,12 +177,8 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
) ?? [],
|
||||
},
|
||||
};
|
||||
rawDataRef.current = data;
|
||||
setDepthData(dataRef.current);
|
||||
return () => {
|
||||
updateDepthData.cancel();
|
||||
};
|
||||
}, [data, marketData, market, updateDepthData]);
|
||||
}, [data, marketData, market]);
|
||||
|
||||
const volumeFormat = useCallback(
|
||||
(volume: number) =>
|
||||
|
||||
@@ -75,11 +75,6 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
|
||||
}, 250)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
deltaRef.current.buy = [];
|
||||
deltaRef.current.sell = [];
|
||||
}, [marketId]);
|
||||
|
||||
const update = useCallback(
|
||||
({
|
||||
delta: deltas,
|
||||
|
||||
@@ -3,4 +3,3 @@ export * from './info-market';
|
||||
export * from './tooltip-mapping';
|
||||
export * from './__generated__/MarketInfo';
|
||||
export * from './market-info-data-provider';
|
||||
export * from './market-info-panels';
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper, TokenLinks } from '@vegaprotocol/utils';
|
||||
import {
|
||||
totalFeesPercentage,
|
||||
calcCandleVolume,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
removePaginationWrapper,
|
||||
TokenLinks,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useDataProvider, useYesterday } from '@vegaprotocol/react-helpers';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
@@ -10,31 +22,15 @@ import {
|
||||
Link as UILink,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import { generatePath, Link } from 'react-router-dom';
|
||||
|
||||
import { MarketInfoTable } from './info-key-value-table';
|
||||
import { marketInfoWithDataAndCandlesProvider } from './market-info-data-provider';
|
||||
|
||||
import type { MarketInfoWithDataAndCandles } from './market-info-data-provider';
|
||||
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
CurrentFeesInfoPanel,
|
||||
InstrumentInfoPanel,
|
||||
InsurancePoolInfoPanel,
|
||||
KeyDetailsInfoPanel,
|
||||
LiquidityInfoPanel,
|
||||
LiquidityMonitoringParametersInfoPanel,
|
||||
LiquidityPriceRangeInfoPanel,
|
||||
MarketPriceInfoPanel,
|
||||
MarketVolumeInfoPanel,
|
||||
MetadataInfoPanel,
|
||||
OracleInfoPanel,
|
||||
PriceMonitoringBoundsInfoPanel,
|
||||
RiskFactorsInfoPanel,
|
||||
RiskModelInfoPanel,
|
||||
RiskParametersInfoPanel,
|
||||
SettlementAssetInfoPanel,
|
||||
} from './market-info-panels';
|
||||
|
||||
export interface InfoProps {
|
||||
market: MarketInfoWithDataAndCandles;
|
||||
@@ -84,6 +80,15 @@ export const MarketInfoContainer = ({
|
||||
export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
const { VEGA_TOKEN_URL, VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const headerClassName = 'uppercase text-lg';
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
@@ -91,75 +96,272 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
market.accountsConnection?.edges
|
||||
);
|
||||
|
||||
const last24hourVolume = market.candles && calcCandleVolume(market.candles);
|
||||
|
||||
const marketDataPanels = [
|
||||
{
|
||||
title: t('Current fees'),
|
||||
content: <CurrentFeesInfoPanel market={market} />,
|
||||
content: (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
...market.fees.factors,
|
||||
totalFees: totalFeesPercentage(market.fees.factors),
|
||||
}}
|
||||
asPercentage={true}
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'All fees are paid by price takers and are a % of the trade notional value. Fees are not paid during auction uncrossing.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Market price'),
|
||||
content: <MarketPriceInfoPanel market={market} />,
|
||||
content: (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
markPrice: market.data?.markPrice,
|
||||
bestBidPrice: market.data?.bestBidPrice,
|
||||
bestOfferPrice: market.data?.bestOfferPrice,
|
||||
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Market volume'),
|
||||
content: <MarketVolumeInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
'24hourVolume':
|
||||
last24hourVolume && last24hourVolume !== '0'
|
||||
? addDecimalsFormatNumber(
|
||||
last24hourVolume,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: '-',
|
||||
openInterest: market.data?.openInterest,
|
||||
bestBidVolume: market.data?.bestBidVolume,
|
||||
bestOfferVolume: market.data?.bestOfferVolume,
|
||||
bestStaticBidVolume: market.data?.bestStaticBidVolume,
|
||||
bestStaticOfferVolume: market.data?.bestStaticOfferVolume,
|
||||
}}
|
||||
decimalPlaces={market.positionDecimalPlaces}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...marketAccounts
|
||||
.filter((a) => a.type === Schema.AccountType.ACCOUNT_TYPE_INSURANCE)
|
||||
.map((a) => ({
|
||||
title: t(`Insurance pool`),
|
||||
content: <InsurancePoolInfoPanel market={market} account={a} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
balance: a.balance,
|
||||
}}
|
||||
assetSymbol={assetSymbol}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset
|
||||
.decimals
|
||||
}
|
||||
/>
|
||||
),
|
||||
})),
|
||||
];
|
||||
|
||||
const keyDetails = {
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
tradingMode: market.tradingMode,
|
||||
state: Schema.MarketStateMapping[market.state],
|
||||
};
|
||||
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
|
||||
const marketSpecPanels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
content: <KeyDetailsInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
keyDetails.tradingMode &&
|
||||
Schema.MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Instrument'),
|
||||
content: <InstrumentInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType:
|
||||
market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: <SettlementAssetInfoPanel market={market} />,
|
||||
content: asset ? (
|
||||
<>
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={true}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Metadata'),
|
||||
content: <MetadataInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk model'),
|
||||
content: <RiskModelInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk parameters'),
|
||||
content: <RiskParametersInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk factors'),
|
||||
content: <RiskFactorsInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(_, triggerIndex) => ({
|
||||
title: t(`Price monitoring bounds ${triggerIndex + 1}`),
|
||||
content: (
|
||||
<PriceMonitoringBoundsInfoPanel
|
||||
market={market}
|
||||
triggerIndex={triggerIndex}
|
||||
/>
|
||||
),
|
||||
})
|
||||
(trigger, i) => {
|
||||
const bounds = market.data?.priceMonitoringBounds?.[i];
|
||||
return {
|
||||
title: t(`Price monitoring bounds ${i + 1}`),
|
||||
content: (
|
||||
<div className="text-xs">
|
||||
<div className="grid grid-cols-2 text-xs mb-4">
|
||||
<p className="col-span-1">
|
||||
{t('%s probability price bounds', [
|
||||
formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
),
|
||||
])}
|
||||
</p>
|
||||
<p className="col-span-1 text-right">
|
||||
{t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
{bounds && (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
{t('Results in %s seconds auction if breached', [
|
||||
trigger.auctionExtensionSecs.toString(),
|
||||
])}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
}
|
||||
),
|
||||
{
|
||||
title: t('Liquidity monitoring parameters'),
|
||||
content: <LiquidityMonitoringParametersInfoPanel market={market} />,
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio:
|
||||
market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity'),
|
||||
content: (
|
||||
<LiquidityInfoPanel market={market}>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
targetStake: market.data && market.data.targetStake,
|
||||
suppliedStake: market.data && market.data?.suppliedStake,
|
||||
marketValueProxy: market.data && market.data.marketValueProxy,
|
||||
}}
|
||||
decimalPlaces={assetDecimals}
|
||||
assetSymbol={assetSymbol}
|
||||
>
|
||||
<Link
|
||||
to={`/liquidity/${market.id}`}
|
||||
onClick={() => onSelect(market.id)}
|
||||
@@ -167,17 +369,57 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
>
|
||||
<UILink>{t('View liquidity provision table')}</UILink>
|
||||
</Link>
|
||||
</LiquidityInfoPanel>
|
||||
</MarketInfoTable>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: <LiquidityPriceRangeInfoPanel market={market} />,
|
||||
content: (
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<OracleInfoPanel market={market}>
|
||||
<MarketInfoTable
|
||||
data={
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
||||
}
|
||||
>
|
||||
<ExternalLink
|
||||
href={`${VEGA_EXPLORER_URL}/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
@@ -188,7 +430,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</ExternalLink>
|
||||
</OracleInfoPanel>
|
||||
</MarketInfoTable>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
totalFeesPercentage,
|
||||
} from '@vegaprotocol/market-list';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
formatNumberPercentage,
|
||||
getMarketExpiryDateFormatted,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { Get } from 'type-fest';
|
||||
import { MarketInfoTable } from './info-key-value-table';
|
||||
import type {
|
||||
MarketInfo,
|
||||
MarketInfoWithData,
|
||||
MarketInfoWithDataAndCandles,
|
||||
} from './market-info-data-provider';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { MarketTradingModeMapping } from '@vegaprotocol/types';
|
||||
|
||||
type PanelProps = Pick<
|
||||
ComponentProps<typeof MarketInfoTable>,
|
||||
'children' | 'noBorder'
|
||||
>;
|
||||
|
||||
type MarketInfoProps = {
|
||||
market: MarketInfo;
|
||||
};
|
||||
|
||||
type MarketInfoWithDataProps = {
|
||||
market: MarketInfoWithData;
|
||||
};
|
||||
|
||||
type MarketInfoWithDataAndCandlesProps = {
|
||||
market: MarketInfoWithDataAndCandles;
|
||||
};
|
||||
|
||||
export const CurrentFeesInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
...market.fees.factors,
|
||||
totalFees: totalFeesPercentage(market.fees.factors),
|
||||
}}
|
||||
asPercentage={true}
|
||||
{...props}
|
||||
/>
|
||||
<p className="text-xs">
|
||||
{t(
|
||||
'All fees are paid by price takers and are a % of the trade notional value. Fees are not paid during auction uncrossing.'
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
export const MarketPriceInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
return (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
markPrice: market.data?.markPrice,
|
||||
bestBidPrice: market.data?.bestBidPrice,
|
||||
bestOfferPrice: market.data?.bestOfferPrice,
|
||||
quoteUnit: market.tradableInstrument.instrument.product.quoteName,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
{...props}
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MarketVolumeInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataAndCandlesProps & PanelProps) => {
|
||||
const last24hourVolume = market.candles && calcCandleVolume(market.candles);
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
'24hourVolume':
|
||||
last24hourVolume && last24hourVolume !== '0'
|
||||
? addDecimalsFormatNumber(
|
||||
last24hourVolume,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: '-',
|
||||
openInterest: market.data?.openInterest,
|
||||
bestBidVolume: market.data?.bestBidVolume,
|
||||
bestOfferVolume: market.data?.bestOfferVolume,
|
||||
bestStaticBidVolume: market.data?.bestStaticBidVolume,
|
||||
bestStaticOfferVolume: market.data?.bestStaticOfferVolume,
|
||||
}}
|
||||
decimalPlaces={market.positionDecimalPlaces}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InsurancePoolInfoPanel = ({
|
||||
market,
|
||||
account,
|
||||
...props
|
||||
}: {
|
||||
account: NonNullable<
|
||||
Get<MarketInfoWithData, 'accountsConnection.edges[0].node'>
|
||||
>;
|
||||
} & MarketInfoProps &
|
||||
PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
balance: account.balance,
|
||||
}}
|
||||
assetSymbol={assetSymbol}
|
||||
decimalPlaces={
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyDetailsInfoPanel = ({
|
||||
market,
|
||||
}: MarketInfoProps & PanelProps) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
market.tradingMode && MarketTradingModeMapping[market.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstrumentInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType: market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const SettlementAssetInfoPanel = ({
|
||||
market,
|
||||
noBorder = true,
|
||||
}: MarketInfoProps & PanelProps) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
return asset ? (
|
||||
<>
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={noBorder}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
<p className="text-xs mt-4">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit (%s).',
|
||||
[assetSymbol, quoteUnit]
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
);
|
||||
};
|
||||
|
||||
export const MetadataInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskModelInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskParametersInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RiskFactorsInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const PriceMonitoringBoundsInfoPanel = ({
|
||||
market,
|
||||
triggerIndex,
|
||||
...props
|
||||
}: {
|
||||
triggerIndex: number;
|
||||
} & MarketInfoWithDataProps &
|
||||
PanelProps) => {
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const trigger =
|
||||
market.priceMonitoringSettings?.parameters?.triggers?.[triggerIndex];
|
||||
const bounds = market.data?.priceMonitoringBounds?.[triggerIndex];
|
||||
if (!trigger) {
|
||||
console.error(
|
||||
`Could not find data for trigger ${triggerIndex} (market id: ${market.id})`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="text-xs">
|
||||
<div className="grid grid-cols-2 text-xs mb-4">
|
||||
<p className="col-span-1">
|
||||
{t('%s probability price bounds', [
|
||||
formatNumberPercentage(
|
||||
new BigNumber(trigger.probability).times(100)
|
||||
),
|
||||
])}
|
||||
</p>
|
||||
<p className="col-span-1 text-right">
|
||||
{t('Within %s seconds', [formatNumber(trigger.horizonSecs)])}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
{bounds && (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
highestPrice: bounds.maxValidPrice,
|
||||
lowestPrice: bounds.minValidPrice,
|
||||
}}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
assetSymbol={quoteUnit}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
{t('Results in %s seconds auction if breached', [
|
||||
trigger.auctionExtensionSecs.toString(),
|
||||
])}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityMonitoringParametersInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
triggeringRatio: market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const LiquidityInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol || '';
|
||||
return (
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
targetStake: market.data && market.data.targetStake,
|
||||
suppliedStake: market.data && market.data?.suppliedStake,
|
||||
marketValueProxy: market.data && market.data.marketValueProxy,
|
||||
}}
|
||||
decimalPlaces={assetDecimals}
|
||||
assetSymbol={assetSymbol}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const LiquidityPriceRangeInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoWithDataProps & PanelProps) => {
|
||||
const quoteUnit =
|
||||
market?.tradableInstrument.instrument.product?.quoteName || '';
|
||||
const liquidityPriceRange = formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<p className="text-xs mb-4">
|
||||
{`For liquidity orders to count towards a commitment, they must be
|
||||
within the liquidity monitoring bounds.`}
|
||||
</p>
|
||||
<p className="text-xs mb-4">
|
||||
{`The liquidity price range is a ${liquidityPriceRange} difference from the mid
|
||||
price.`}
|
||||
</p>
|
||||
<div className="pl-2 pb-0 text-xs border-l-2">
|
||||
<MarketInfoTable
|
||||
data={{
|
||||
liquidityPriceRange: `${liquidityPriceRange} of mid price`,
|
||||
lowestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
highestPrice:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${quoteUnit}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const OracleInfoPanel = ({
|
||||
market,
|
||||
...props
|
||||
}: MarketInfoProps & PanelProps) => (
|
||||
<MarketInfoTable
|
||||
data={market.tradableInstrument.instrument.product.dataSourceSpecBinding}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -74,7 +74,7 @@ const marketsDataFieldsFragments: MarketsDataFieldsFragment[] = [
|
||||
bestBidPrice: '0',
|
||||
bestOfferPrice: '0',
|
||||
markPrice: '4612690058',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET,
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
|
||||
__typename: 'MarketData',
|
||||
},
|
||||
{
|
||||
@@ -91,7 +91,7 @@ const marketsDataFieldsFragments: MarketsDataFieldsFragment[] = [
|
||||
bestBidPrice: '0',
|
||||
bestOfferPrice: '0',
|
||||
markPrice: '4612690058',
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET,
|
||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY,
|
||||
__typename: 'MarketData',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { PromotedStatsItem } from './promoted-stats-item';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Callout, Indicator, Intent, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { StatFields } from '../../config/types';
|
||||
import { defaultFieldFormatter } from '../table-row';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const PromotedStatsItem = ({
|
||||
title,
|
||||
formatter,
|
||||
goodThreshold,
|
||||
value,
|
||||
description,
|
||||
...props
|
||||
}: StatFields) => {
|
||||
const variant = useMemo(
|
||||
() =>
|
||||
goodThreshold
|
||||
? goodThreshold(value)
|
||||
? Intent.Success
|
||||
: Intent.Danger
|
||||
: Intent.Primary,
|
||||
[goodThreshold, value]
|
||||
);
|
||||
return (
|
||||
<Tooltip description={description} align="start">
|
||||
<Callout>
|
||||
<div className="uppercase text-sm">
|
||||
<Indicator variant={variant} />
|
||||
<span data-testid="stats-title">{title}</span>
|
||||
</div>
|
||||
<div data-testid="stats-value" className="mt-2 text-2xl">
|
||||
{formatter ? formatter(value) : defaultFieldFormatter(value)}
|
||||
</div>
|
||||
</Callout>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { PromotedStats } from './promoted-stats';
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
interface PromotedStatsProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PromotedStats = ({ children }: PromotedStatsProps) => {
|
||||
return (
|
||||
<div className="grid promoted-stats content-start gap-4 mb-24">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +1,107 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { Statistics, NodeData } from '../../config/stats-fields';
|
||||
import { fieldsDefinition } from '../../config/stats-fields';
|
||||
import { useStatsQuery } from './__generated__/Stats';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { statsFields } from '../../config/stats-fields';
|
||||
import type {
|
||||
Stats as IStats,
|
||||
StructuredStats as IStructuredStats,
|
||||
} from '../../config/types';
|
||||
import { Table } from '../table';
|
||||
import { TableRow } from '../table-row';
|
||||
import { PromotedStats } from '../promoted-stats';
|
||||
import { PromotedStatsItem } from '../promoted-stats-item';
|
||||
import { useStatsQuery } from './__generated__/Stats';
|
||||
import type { StatsQuery } from './__generated__/Stats';
|
||||
|
||||
interface StatsManagerProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const compileData = (data?: StatsQuery) => {
|
||||
const { nodeData, statistics } = data || {};
|
||||
const returned = { ...nodeData, ...statistics };
|
||||
|
||||
// Loop through the stats fields config, grabbing values from the fetched
|
||||
// data and building a set of promoted and standard table entries.
|
||||
return Object.entries(statsFields).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const statKey = key as keyof IStats;
|
||||
const statData = returned[statKey];
|
||||
|
||||
value.forEach((x) => {
|
||||
const stat = {
|
||||
...x,
|
||||
value: statData || '-',
|
||||
};
|
||||
|
||||
stat.promoted ? acc.promoted.push(stat) : acc.table.push(stat);
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ promoted: [], table: [] } as IStructuredStats
|
||||
);
|
||||
};
|
||||
|
||||
export const StatsManager = ({ className }: StatsManagerProps) => {
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const { data, startPolling, stopPolling } = useStatsQuery();
|
||||
const { data, error, startPolling, stopPolling } = useStatsQuery();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(500);
|
||||
startPolling(1000);
|
||||
return () => stopPolling();
|
||||
}, [startPolling, stopPolling]);
|
||||
});
|
||||
|
||||
const getValue = (field: keyof NodeData | keyof Statistics) =>
|
||||
['stakedTotal', 'totalNodes', 'inactiveNodes'].includes(field)
|
||||
? data?.nodeData?.[field as keyof NodeData]
|
||||
: data?.statistics?.[field as keyof Statistics];
|
||||
const displayData = compileData(data);
|
||||
|
||||
const panels = fieldsDefinition.map(
|
||||
({ field, title, description, formatter, goodThreshold }) => ({
|
||||
field,
|
||||
title,
|
||||
description,
|
||||
value: formatter ? formatter(getValue(field)) : getValue(field),
|
||||
good:
|
||||
goodThreshold && getValue(field)
|
||||
? goodThreshold(getValue(field))
|
||||
: undefined,
|
||||
})
|
||||
const classes = classnames(
|
||||
className,
|
||||
'stats-grid w-full self-start justify-self-center'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'grid grid-cols-2 md:grid-cols-3 gap-3 w-full self-start justify-self-center',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{panels.map(({ field, title, description, value, good }, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={classNames(
|
||||
'border rounded p-2 relative border-vega-light-200 dark:border-vega-dark-200',
|
||||
{
|
||||
'col-span-2': field === 'chainId' || field === 'status',
|
||||
},
|
||||
{
|
||||
'bg-transparent border-vega-light-200 dark:border-vega-dark-200':
|
||||
good === undefined,
|
||||
'bg-vega-pink-300 dark:bg-vega-pink-700 border-vega-pink-500 dark:border-vega-pink-500':
|
||||
good !== undefined && !good,
|
||||
'bg-vega-green-300 dark:bg-vega-green-700 border-vega-green-500 dark:border-vega-green-500':
|
||||
good !== undefined && good,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="uppercase flex items-center gap-2 text-xs font-alpha calt">
|
||||
<div
|
||||
className={classNames('w-2 h-2 rounded-full', {
|
||||
'bg-vega-light-150 dark:bg-vega-dark-150': good === undefined,
|
||||
'bg-vega-pink dark:bg-vega-pink': good !== undefined && !good,
|
||||
'bg-vega-green dark:bg-vega-green': good !== undefined && good,
|
||||
})}
|
||||
></div>
|
||||
<div data-testid="stats-title">{title}</div>
|
||||
{description && (
|
||||
<Tooltip description={description} align="center">
|
||||
<div className="absolute top-1 right-2 text-vega-light-200 dark:text-vega-dark-200 cursor-help">
|
||||
<Icon name="info-sign" size={3} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div data-testid="stats-value" className="font-mono text-xl pt-2">
|
||||
{value} {field === 'status' && `(${VEGA_ENV})`}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className={classes}>
|
||||
<h3
|
||||
data-testid="stats-environment"
|
||||
className="font-alpha calt uppercase text-2xl pb-8 col-span-full"
|
||||
>
|
||||
{(error && `/ ${error}`) ||
|
||||
(data ? `/ ${VEGA_ENV}` : '/ Connecting...')}
|
||||
</h3>
|
||||
|
||||
{displayData?.promoted ? (
|
||||
<PromotedStats>
|
||||
{displayData.promoted.map((stat, i) => {
|
||||
return (
|
||||
<PromotedStatsItem
|
||||
title={stat.title}
|
||||
value={stat.value || '-'}
|
||||
formatter={stat.formatter}
|
||||
goodThreshold={stat.goodThreshold}
|
||||
description={stat.description}
|
||||
key={i}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</PromotedStats>
|
||||
) : null}
|
||||
|
||||
<Table>
|
||||
{displayData?.table
|
||||
? displayData.table.map((stat, i) => {
|
||||
return (
|
||||
<TableRow
|
||||
title={stat.title}
|
||||
value={stat.value || '-'}
|
||||
formatter={stat.formatter}
|
||||
goodThreshold={stat.goodThreshold}
|
||||
description={stat.description}
|
||||
key={i}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { TableRow, defaultFieldFormatter } from './table-row';
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { StatFields } from '../../config/types';
|
||||
import { useMemo } from 'react';
|
||||
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const defaultFieldFormatter = (field: unknown) =>
|
||||
field === undefined ? 'no data' : field;
|
||||
|
||||
export const TableRow = ({
|
||||
title,
|
||||
formatter,
|
||||
goodThreshold,
|
||||
value,
|
||||
description,
|
||||
...props
|
||||
}: StatFields) => {
|
||||
const variant = useMemo(
|
||||
() =>
|
||||
goodThreshold
|
||||
? goodThreshold(value)
|
||||
? Intent.Success
|
||||
: Intent.Danger
|
||||
: Intent.None,
|
||||
[goodThreshold, value]
|
||||
);
|
||||
return (
|
||||
<Tooltip description={description} align="start">
|
||||
<tr className="border border-black dark:border-white">
|
||||
<td data-testid="stats-title" className="py-2 px-4">
|
||||
{title}
|
||||
</td>
|
||||
<td data-testid="stats-value" className="py-2 px-4 text-right">
|
||||
{formatter ? formatter(value) : defaultFieldFormatter(value)}
|
||||
</td>
|
||||
<td className="py-2 px-4">
|
||||
<Indicator variant={variant} />
|
||||
</td>
|
||||
</tr>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user