chore(markets): keep market list live updated (#3954)
This commit is contained in:
parent
0f2b86770a
commit
702ea0c6b4
@ -11,6 +11,8 @@ import type {
|
|||||||
UpdateCallback,
|
UpdateCallback,
|
||||||
Update,
|
Update,
|
||||||
PageInfo,
|
PageInfo,
|
||||||
|
Reload,
|
||||||
|
Load,
|
||||||
} from './generic-data-provider';
|
} from './generic-data-provider';
|
||||||
import type {
|
import type {
|
||||||
ApolloClient,
|
ApolloClient,
|
||||||
@ -600,6 +602,17 @@ describe('data provider', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('derived data provider', () => {
|
describe('derived data provider', () => {
|
||||||
|
let subscription: {
|
||||||
|
unsubscribe: () => void;
|
||||||
|
reload: Reload;
|
||||||
|
flush: () => void;
|
||||||
|
load?: Load<CombinedData>;
|
||||||
|
};
|
||||||
|
afterEach(() => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
it('memoize instance and unsubscribe if no subscribers', () => {
|
it('memoize instance and unsubscribe if no subscribers', () => {
|
||||||
clientSubscribeSubscribe.mockClear();
|
clientSubscribeSubscribe.mockClear();
|
||||||
clientSubscribeUnsubscribe.mockClear();
|
clientSubscribeUnsubscribe.mockClear();
|
||||||
@ -628,7 +641,7 @@ describe('derived data provider', () => {
|
|||||||
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
||||||
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
||||||
>();
|
>();
|
||||||
const subscription = derivedSubscribe(callback, client, variables);
|
subscription = derivedSubscribe(callback, client, variables);
|
||||||
const data = { totalCount: 0 };
|
const data = { totalCount: 0 };
|
||||||
combineData.mockReturnValueOnce(data);
|
combineData.mockReturnValueOnce(data);
|
||||||
expect(callback.mock.calls.length).toBe(0);
|
expect(callback.mock.calls.length).toBe(0);
|
||||||
@ -642,7 +655,6 @@ describe('derived data provider', () => {
|
|||||||
expect(callback.mock.calls.length).toBe(1);
|
expect(callback.mock.calls.length).toBe(1);
|
||||||
expect(callback.mock.calls[0][0].data).toBe(data);
|
expect(callback.mock.calls[0][0].data).toBe(data);
|
||||||
expect(callback.mock.calls[0][0].loading).toBe(false);
|
expect(callback.mock.calls[0][0].loading).toBe(false);
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('callback with error if any dependency has error, reloads all dependencies on reload', async () => {
|
it('callback with error if any dependency has error, reloads all dependencies on reload', async () => {
|
||||||
@ -655,7 +667,7 @@ describe('derived data provider', () => {
|
|||||||
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
||||||
>();
|
>();
|
||||||
expect(callback.mock.calls.length).toBe(0);
|
expect(callback.mock.calls.length).toBe(0);
|
||||||
const subscription = derivedSubscribe(callback, client, variables);
|
subscription = derivedSubscribe(callback, client, variables);
|
||||||
const data = { totalCount: 0 };
|
const data = { totalCount: 0 };
|
||||||
combineData.mockReturnValueOnce(data);
|
combineData.mockReturnValueOnce(data);
|
||||||
expect(callback.mock.calls.length).toBe(0);
|
expect(callback.mock.calls.length).toBe(0);
|
||||||
@ -678,7 +690,6 @@ describe('derived data provider', () => {
|
|||||||
expect(callback.mock.calls[2][0].data).toStrictEqual(data);
|
expect(callback.mock.calls[2][0].data).toStrictEqual(data);
|
||||||
expect(callback.mock.calls[2][0].loading).toBe(false);
|
expect(callback.mock.calls[2][0].loading).toBe(false);
|
||||||
expect(callback.mock.calls[2][0].error).toBeUndefined();
|
expect(callback.mock.calls[2][0].error).toBeUndefined();
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pass isUpdate on any dependency isUpdate, uses result of combineDelta as delta in next callback', async () => {
|
it('pass isUpdate on any dependency isUpdate, uses result of combineDelta as delta in next callback', async () => {
|
||||||
@ -690,7 +701,7 @@ describe('derived data provider', () => {
|
|||||||
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
||||||
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
||||||
>();
|
>();
|
||||||
const subscription = derivedSubscribe(callback, client, variables);
|
subscription = derivedSubscribe(callback, client, variables);
|
||||||
const data = { totalCount: 0 };
|
const data = { totalCount: 0 };
|
||||||
combineData.mockReturnValueOnce(data);
|
combineData.mockReturnValueOnce(data);
|
||||||
await resolveQuery({ data: part1 });
|
await resolveQuery({ data: part1 });
|
||||||
@ -711,7 +722,6 @@ describe('derived data provider', () => {
|
|||||||
expect(callback).toBeCalledTimes(2);
|
expect(callback).toBeCalledTimes(2);
|
||||||
expect(callback.mock.calls[1][0].isUpdate).toBe(true);
|
expect(callback.mock.calls[1][0].isUpdate).toBe(true);
|
||||||
expect(callback.mock.calls[1][0].delta).toBe(combinedDelta);
|
expect(callback.mock.calls[1][0].delta).toBe(combinedDelta);
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pass isInsert on any dependency isInsert, uses result of combineInsertionData as insertionData in next callback', async () => {
|
it('pass isInsert on any dependency isInsert, uses result of combineInsertionData as insertionData in next callback', async () => {
|
||||||
@ -722,7 +732,7 @@ describe('derived data provider', () => {
|
|||||||
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
|
||||||
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
Parameters<UpdateCallback<CombinedData, CombinedData>>
|
||||||
>();
|
>();
|
||||||
const subscription = derivedSubscribe(callback, client, variables);
|
subscription = derivedSubscribe(callback, client, variables);
|
||||||
const data = { totalCount: 0 };
|
const data = { totalCount: 0 };
|
||||||
combineData.mockReturnValueOnce(data);
|
combineData.mockReturnValueOnce(data);
|
||||||
await resolveQuery({
|
await resolveQuery({
|
||||||
@ -758,6 +768,5 @@ describe('derived data provider', () => {
|
|||||||
expect(callback).toBeCalledTimes(2);
|
expect(callback).toBeCalledTimes(2);
|
||||||
expect(callback.mock.calls[1][0].isInsert).toBe(true);
|
expect(callback.mock.calls[1][0].isInsert).toBe(true);
|
||||||
expect(callback.mock.calls[1][0].insertionData).toBe(combinedInsertionData);
|
expect(callback.mock.calls[1][0].insertionData).toBe(combinedInsertionData);
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -443,8 +443,8 @@ function makeDataProviderInternal<
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// hard reset on demand or when there is no apollo subscription yet
|
// hard reset on demand or when error occurs
|
||||||
if (forceReset || !subscription) {
|
if (forceReset || error) {
|
||||||
reset();
|
reset();
|
||||||
initialize();
|
initialize();
|
||||||
} else {
|
} else {
|
||||||
|
@ -104,9 +104,10 @@ export const useDataProvider = <
|
|||||||
totalCount,
|
totalCount,
|
||||||
isInsert,
|
isInsert,
|
||||||
isUpdate,
|
isUpdate,
|
||||||
|
loaded,
|
||||||
} = args;
|
} = args;
|
||||||
setError(error);
|
setError(error);
|
||||||
setLoading(loading);
|
setLoading(!loaded && loading);
|
||||||
// if update or insert function returns true it means that component handles updates
|
// if update or insert function returns true it means that component handles updates
|
||||||
// component can use flush() which will call callback without delta and cause data state update
|
// component can use flush() which will call callback without delta and cause data state update
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
@ -93,15 +93,14 @@ export const MarketListTable = forwardRef<
|
|||||||
data,
|
data,
|
||||||
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
|
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
|
||||||
if (!data?.data) return undefined;
|
if (!data?.data) return undefined;
|
||||||
const { trigger } = data.data;
|
const { trigger, marketTradingMode } = data.data;
|
||||||
const { tradingMode } = data;
|
return marketTradingMode ===
|
||||||
return tradingMode ===
|
|
||||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
|
||||||
trigger &&
|
trigger &&
|
||||||
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
|
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
|
||||||
? `${Schema.MarketTradingModeMapping[tradingMode]}
|
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
|
||||||
- ${Schema.AuctionTriggerMapping[trigger]}`
|
- ${Schema.AuctionTriggerMapping[trigger]}`
|
||||||
: Schema.MarketTradingModeMapping[tradingMode];
|
: Schema.MarketTradingModeMapping[marketTradingMode];
|
||||||
}}
|
}}
|
||||||
filter={SetFilter}
|
filter={SetFilter}
|
||||||
filterParams={{
|
filterParams={{
|
||||||
@ -228,6 +227,7 @@ export const MarketListTable = forwardRef<
|
|||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
colId="market-actions"
|
colId="market-actions"
|
||||||
|
field="id"
|
||||||
{...COL_DEFS.actions}
|
{...COL_DEFS.actions}
|
||||||
cellRenderer={({
|
cellRenderer={({
|
||||||
data,
|
data,
|
||||||
|
@ -1,37 +1,60 @@
|
|||||||
import type { MouseEvent } from 'react';
|
import type { MouseEvent } from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { CellClickedEvent } from 'ag-grid-community';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { MarketListTable } from './market-list-table';
|
import { MarketListTable } from './market-list-table';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import type { CellClickedEvent } from 'ag-grid-community';
|
|
||||||
import { marketsWithDataProvider as dataProvider } from '../../markets-provider';
|
import { marketsWithDataProvider as dataProvider } from '../../markets-provider';
|
||||||
import type { MarketMaybeWithData } from '../../markets-provider';
|
import type { MarketMaybeWithData } from '../../markets-provider';
|
||||||
|
|
||||||
|
const POLLING_TIME = 2000;
|
||||||
interface MarketsContainerProps {
|
interface MarketsContainerProps {
|
||||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const [dataCount, setDataCount] = useState(0);
|
const dataRef = useRef<MarketMaybeWithData[] | null>(null);
|
||||||
const { data, error, loading, reload } = useDataProvider({
|
const [dataCount, setDataCount] = useState(1);
|
||||||
dataProvider,
|
const handleDataCount = useCallback(() => {
|
||||||
skipUpdates: true,
|
|
||||||
variables: undefined,
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
|
||||||
}, [data]);
|
|
||||||
const onFilterChanged = useCallback(() => {
|
|
||||||
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
|
||||||
}, []);
|
}, []);
|
||||||
|
const update = useCallback(
|
||||||
|
({ data }: { data: MarketMaybeWithData[] | null }) => {
|
||||||
|
data && gridRef.current?.api?.setRowData(data);
|
||||||
|
dataRef.current = data;
|
||||||
|
handleDataCount();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[handleDataCount]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, loading, reload } = useDataProvider({
|
||||||
|
dataProvider,
|
||||||
|
variables: undefined,
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
reload();
|
||||||
|
}, POLLING_TIME);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const handleOnGridReady = useCallback(() => {
|
||||||
|
dataRef?.current && update({ data: dataRef.current });
|
||||||
|
handleDataCount();
|
||||||
|
}, [handleDataCount, update]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full relative">
|
<div className="h-full relative">
|
||||||
<MarketListTable
|
<MarketListTable
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
rowData={data}
|
|
||||||
suppressLoadingOverlay
|
suppressLoadingOverlay
|
||||||
suppressNoRowsOverlay
|
suppressNoRowsOverlay
|
||||||
onCellClicked={(cellEvent: CellClickedEvent) => {
|
onCellClicked={(cellEvent: CellClickedEvent) => {
|
||||||
@ -44,6 +67,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
|||||||
'id',
|
'id',
|
||||||
'tradableInstrument.instrument.code',
|
'tradableInstrument.instrument.code',
|
||||||
'tradableInstrument.instrument.product.settlementAsset',
|
'tradableInstrument.instrument.product.settlementAsset',
|
||||||
|
'tradableInstrument.instrument.product.settlementAsset.symbol',
|
||||||
].includes(colId)
|
].includes(colId)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -55,13 +79,14 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
onMarketClick={onSelect}
|
onMarketClick={onSelect}
|
||||||
onFilterChanged={onFilterChanged}
|
onFilterChanged={handleDataCount}
|
||||||
|
onGridReady={handleOnGridReady}
|
||||||
/>
|
/>
|
||||||
<div className="pointer-events-none absolute inset-0">
|
<div className="pointer-events-none absolute inset-0">
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
loading={loading}
|
loading={loading}
|
||||||
error={error}
|
error={error}
|
||||||
data={data}
|
data={dataRef?.current || []}
|
||||||
noDataMessage={t('No markets')}
|
noDataMessage={t('No markets')}
|
||||||
noDataCondition={() => !dataCount}
|
noDataCondition={() => !dataCount}
|
||||||
reload={reload}
|
reload={reload}
|
||||||
|
@ -2,7 +2,7 @@ import { formatNumberPercentage } from '@vegaprotocol/utils';
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import type { Market, Candle } from '../';
|
import type { Market, Candle, MarketMaybeWithData } from '../';
|
||||||
const { MarketState, MarketTradingMode } = Schema;
|
const { MarketState, MarketTradingMode } = Schema;
|
||||||
|
|
||||||
export const totalFees = (fees: Market['fees']['factors']) => {
|
export const totalFees = (fees: Market['fees']['factors']) => {
|
||||||
@ -19,7 +19,7 @@ export const totalFeesPercentage = (fees: Market['fees']['factors']) => {
|
|||||||
return total ? formatNumberPercentage(total) : undefined;
|
return total ? formatNumberPercentage(total) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterAndSortMarkets = (markets: Market[]) => {
|
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
|
||||||
const tradingModesOrdering = [
|
const tradingModesOrdering = [
|
||||||
MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||||
@ -28,27 +28,30 @@ export const filterAndSortMarkets = (markets: Market[]) => {
|
|||||||
MarketTradingMode.TRADING_MODE_NO_TRADING,
|
MarketTradingMode.TRADING_MODE_NO_TRADING,
|
||||||
];
|
];
|
||||||
const orderedMarkets = orderBy(
|
const orderedMarkets = orderBy(
|
||||||
markets?.filter(
|
markets?.filter((m) => {
|
||||||
(m) =>
|
const state = m.data?.marketState || m.state;
|
||||||
m.state !== MarketState.STATE_REJECTED &&
|
const tradingMode = m.data?.marketTradingMode || m.tradingMode;
|
||||||
m.tradingMode !== MarketTradingMode.TRADING_MODE_NO_TRADING
|
return (
|
||||||
) || [],
|
state !== MarketState.STATE_REJECTED &&
|
||||||
|
tradingMode !== MarketTradingMode.TRADING_MODE_NO_TRADING
|
||||||
|
);
|
||||||
|
}) || [],
|
||||||
['marketTimestamps.open', 'id'],
|
['marketTimestamps.open', 'id'],
|
||||||
['asc', 'asc']
|
['asc', 'asc']
|
||||||
);
|
);
|
||||||
return orderedMarkets.sort(
|
return orderedMarkets.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
tradingModesOrdering.indexOf(a.tradingMode) -
|
tradingModesOrdering.indexOf(a.data?.marketTradingMode || a.tradingMode) -
|
||||||
tradingModesOrdering.indexOf(b.tradingMode)
|
tradingModesOrdering.indexOf(b.data?.marketTradingMode || b.tradingMode)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterAndSortClosedMarkets = (markets: Market[]) => {
|
export const filterAndSortClosedMarkets = (markets: MarketMaybeWithData[]) => {
|
||||||
return markets.filter((m) => {
|
return markets.filter((m) => {
|
||||||
return [
|
return [
|
||||||
MarketState.STATE_SETTLED,
|
MarketState.STATE_SETTLED,
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
MarketState.STATE_TRADING_TERMINATED,
|
||||||
].includes(m.state);
|
].includes(m.data?.marketState || m.state);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user