feat: update market data components when they are in view (#2607)

This commit is contained in:
Bartłomiej Głownia 2023-01-16 18:51:30 +01:00 committed by GitHub
parent 4f7b589cbd
commit 5a0da4e158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 443 additions and 473 deletions

View File

@ -215,7 +215,9 @@ export const useOrderValidation = ({
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={<DataGrid grid={compileGridData(market)} />}
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
@ -237,7 +239,9 @@ export const useOrderValidation = ({
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={<DataGrid grid={compileGridData(market)} />}
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
@ -276,7 +280,9 @@ export const useOrderValidation = ({
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={<DataGrid grid={compileGridData(market)} />}
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
@ -300,7 +306,9 @@ export const useOrderValidation = ({
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={<DataGrid grid={compileGridData(market)} />}
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>

View File

@ -1,27 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL subscription operation: CandleLive
// ====================================================
export interface CandleLive_candles {
__typename: "Candle";
/**
* Close price (uint64)
*/
close: string;
}
export interface CandleLive {
/**
* Subscribe to the candles updates
*/
candles: CandleLive_candles;
}
export interface CandleLiveVariables {
marketId: string;
}

View File

@ -1,43 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type CandleLiveSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type CandleLiveSubscription = { __typename?: 'Subscription', candles: { __typename?: 'Candle', close: string } };
export const CandleLiveDocument = gql`
subscription CandleLive($marketId: ID!) {
candles(marketId: $marketId, interval: INTERVAL_I1H) {
close
}
}
`;
/**
* __useCandleLiveSubscription__
*
* To run a query within a React component, call `useCandleLiveSubscription` and pass it any options that fit your needs.
* When your component renders, `useCandleLiveSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useCandleLiveSubscription({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useCandleLiveSubscription(baseOptions: Apollo.SubscriptionHookOptions<CandleLiveSubscription, CandleLiveSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<CandleLiveSubscription, CandleLiveSubscriptionVariables>(CandleLiveDocument, options);
}
export type CandleLiveSubscriptionHookResult = ReturnType<typeof useCandleLiveSubscription>;
export type CandleLiveSubscriptionResult = Apollo.SubscriptionResult<CandleLiveSubscription>;

View File

@ -15,7 +15,7 @@ import {
import type { Candle } from '@vegaprotocol/market-list';
import { marketCandlesProvider } from '@vegaprotocol/market-list';
const DEBOUNCE_UPDATE_TIME = 500;
const THROTTLE_UPDATE_TIME = 500;
export const Last24hVolume = ({
marketId,
@ -54,7 +54,7 @@ export const Last24hVolume = ({
const throttledSetCandles = useRef(
throttle((data: Candle[]) => {
setCandleVolume(calcDayVolume(data));
}, DEBOUNCE_UPDATE_TIME)
}, THROTTLE_UPDATE_TIME)
).current;
const update = useCallback(
@ -78,7 +78,7 @@ export const Last24hVolume = ({
throttle((candles: Candle[]) => {
const candle24hAgo = candles?.[0];
setVolumeChange(getChange(data || [], candle24hAgo?.close));
}, DEBOUNCE_UPDATE_TIME)
}, THROTTLE_UPDATE_TIME)
).current;
const updateCandle24hAgo = useCallback(

View File

@ -1 +0,0 @@
window._env_ = {};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@ -16,7 +16,7 @@ import { MarketMarkPrice } from '../../components/market-mark-price';
import { Last24hPriceChange } from '../../components/last-24h-price-change';
import { Last24hVolume } from '../../components/last-24h-volume';
import { MarketState } from '../../components/market-state';
import { MarketTradingMode } from '../../components/market-trading-mode';
import { HeaderStatMarketTradingMode } from '../../components/market-trading-mode';
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
@ -64,22 +64,35 @@ export const TradeMarketHeader = ({
>
<ExpiryLabel market={market} />
</HeaderStat>
<MarketMarkPrice
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total amount of assets traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
isHeader
onSelect={onSelect}
initialTradingMode={market?.tradingMode}
/>
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
isHeader
/>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
isHeader
/>
<MarketTradingMode marketId={market?.id} onSelect={onSelect} isHeader />
<MarketState market={market} />
{asset ? (
<HeaderStat

View File

@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/react-helpers';
export const DEBOUNCE_UPDATE_TIME = 500;
export const THROTTLE_UPDATE_TIME = 500;
export const RISK_ACCEPTED_KEY = 'vega-risk-accepted';
export const MAINNET_WELCOME_HEADER = t(
'Trade cash settled futures on the fully decentralised Vega network.'

View File

@ -1,9 +1,9 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import throttle from 'lodash/throttle';
import type { RefObject } from 'react';
import { useMemo } from 'react';
import { useInView } from 'react-intersection-observer';
import {
isNumeric,
t,
useDataProvider,
useThrottledDataProvider,
useYesterday,
} from '@vegaprotocol/react-helpers';
import { PriceCellChange } from '@vegaprotocol/ui-toolkit';
@ -11,8 +11,7 @@ import * as Schema from '@vegaprotocol/types';
import type { CandleClose } from '@vegaprotocol/types';
import type { Candle } from '@vegaprotocol/market-list';
import { marketCandlesProvider } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import * as constants from '../constants';
import { THROTTLE_UPDATE_TIME } from '../constants';
interface Props {
marketId?: string;
@ -20,75 +19,48 @@ interface Props {
initialValue?: string[];
isHeader?: boolean;
noUpdate?: boolean;
inViewRoot?: RefObject<Element>;
}
export const Last24hPriceChange = ({
marketId,
decimalPlaces,
initialValue,
isHeader = false,
noUpdate = false,
inViewRoot,
}: Props) => {
const [candlesClose, setCandlesClose] = useState<string[]>(
initialValue || []
);
const [ref, inView] = useInView({ root: inViewRoot?.current });
const yesterday = useYesterday();
// Cache timestamp for yesterday to prevent full unmount of market page when
// a rerender occurs
const yTimestamp = useMemo(() => {
return new Date(yesterday).toISOString();
}, [yesterday]);
const variables = useMemo(
() => ({
marketId: marketId,
interval: Schema.Interval.INTERVAL_I1H,
since: yTimestamp,
since: new Date(yesterday).toISOString(),
}),
[marketId, yTimestamp]
[marketId, yesterday]
);
const throttledSetCandles = useRef(
throttle((data: Candle[]) => {
if (!noUpdate) {
const candlesClose: string[] = data
.map((candle) => candle?.close)
.filter((c): c is CandleClose => c !== null);
setCandlesClose(candlesClose);
}
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetCandles(data);
}
return true;
const { data, error } = useThrottledDataProvider<Candle[], Candle>(
{
dataProvider: marketCandlesProvider,
variables,
skip: !marketId || !inView,
},
[throttledSetCandles]
THROTTLE_UPDATE_TIME
);
const { error } = useDataProvider<Candle[], Candle>({
dataProvider: marketCandlesProvider,
update,
variables,
skip: noUpdate || !marketId,
});
const candles =
data
?.map((candle) => candle?.close)
.filter((c): c is CandleClose => c !== null) || initialValue;
const content = useMemo(() => {
if (error || !isNumeric(decimalPlaces)) {
return <>-</>;
}
return (
<PriceCellChange candles={candlesClose} decimalPlaces={decimalPlaces} />
);
}, [candlesClose, decimalPlaces, error]);
return isHeader ? (
<HeaderStat heading={t('Change (24h)')} testId="market-change">
{content}
</HeaderStat>
) : (
content
if (error || !isNumeric(decimalPlaces)) {
return <span ref={ref}>-</span>;
}
return (
<PriceCellChange
candles={candles || []}
decimalPlaces={decimalPlaces}
ref={ref}
/>
);
};

View File

@ -1,102 +1,65 @@
import type { RefObject } from 'react';
import { useInView } from 'react-intersection-observer';
import {
calcCandleVolume,
marketCandlesProvider,
} from '@vegaprotocol/market-list';
import {
addDecimalsFormatNumber,
t,
useDataProvider,
useThrottledDataProvider,
useYesterday,
isNumeric,
} from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import throttle from 'lodash/throttle';
import { useCallback, useMemo, useRef, useState } from 'react';
import * as constants from '../constants';
import { HeaderStat } from '../header';
import { useMemo } from 'react';
import type { Candle } from '@vegaprotocol/market-list';
import { THROTTLE_UPDATE_TIME } from '../constants';
interface Props {
marketId?: string;
positionDecimalPlaces?: number;
noUpdate?: boolean;
isHeader?: boolean;
formatDecimals?: number;
inViewRoot?: RefObject<Element>;
initialValue?: string;
}
export const Last24hVolume = ({
marketId,
positionDecimalPlaces,
noUpdate = false,
isHeader = false,
formatDecimals,
inViewRoot,
initialValue,
}: Props) => {
const [candleVolume, setCandleVolume] = useState<string>(initialValue || '');
const yesterday = useYesterday();
// Cache timestamp for yesterday to prevent full unmount of market page when
// a rerender occurs
const yTimestamp = useMemo(() => {
return new Date(yesterday).toISOString();
}, [yesterday]);
const [ref, inView] = useInView({ root: inViewRoot?.current });
const variables = useMemo(
() => ({
marketId: marketId,
interval: Schema.Interval.INTERVAL_I1H,
since: yTimestamp,
since: new Date(yesterday).toISOString(),
}),
[marketId, yTimestamp]
[marketId, yesterday]
);
const throttledSetCandles = useRef(
throttle((data: Candle[]) => {
noUpdate || setCandleVolume(calcCandleVolume(data) || '');
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetCandles(data);
}
return true;
const { data } = useThrottledDataProvider<Candle[], Candle>(
{
dataProvider: marketCandlesProvider,
variables,
skip: !(inView && marketId),
},
[throttledSetCandles]
THROTTLE_UPDATE_TIME
);
const { error } = useDataProvider<Candle[], Candle>({
dataProvider: marketCandlesProvider,
update,
variables,
skip: noUpdate || !marketId,
});
const formatDecimals = isHeader ? positionDecimalPlaces || 0 : 2;
const content = useMemo(() => {
return (
<>
{!error && candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-'}
</>
);
}, [error, candleVolume, positionDecimalPlaces, formatDecimals]);
return isHeader ? (
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={
error && candleVolume && positionDecimalPlaces
? t('The total amount of assets traded in the last 24 hours.')
: null
}
>
{content}
</HeaderStat>
) : (
content
const candleVolume = data ? calcCandleVolume(data) : initialValue;
return (
<span ref={ref}>
{candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-'}
</span>
);
};

View File

@ -1,10 +1,10 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import throttle from 'lodash/throttle';
import type { RefObject } from 'react';
import { useMemo } from 'react';
import { useInView } from 'react-intersection-observer';
import {
addDecimalsFormatNumber,
t,
PriceCell,
useDataProvider,
useThrottledDataProvider,
isNumeric,
} from '@vegaprotocol/react-helpers';
import type {
@ -12,14 +12,13 @@ import type {
MarketDataUpdateFieldsFragment,
} from '@vegaprotocol/market-list';
import { marketDataProvider } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import * as constants from '../constants';
import { THROTTLE_UPDATE_TIME } from '../constants';
interface Props {
marketId?: string;
decimalPlaces?: number;
isHeader?: boolean;
noUpdate?: boolean;
asPriceCell?: boolean;
inViewRoot?: RefObject<Element>;
initialValue?: string;
}
@ -27,58 +26,39 @@ export const MarketMarkPrice = ({
marketId,
decimalPlaces,
initialValue,
isHeader = false,
noUpdate = false,
inViewRoot,
asPriceCell,
}: Props) => {
const [marketPrice, setMarketPrice] = useState<string | null>(
initialValue || null
);
const variables = useMemo(
() => ({
marketId: marketId,
}),
[marketId]
);
const [ref, inView] = useInView({ root: inViewRoot?.current });
const variables = useMemo(() => ({ marketId }), [marketId]);
const throttledSetMarketPrice = useRef(
throttle((price: string) => {
noUpdate || setMarketPrice(price);
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data: marketData }: { data: MarketData | null }) => {
throttledSetMarketPrice(marketData?.markPrice || '');
return true;
const { data } = useThrottledDataProvider<
MarketData,
MarketDataUpdateFieldsFragment
>(
{
dataProvider: marketDataProvider,
variables,
skip: !inView,
},
[throttledSetMarketPrice]
THROTTLE_UPDATE_TIME
);
useDataProvider<MarketData, MarketDataUpdateFieldsFragment>({
dataProvider: marketDataProvider,
update,
variables,
skip: noUpdate || !marketId,
});
const marketPrice = data?.markPrice || initialValue;
const content = useMemo(() => {
if (!marketPrice || !isNumeric(decimalPlaces)) {
return <>-</>;
}
return isHeader ? (
<div>{addDecimalsFormatNumber(marketPrice, decimalPlaces)}</div>
) : (
if (!marketPrice || !isNumeric(decimalPlaces)) {
return <span ref={ref}>-</span>;
}
if (asPriceCell) {
return (
<PriceCell
ref={ref}
value={Number(marketPrice)}
valueFormatted={addDecimalsFormatNumber(marketPrice, decimalPlaces, 2)}
/>
);
}, [marketPrice, decimalPlaces, isHeader]);
return isHeader ? (
<HeaderStat heading={t('Price')} testId="market-price">
{content}
</HeaderStat>
) : (
content
}
return (
<span ref={ref}>{addDecimalsFormatNumber(marketPrice, decimalPlaces)}</span>
);
};

View File

@ -23,7 +23,7 @@ export const MarketState = ({
const throttledSetMarketState = useRef(
throttle((state: Schema.MarketState) => {
setMarketState(state);
}, constants.DEBOUNCE_UPDATE_TIME)
}, constants.THROTTLE_UPDATE_TIME)
).current;
const update = useCallback(

View File

@ -1,110 +1,78 @@
import { useCallback, useMemo, useState } from 'react';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import { compileGridData, TradingModeTooltip } from '@vegaprotocol/deal-ticket';
import type { RefObject } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { TradingModeTooltip } from '@vegaprotocol/deal-ticket';
import { useInView } from 'react-intersection-observer';
import * as Schema from '@vegaprotocol/types';
import type {
MarketData,
MarketDataUpdateFieldsFragment,
SingleMarketFieldsFragment,
} from '@vegaprotocol/market-list';
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import { useStaticMarketData } from '@vegaprotocol/market-list';
import { HeaderStat } from '../header';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
interface Props {
marketId?: string;
onSelect?: (marketId: string) => void;
isHeader?: boolean;
noUpdate?: boolean;
initialMode?: Schema.MarketTradingMode;
initialTrigger?: Schema.AuctionTrigger;
}
export const MarketTradingMode = ({
marketId,
onSelect,
isHeader = false,
noUpdate = false,
initialMode,
initialTrigger,
}: Props) => {
const [tradingMode, setTradingMode] =
useState<Schema.MarketTradingMode | null>(initialMode || null);
const [trigger, setTrigger] = useState<Schema.AuctionTrigger | null>(
initialTrigger || null
);
const [market, setMarket] = useState<MarketDealTicket | null>(null);
const variables = useMemo(
() => ({
marketId: marketId,
}),
[marketId]
);
const { data } = useDataProvider<SingleMarketFieldsFragment, never>({
dataProvider: marketProvider,
variables,
skip: !marketId,
});
const update = useCallback(
({ data: marketData }: { data: MarketData | null }) => {
if (!noUpdate && marketData) {
setTradingMode(marketData.marketTradingMode);
setTrigger(marketData.trigger);
setMarket({
...data,
data: marketData,
} as MarketDealTicket);
}
return true;
},
[noUpdate, data]
);
useDataProvider<MarketData, MarketDataUpdateFieldsFragment>({
dataProvider: marketDataProvider,
update,
variables,
skip: noUpdate || !marketId || !data,
});
const content =
const getTradingModeLabel = (
tradingMode?: Schema.MarketTradingMode,
trigger?: Schema.AuctionTrigger
) => {
return (
(tradingMode === Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[tradingMode]} - ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[
tradingMode as Schema.MarketTradingMode
]) || '-';
]) || '-'
);
};
return isHeader ? (
interface HeaderStatMarketTradingModeProps {
marketId?: string;
onSelect?: (marketId: string) => void;
initialTradingMode?: Schema.MarketTradingMode;
initialTrigger?: Schema.AuctionTrigger;
}
export const HeaderStatMarketTradingMode = ({
marketId,
onSelect,
initialTradingMode,
initialTrigger,
}: HeaderStatMarketTradingModeProps) => {
const data = useStaticMarketData(marketId);
const tradingMode = data?.marketTradingMode ?? initialTradingMode;
const trigger = data?.trigger ?? initialTrigger;
return (
<HeaderStat
heading={t('Trading mode')}
description={
market && (
<TradingModeTooltip
tradingMode={tradingMode}
trigger={trigger}
compiledGrid={compileGridData(market, onSelect)}
/>
)
<TradingModeTooltip marketId={marketId} onSelect={onSelect} />
}
testId="market-trading-mode"
>
<div>{content}</div>
<div>{getTradingModeLabel(tradingMode, trigger)}</div>
</HeaderStat>
) : (
);
};
export const MarketTradingMode = ({
marketId,
initialTradingMode,
initialTrigger,
inViewRoot,
}: Omit<HeaderStatMarketTradingModeProps, 'onUpdate'> & {
inViewRoot?: RefObject<Element>;
}) => {
const [ref, inView] = useInView({ root: inViewRoot?.current });
const data = useStaticMarketData(marketId, !inView);
return (
<Tooltip
description={
tradingMode &&
trigger && (
<TradingModeTooltip tradingMode={tradingMode} trigger={trigger} />
)
}
description={<TradingModeTooltip marketId={marketId} skip={!inView} />}
>
<span>{content}</span>
<span ref={ref}>
{getTradingModeLabel(
data?.marketTradingMode ?? initialTradingMode,
data?.trigger ?? initialTrigger
)}
</span>
</Tooltip>
);
};

View File

@ -31,7 +31,7 @@ export const MarketVolume = ({ marketId }: { marketId: string }) => {
const throttledSetMarketVolume = useRef(
throttle((volume: string) => {
setMarketVolume(volume);
}, constants.DEBOUNCE_UPDATE_TIME)
}, constants.THROTTLE_UPDATE_TIME)
).current;
const update = useCallback(
({ data: marketData }: { data: MarketData | null }) => {

View File

@ -1,3 +1,4 @@
import type { RefObject } from 'react';
import { FeesCell } from '@vegaprotocol/market-info';
import {
calcCandleHigh,
@ -173,7 +174,7 @@ export const columns = (
market: Market,
onSelect: (id: string) => void,
onCellClick: OnCellClickHandler,
activeMarketId?: string | null
inViewRoot?: RefObject<HTMLElement>
) => {
const candlesClose = market.candles
?.map((candle) => candle?.close)
@ -189,7 +190,6 @@ export const columns = (
return onSelect(id);
}
};
const noUpdate = !activeMarketId || market.id !== activeMarketId;
const selectMarketColumns: Column[] = [
{
kind: ColumnKind.Market,
@ -221,8 +221,9 @@ export const columns = (
<MarketMarkPrice
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
initialValue={market.data?.markPrice.toString()}
noUpdate={noUpdate}
initialValue={market.data?.markPrice}
inViewRoot={inViewRoot}
asPriceCell
/>
),
className: `${cellClassNames} max-w-[100px]`,
@ -234,7 +235,7 @@ export const columns = (
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
noUpdate={noUpdate}
inViewRoot={inViewRoot}
initialValue={candlesClose}
/>
),
@ -317,7 +318,8 @@ export const columns = (
marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces}
initialValue={candleVolume}
noUpdate={noUpdate}
inViewRoot={inViewRoot}
formatDecimals={2}
/>
),
className: `${cellClassNames} hidden lg:table-cell font-mono`,
@ -329,8 +331,8 @@ export const columns = (
value: (
<MarketTradingMode
marketId={market?.id}
noUpdate={noUpdate}
initialMode={market.tradingMode}
inViewRoot={inViewRoot}
initialTradingMode={market.tradingMode}
initialTrigger={market.data?.trigger}
/>
),
@ -359,9 +361,9 @@ export const columns = (
export const columnsPositionMarkets = (
market: Market,
onSelect: (id: string) => void,
inViewRoot?: RefObject<HTMLElement>,
openVolume?: string,
onCellClick?: OnCellClickHandler,
activeMarketId?: string | null
onCellClick?: OnCellClickHandler
) => {
const candlesClose = market.candles
?.map((candle) => candle?.close)
@ -377,7 +379,6 @@ export const columnsPositionMarkets = (
}
};
const candleVolume = market.candles && calcCandleVolume(market.candles);
const noUpdate = !activeMarketId || market.id !== activeMarketId;
const selectMarketColumns: Column[] = [
{
kind: ColumnKind.Market,
@ -409,8 +410,9 @@ export const columnsPositionMarkets = (
<MarketMarkPrice
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
initialValue={market.data?.markPrice.toString()}
noUpdate={noUpdate}
inViewRoot={inViewRoot}
initialValue={market.data?.markPrice}
asPriceCell
/>
),
className: cellClassNames,
@ -422,7 +424,7 @@ export const columnsPositionMarkets = (
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
noUpdate={noUpdate}
inViewRoot={inViewRoot}
initialValue={candlesClose}
/>
),
@ -503,9 +505,10 @@ export const columnsPositionMarkets = (
value: (
<Last24hVolume
marketId={market.id}
inViewRoot={inViewRoot}
positionDecimalPlaces={market.positionDecimalPlaces}
initialValue={candleVolume}
noUpdate={noUpdate}
formatDecimals={2}
/>
),
className: `${cellClassNames} hidden lg:table-cell font-mono`,
@ -517,8 +520,8 @@ export const columnsPositionMarkets = (
value: (
<MarketTradingMode
marketId={market?.id}
noUpdate={noUpdate}
initialMode={market.tradingMode}
inViewRoot={inViewRoot}
initialTradingMode={market.tradingMode}
initialTrigger={market.data?.trigger}
/>
),

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import type { RefObject } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
@ -27,7 +28,6 @@ import {
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { useGlobalStore } from '../../stores';
export type Market = MarketWithCandles & MarketWithData;
@ -36,19 +36,22 @@ export const SelectAllMarketsTableBody = ({
positions,
onSelect,
onCellClick,
activeMarketId,
inViewRoot,
headers = columnHeaders,
tableColumns = (market) =>
columns(market, onSelect, onCellClick, activeMarketId),
tableColumns = (market) => columns(market, onSelect, onCellClick, inViewRoot),
}: {
markets?: Market[] | null;
positions?: PositionFieldsFragment[];
title?: string;
onSelect: (id: string) => void;
onCellClick: OnCellClickHandler;
activeMarketId?: string | null;
headers?: Column[];
tableColumns?: (market: Market, openVolume?: string) => Column[];
tableColumns?: (
market: Market,
inViewRoot?: RefObject<HTMLDivElement>,
openVolume?: string
) => Column[];
inViewRoot?: RefObject<HTMLDivElement>;
}) => {
const tokenLink = useLinks(DApp.Token);
if (!markets) return null;
@ -68,6 +71,7 @@ export const SelectAllMarketsTableBody = ({
onSelect={onSelect}
columns={tableColumns(
market,
inViewRoot,
positions &&
positions.find((p) => p.market.id === market.id)?.openVolume
)}
@ -95,11 +99,11 @@ export const SelectMarketPopover = ({
onSelect: (id: string) => void;
onCellClick: OnCellClickHandler;
}) => {
const activeMarketId = useGlobalStore((store) => store.marketId);
const triggerClasses =
'sm:text-lg md:text-xl lg:text-2xl flex items-center gap-2 whitespace-nowrap hover:text-neutral-500 dark:hover:text-neutral-300 mt-1';
const { pubKey } = useVegaWallet();
const [open, setOpen] = useState(false);
const inViewRoot = useRef<HTMLDivElement>(null);
const {
data,
loading: marketsLoading,
@ -155,6 +159,7 @@ export const SelectMarketPopover = ({
<div
className="w-[90vw] max-h-[80vh] overflow-y-auto"
data-testid="select-market-list"
ref={inViewRoot}
>
{marketsLoading || (pubKey && positionsLoading) ? (
<div className="flex items-center gap-4">
@ -167,6 +172,7 @@ export const SelectMarketPopover = ({
<>
<TableTitle>{t('My markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={markets}
positions={party?.positionsConnection?.edges
?.filter((edge) => edge.node)
@ -174,13 +180,13 @@ export const SelectMarketPopover = ({
onSelect={onSelectMarket}
onCellClick={onCellClick}
headers={columnHeadersPositionMarkets}
tableColumns={(market, openVolume) =>
tableColumns={(market, inViewRoot, openVolume) =>
columnsPositionMarkets(
market,
onSelectMarket,
inViewRoot,
openVolume,
onCellClick,
activeMarketId
onCellClick
)
}
/>
@ -188,10 +194,10 @@ export const SelectMarketPopover = ({
) : null}
<TableTitle>{t('All markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={data}
onSelect={onSelectMarket}
onCellClick={onCellClick}
activeMarketId={activeMarketId}
/>
</table>
)}

View File

@ -1,7 +1,9 @@
import '@testing-library/jest-dom';
import 'jest-canvas-mock';
import ResizeObserver from 'resize-observer-polyfill';
import { defaultFallbackInView } from 'react-intersection-observer';
defaultFallbackInView(true);
global.ResizeObserver = ResizeObserver;
// Required by radix-ui/react-tooltip

View File

@ -78,7 +78,11 @@ export const TimeInForceSelector = ({
return (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip description={<DataGrid grid={compileGridData(market)} />}>
<Tooltip
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
@ -93,7 +97,11 @@ export const TimeInForceSelector = ({
return (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip description={<DataGrid grid={compileGridData(market)} />}>
<Tooltip
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}

View File

@ -33,7 +33,11 @@ export const TypeSelector = ({
return (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip description={<DataGrid grid={compileGridData(market)} />}>
<Tooltip
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
@ -46,7 +50,11 @@ export const TypeSelector = ({
return (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip description={<DataGrid grid={compileGridData(market)} />}>
<Tooltip
description={
<DataGrid grid={compileGridData(market, market.data)} />
}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}

View File

@ -8,17 +8,31 @@ import * as Schema from '@vegaprotocol/types';
import { Link as UILink } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import type { MarketDealTicket } from '@vegaprotocol/market-list';
import type { Market, MarketData } from '@vegaprotocol/market-list';
export const compileGridData = (
market: MarketDealTicket,
market: Pick<
Market,
'tradableInstrument' | 'id' | 'decimalPlaces' | 'positionDecimalPlaces'
>,
marketData: Pick<
MarketData,
| 'marketTradingMode'
| 'auctionStart'
| 'auctionEnd'
| 'indicativePrice'
| 'indicativeVolume'
| 'suppliedStake'
| 'targetStake'
| 'trigger'
>,
onSelect?: (id: string) => void
): { label: ReactNode; value?: ReactNode }[] => {
const grid: DataGridProps['grid'] = [];
const isLiquidityMonitoringAuction =
market.data.marketTradingMode ===
marketData.marketTradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
market.data.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
marketData.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
const formatStake = (value: string) => {
const formattedValue = addDecimalsFormatNumber(
@ -30,19 +44,17 @@ export const compileGridData = (
return `${formattedValue} ${asset}`;
};
if (!market.data) return grid;
if (!marketData) return grid;
if (market.data.auctionStart) {
if (marketData.auctionStart) {
grid.push({
label: t('Auction start'),
value: getDateTimeFormat().format(new Date(market.data.auctionStart)),
value: getDateTimeFormat().format(new Date(marketData.auctionStart)),
});
}
if (market.data.auctionEnd) {
const endDate = getDateTimeFormat().format(
new Date(market.data.auctionEnd)
);
if (marketData.auctionEnd) {
const endDate = getDateTimeFormat().format(new Date(marketData.auctionEnd));
grid.push({
label: isLiquidityMonitoringAuction
? t('Est. auction end')
@ -51,14 +63,14 @@ export const compileGridData = (
});
}
if (isLiquidityMonitoringAuction && market.data.targetStake) {
if (isLiquidityMonitoringAuction && marketData.targetStake) {
grid.push({
label: t('Target liquidity'),
value: formatStake(market.data.targetStake),
value: formatStake(marketData.targetStake),
});
}
if (isLiquidityMonitoringAuction && market.data.suppliedStake) {
if (isLiquidityMonitoringAuction && marketData.suppliedStake) {
grid.push({
label: (
<Link
@ -68,31 +80,31 @@ export const compileGridData = (
<UILink>{t('Current liquidity')}</UILink>
</Link>
),
value: formatStake(market.data.suppliedStake),
value: formatStake(marketData.suppliedStake),
});
}
if (market.data.indicativePrice) {
if (marketData.indicativePrice) {
grid.push({
label: t('Est. uncrossing price'),
value:
market.data.indicativePrice && market.data.indicativePrice !== '0'
marketData.indicativePrice && marketData.indicativePrice !== '0'
? `~
${addDecimalsFormatNumber(
market.data.indicativePrice,
marketData.indicativePrice,
market.decimalPlaces
)}`
: '-',
});
}
if (market.data.indicativeVolume) {
if (marketData.indicativeVolume) {
grid.push({
label: t('Est. uncrossing vol'),
value:
market.data.indicativeVolume && market.data.indicativeVolume !== '0'
marketData.indicativeVolume && marketData.indicativeVolume !== '0'
? '~' +
addDecimalsFormatNumber(
market.data.indicativeVolume,
marketData.indicativeVolume,
market.positionDecimalPlaces
)
: '-',

View File

@ -1,23 +1,35 @@
import type { ReactNode } from 'react';
import classNames from 'classnames';
import { useEnvironment } from '@vegaprotocol/environment';
import { DataGrid, t } from '@vegaprotocol/react-helpers';
import * as Schema from '@vegaprotocol/types';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/react-helpers';
import { compileGridData } from './compile-grid-data';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
type TradingModeTooltipProps = {
tradingMode: Schema.MarketTradingMode | null;
trigger: Schema.AuctionTrigger | null;
compiledGrid?: { label: ReactNode; value?: ReactNode }[];
marketId?: string;
onSelect?: (marketId: string) => void;
skip?: boolean;
};
export const TradingModeTooltip = ({
tradingMode,
trigger,
compiledGrid,
marketId,
onSelect,
skip,
}: TradingModeTooltipProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const market = useMarket(marketId);
const marketData = useStaticMarketData(marketId, skip);
if (!market || !marketData) {
return null;
}
const compiledGrid =
onSelect && compileGridData(market, marketData, onSelect);
const { marketTradingMode: tradingMode, trigger } = marketData;
switch (tradingMode) {
case Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS: {
return (

View File

@ -1,4 +1,9 @@
import produce from 'immer';
import { useMemo } from 'react';
import {
makeDerivedDataProvider,
useDataProvider,
} from '@vegaprotocol/react-helpers';
import { makeDataProvider } from '@vegaprotocol/react-helpers';
import {
MarketDataDocument,
@ -39,3 +44,51 @@ export const marketDataProvider = makeDataProvider<
getData,
getDelta,
});
export type StaticMarketData = Pick<
MarketData,
| 'marketTradingMode'
| 'auctionStart'
| 'auctionEnd'
| 'indicativePrice'
| 'indicativeVolume'
| 'suppliedStake'
| 'targetStake'
| 'trigger'
>;
export const staticMarketDataProvider = makeDerivedDataProvider<
StaticMarketData,
never
>([marketDataProvider], (parts, variables, prevData) => {
const marketData = parts[0] as ReturnType<typeof getData>;
if (!marketData) {
return marketData;
}
const data: StaticMarketData = {
marketTradingMode: marketData.marketTradingMode,
auctionStart: marketData.auctionStart,
auctionEnd: marketData.auctionEnd,
indicativePrice: marketData.indicativePrice,
indicativeVolume: marketData.indicativeVolume,
suppliedStake: marketData.suppliedStake,
targetStake: marketData.targetStake,
trigger: marketData.trigger,
};
if (!prevData) {
return data;
}
return produce(prevData, (draft) => {
Object.assign(draft, data);
});
});
export const useStaticMarketData = (marketId?: string, skip?: boolean) => {
const variables = useMemo(() => ({ marketId }), [marketId]);
const { data } = useDataProvider({
dataProvider: staticMarketDataProvider,
variables,
skip: skip || !marketId,
});
return data;
};

View File

@ -35,6 +35,28 @@ export const marketsProvider = makeDataProvider<
fetchPolicy: 'cache-first',
});
const marketProvider = makeDerivedDataProvider<
Market,
never,
{ marketId: string }
>(
[marketsProvider],
([markets], variables) =>
((markets as ReturnType<typeof getData>) || []).find(
(market) => market.id === variables?.marketId
) || null
);
export const useMarket = (marketId?: string) => {
const variables = useMemo(() => ({ marketId: marketId || '' }), [marketId]);
const { data } = useDataProvider({
dataProvider: marketProvider,
variables,
skip: !marketId,
});
return data;
};
export const activeMarketsProvider = makeDerivedDataProvider<Market[], never>(
[marketsProvider],
([markets]) => filterAndSortMarkets(markets)

View File

@ -86,6 +86,8 @@ describe('isNumeric', () => {
{ i: '--123.01', o: false },
{ i: '123.', o: false },
{ i: '123.1.1', o: false },
{ i: BigInt(123), o: true },
{ i: BigInt(-1), o: true },
{ i: new BigNumber(123), o: true },
{ i: new BigNumber(123.123), o: true },
{ i: new BigNumber(123.123).toString(), o: true },
@ -98,7 +100,7 @@ describe('isNumeric', () => {
i,
o,
}: {
i: number | string | undefined | null | BigNumber;
i: number | string | undefined | null | BigNumber | bigint;
o: boolean;
}) => {
expect(isNumeric(i)).toStrictEqual(o);

View File

@ -138,7 +138,7 @@ export const useNumberParts = (
};
export const isNumeric = (
value?: string | number | BigNumber | null
value?: string | number | BigNumber | bigint | null
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
const INFINITY = '∞';

View File

@ -560,7 +560,8 @@ export type CombineDerivedData<
Variables extends OperationVariables = OperationVariables
> = (
data: DerivedPart<Variables>['data'][],
variables?: Variables
variables: Variables | undefined,
prevData: Data | null
) => Data | null;
export type CombineDerivedDelta<
@ -641,7 +642,8 @@ function makeDerivedDataProviderInternal<
const newData = newLoaded
? combineData(
parts.map((part) => part.data),
variables
variables,
data
)
: data;
if (
@ -655,7 +657,7 @@ function makeDerivedDataProviderInternal<
loaded = newLoaded;
const previousData = data;
data = newData;
if (newLoaded) {
if (loaded) {
const updatedPart = parts[updatedPartIndex];
if (updatedPart.isUpdate) {
isUpdate = true;

View File

@ -1,37 +1,41 @@
import React from 'react';
import { getDecimalSeparator } from '../format';
import { memo, forwardRef } from 'react';
import { getDecimalSeparator, isNumeric } from '../format';
export interface IPriceCellProps {
value: number | bigint | null | undefined;
valueFormatted: string;
testId?: string;
}
export const PriceCell = React.memo(
({ value, valueFormatted, testId }: IPriceCellProps) => {
if (
(!value && value !== 0) ||
(typeof value === 'number' && isNaN(Number(value)))
) {
return <span data-testid="price">-</span>;
export const PriceCell = memo(
forwardRef<HTMLSpanElement, IPriceCellProps>(
({ value, valueFormatted, testId }: IPriceCellProps, ref) => {
if (!isNumeric(value)) {
return (
<span data-testid="price" ref={ref}>
-
</span>
);
}
const decimalSeparator = getDecimalSeparator();
const valueSplit: string[] = decimalSeparator
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
: [`${value}`];
return (
<span
ref={ref}
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
data-testid={testId || 'price'}
title={valueFormatted}
>
{valueSplit[0]}
{valueSplit[1] ? decimalSeparator : null}
{valueSplit[1] ? (
<span className="opacity-60">{valueSplit[1]}</span>
) : null}
</span>
);
}
const decimalSeparator = getDecimalSeparator();
const valueSplit: string[] = decimalSeparator
? valueFormatted.split(decimalSeparator).map((v) => `${v}`)
: [`${value}`];
return (
<span
className="font-mono relative text-black dark:text-white whitespace-nowrap overflow-hidden text-ellipsis text-right rtl-dir"
data-testid={testId || 'price'}
title={valueFormatted}
>
{valueSplit[0]}
{valueSplit[1] ? decimalSeparator : null}
{valueSplit[1] ? (
<span className="opacity-60">{valueSplit[1]}</span>
) : null}
</span>
);
}
)
);
PriceCell.displayName = 'PriceCell';

View File

@ -3,7 +3,7 @@ import {
formatNumberPercentage,
} from '@vegaprotocol/react-helpers';
import BigNumber from 'bignumber.js';
import React from 'react';
import { memo, forwardRef } from 'react';
import { signedNumberCssClass } from '@vegaprotocol/react-helpers';
import { Arrow } from '../arrows/arrow';
@ -36,30 +36,33 @@ export const priceChange = (candles: string[]) => {
: 0;
};
export const PriceCellChange = React.memo(
({ candles, decimalPlaces }: PriceChangeCellProps) => {
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
return (
<span
className={`${signedNumberCssClass(
change
)} flex items-center gap-2 justify-end font-mono text-ui-small`}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">
{formatNumberPercentage(
new BigNumber(changePercentage.toString()),
2
)}
&nbsp;
export const PriceCellChange = memo(
forwardRef<HTMLSpanElement, PriceChangeCellProps>(
({ candles, decimalPlaces }: PriceChangeCellProps, ref) => {
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
return (
<span
ref={ref}
className={`${signedNumberCssClass(
change
)} flex items-center gap-2 justify-end font-mono text-ui-small`}
>
<Arrow value={change} />
<span data-testid="price-change-percentage">
{formatNumberPercentage(
new BigNumber(changePercentage.toString()),
2
)}
&nbsp;
</span>
<span data-testid="price-change">
{addDecimalsFormatNumber(change.toString(), decimalPlaces ?? 0, 3)}
</span>
</span>
<span data-testid="price-change">
{addDecimalsFormatNumber(change.toString(), decimalPlaces ?? 0, 3)}
</span>
</span>
);
}
);
}
)
);
PriceCellChange.displayName = 'PriceCellChange';