chore(markets): keep market list live updated (#3954)

This commit is contained in:
Maciek 2023-05-29 09:39:42 +02:00 committed by GitHub
parent 0f2b86770a
commit 702ea0c6b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 79 additions and 41 deletions

View File

@ -11,6 +11,8 @@ import type {
UpdateCallback,
Update,
PageInfo,
Reload,
Load,
} from './generic-data-provider';
import type {
ApolloClient,
@ -600,6 +602,17 @@ describe('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', () => {
clientSubscribeSubscribe.mockClear();
clientSubscribeUnsubscribe.mockClear();
@ -628,7 +641,7 @@ describe('derived data provider', () => {
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
Parameters<UpdateCallback<CombinedData, CombinedData>>
>();
const subscription = derivedSubscribe(callback, client, variables);
subscription = derivedSubscribe(callback, client, variables);
const data = { totalCount: 0 };
combineData.mockReturnValueOnce(data);
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[0][0].data).toBe(data);
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 () => {
@ -655,7 +667,7 @@ describe('derived data provider', () => {
Parameters<UpdateCallback<CombinedData, CombinedData>>
>();
expect(callback.mock.calls.length).toBe(0);
const subscription = derivedSubscribe(callback, client, variables);
subscription = derivedSubscribe(callback, client, variables);
const data = { totalCount: 0 };
combineData.mockReturnValueOnce(data);
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].loading).toBe(false);
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 () => {
@ -690,7 +701,7 @@ describe('derived data provider', () => {
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
Parameters<UpdateCallback<CombinedData, CombinedData>>
>();
const subscription = derivedSubscribe(callback, client, variables);
subscription = derivedSubscribe(callback, client, variables);
const data = { totalCount: 0 };
combineData.mockReturnValueOnce(data);
await resolveQuery({ data: part1 });
@ -711,7 +722,6 @@ describe('derived data provider', () => {
expect(callback).toBeCalledTimes(2);
expect(callback.mock.calls[1][0].isUpdate).toBe(true);
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 () => {
@ -722,7 +732,7 @@ describe('derived data provider', () => {
ReturnType<UpdateCallback<CombinedData, CombinedData>>,
Parameters<UpdateCallback<CombinedData, CombinedData>>
>();
const subscription = derivedSubscribe(callback, client, variables);
subscription = derivedSubscribe(callback, client, variables);
const data = { totalCount: 0 };
combineData.mockReturnValueOnce(data);
await resolveQuery({
@ -758,6 +768,5 @@ describe('derived data provider', () => {
expect(callback).toBeCalledTimes(2);
expect(callback.mock.calls[1][0].isInsert).toBe(true);
expect(callback.mock.calls[1][0].insertionData).toBe(combinedInsertionData);
subscription.unsubscribe();
});
});

View File

@ -443,8 +443,8 @@ function makeDataProviderInternal<
if (loading) {
return;
}
// hard reset on demand or when there is no apollo subscription yet
if (forceReset || !subscription) {
// hard reset on demand or when error occurs
if (forceReset || error) {
reset();
initialize();
} else {

View File

@ -104,9 +104,10 @@ export const useDataProvider = <
totalCount,
isInsert,
isUpdate,
loaded,
} = args;
setError(error);
setLoading(loading);
setLoading(!loaded && loading);
// 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
if (!loading) {

View File

@ -93,15 +93,14 @@ export const MarketListTable = forwardRef<
data,
}: VegaValueFormatterParams<MarketMaybeWithData, 'data'>) => {
if (!data?.data) return undefined;
const { trigger } = data.data;
const { tradingMode } = data;
return tradingMode ===
const { trigger, marketTradingMode } = data.data;
return marketTradingMode ===
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION &&
trigger &&
trigger !== AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED
? `${Schema.MarketTradingModeMapping[tradingMode]}
? `${Schema.MarketTradingModeMapping[marketTradingMode]}
- ${Schema.AuctionTriggerMapping[trigger]}`
: Schema.MarketTradingModeMapping[tradingMode];
: Schema.MarketTradingModeMapping[marketTradingMode];
}}
filter={SetFilter}
filterParams={{
@ -228,6 +227,7 @@ export const MarketListTable = forwardRef<
/>
<AgGridColumn
colId="market-actions"
field="id"
{...COL_DEFS.actions}
cellRenderer={({
data,

View File

@ -1,37 +1,60 @@
import type { MouseEvent } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { CellClickedEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/i18n';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { MarketListTable } from './market-list-table';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type { CellClickedEvent } from 'ag-grid-community';
import { marketsWithDataProvider as dataProvider } from '../../markets-provider';
import type { MarketMaybeWithData } from '../../markets-provider';
const POLLING_TIME = 2000;
interface MarketsContainerProps {
onSelect: (marketId: string, metaKey?: boolean) => void;
}
export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [dataCount, setDataCount] = useState(0);
const { data, error, loading, reload } = useDataProvider({
dataProvider,
skipUpdates: true,
variables: undefined,
});
useEffect(() => {
setDataCount(gridRef.current?.api?.getModel().getRowCount() ?? 0);
}, [data]);
const onFilterChanged = useCallback(() => {
const dataRef = useRef<MarketMaybeWithData[] | null>(null);
const [dataCount, setDataCount] = useState(1);
const handleDataCount = useCallback(() => {
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 (
<div className="h-full relative">
<MarketListTable
ref={gridRef}
rowData={data}
suppressLoadingOverlay
suppressNoRowsOverlay
onCellClicked={(cellEvent: CellClickedEvent) => {
@ -44,6 +67,7 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
'id',
'tradableInstrument.instrument.code',
'tradableInstrument.instrument.product.settlementAsset',
'tradableInstrument.instrument.product.settlementAsset.symbol',
].includes(colId)
) {
return;
@ -55,13 +79,14 @@ export const MarketsContainer = ({ onSelect }: MarketsContainerProps) => {
);
}}
onMarketClick={onSelect}
onFilterChanged={onFilterChanged}
onFilterChanged={handleDataCount}
onGridReady={handleOnGridReady}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
data={dataRef?.current || []}
noDataMessage={t('No markets')}
noDataCondition={() => !dataCount}
reload={reload}

View File

@ -2,7 +2,7 @@ import { formatNumberPercentage } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import orderBy from 'lodash/orderBy';
import type { Market, Candle } from '../';
import type { Market, Candle, MarketMaybeWithData } from '../';
const { MarketState, MarketTradingMode } = Schema;
export const totalFees = (fees: Market['fees']['factors']) => {
@ -19,7 +19,7 @@ export const totalFeesPercentage = (fees: Market['fees']['factors']) => {
return total ? formatNumberPercentage(total) : undefined;
};
export const filterAndSortMarkets = (markets: Market[]) => {
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
const tradingModesOrdering = [
MarketTradingMode.TRADING_MODE_CONTINUOUS,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
@ -28,27 +28,30 @@ export const filterAndSortMarkets = (markets: Market[]) => {
MarketTradingMode.TRADING_MODE_NO_TRADING,
];
const orderedMarkets = orderBy(
markets?.filter(
(m) =>
m.state !== MarketState.STATE_REJECTED &&
m.tradingMode !== MarketTradingMode.TRADING_MODE_NO_TRADING
) || [],
markets?.filter((m) => {
const state = m.data?.marketState || m.state;
const tradingMode = m.data?.marketTradingMode || m.tradingMode;
return (
state !== MarketState.STATE_REJECTED &&
tradingMode !== MarketTradingMode.TRADING_MODE_NO_TRADING
);
}) || [],
['marketTimestamps.open', 'id'],
['asc', 'asc']
);
return orderedMarkets.sort(
(a, b) =>
tradingModesOrdering.indexOf(a.tradingMode) -
tradingModesOrdering.indexOf(b.tradingMode)
tradingModesOrdering.indexOf(a.data?.marketTradingMode || a.tradingMode) -
tradingModesOrdering.indexOf(b.data?.marketTradingMode || b.tradingMode)
);
};
export const filterAndSortClosedMarkets = (markets: Market[]) => {
export const filterAndSortClosedMarkets = (markets: MarketMaybeWithData[]) => {
return markets.filter((m) => {
return [
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(m.state);
].includes(m.data?.marketState || m.state);
});
};