diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts index 4466ba980..e138d2768 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts +++ b/apps/simple-trading-app/src/app/components/simple-market-list/data-provider.ts @@ -91,14 +91,15 @@ export const FILTERS_QUERY = gql` const update = ( data: SimpleMarkets_markets[], delta: SimpleMarketDataSub_marketData -) => - produce(data, (draft) => { +) => { + return produce(data, (draft) => { const index = draft.findIndex((m) => m.id === delta.market.id); if (index !== -1) { draft[index].data = delta; } // @TODO - else push new market to draft }); +}; const getData = (responseData: SimpleMarkets) => responseData.markets; const getDelta = ( diff --git a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx index 391e9efe7..3e6023164 100644 --- a/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx +++ b/apps/simple-trading-app/src/app/components/simple-market-list/simple-market-list.tsx @@ -16,11 +16,11 @@ import { ThemeContext } from '@vegaprotocol/react-helpers'; import type { MarketState } from '@vegaprotocol/types'; import useMarketsFilterData from '../../hooks/use-markets-filter-data'; import useColumnDefinitions from '../../hooks/use-column-definitions'; -import DataProvider from './data-provider'; +import dataProvider from './data-provider'; import * as constants from './constants'; import SimpleMarketToolbar from './simple-market-toolbar'; import type { SimpleMarkets_markets } from './__generated__/SimpleMarkets'; - +import type { SimpleMarketDataSub_marketData } from './__generated__/SimpleMarketDataSub'; export type SimpleMarketsType = SimpleMarkets_markets & { percentChange?: number | '-'; }; @@ -44,15 +44,16 @@ const SimpleMarketList = () => { [] ); const update = useCallback( - (delta) => statusesRef.current[delta.market.id] === delta.market.state, + ({ delta }: { delta: SimpleMarketDataSub_marketData }) => + statusesRef.current[delta.market.id] === delta.market.state, [statusesRef] ); - const { data, error, loading } = useDataProvider( - DataProvider, + const { data, error, loading } = useDataProvider({ + dataProvider, update, - variables - ); + variables, + }); const localData: Array = useMarketsFilterData( data || [], params diff --git a/libs/accounts/src/lib/accounts-data-provider.ts b/libs/accounts/src/lib/accounts-data-provider.ts index 9bf905152..bd2a9f5ef 100644 --- a/libs/accounts/src/lib/accounts-data-provider.ts +++ b/libs/accounts/src/lib/accounts-data-provider.ts @@ -55,8 +55,8 @@ export const getId = ( const update = ( data: Accounts_party_accounts[], delta: AccountSubscribe_accounts -) => - produce(data, (draft) => { +) => { + return produce(data, (draft) => { const id = getId(delta); const index = draft.findIndex((a) => getId(a) === id); if (index !== -1) { @@ -65,6 +65,8 @@ const update = ( draft.push(delta); } }); +}; + const getData = (responseData: Accounts): Accounts_party_accounts[] | null => responseData.party ? responseData.party.accounts : null; const getDelta = ( diff --git a/libs/accounts/src/lib/accounts-manager.tsx b/libs/accounts/src/lib/accounts-manager.tsx index d0c08100d..9d5717f44 100644 --- a/libs/accounts/src/lib/accounts-manager.tsx +++ b/libs/accounts/src/lib/accounts-manager.tsx @@ -22,7 +22,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => { const gridRef = useRef(null); const variables = useMemo(() => ({ partyId }), [partyId]); const update = useCallback( - (delta: AccountSubscribe_accounts) => { + ({ delta }: { delta: AccountSubscribe_accounts }) => { const update: Accounts_party_accounts[] = []; const add: Accounts_party_accounts[] = []; if (!gridRef.current) { @@ -64,7 +64,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => { const { data, error, loading } = useDataProvider< Accounts_party_accounts[], AccountSubscribe_accounts - >(accountsDataProvider, update, variables); + >({ dataProvider: accountsDataProvider, update, variables }); return ( diff --git a/libs/fills/src/lib/fills-data-provider.ts b/libs/fills/src/lib/fills-data-provider.ts index d22993790..54e432b16 100644 --- a/libs/fills/src/lib/fills-data-provider.ts +++ b/libs/fills/src/lib/fills-data-provider.ts @@ -1,10 +1,11 @@ +import produce from 'immer'; import { gql } from '@apollo/client'; import { makeDataProvider } from '@vegaprotocol/react-helpers'; -import produce from 'immer'; +import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers'; import type { FillFields } from './__generated__/FillFields'; import type { Fills, - Fills_party_tradesPaged_edges_node, + Fills_party_tradesPaged_edges, } from './__generated__/Fills'; import type { FillsSub } from './__generated__/FillsSub'; @@ -16,41 +17,19 @@ const FILL_FRAGMENT = gql` size buyOrder sellOrder - aggressor buyer { id } seller { id } - buyerFee { - makerFee - infrastructureFee - liquidityFee - } - sellerFee { - makerFee - infrastructureFee - liquidityFee - } market { id - name decimalPlaces - positionDecimalPlaces tradableInstrument { instrument { id code - product { - ... on Future { - settlementAsset { - id - symbol - decimals - } - } - } } } } @@ -88,30 +67,69 @@ export const FILLS_SUB = gql` } `; -const update = (data: FillFields[], delta: FillFields[]) => { - // Add or update incoming trades +const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => { return produce(data, (draft) => { - delta.forEach((trade) => { - const index = draft.findIndex((t) => t.id === trade.id); - if (index === -1) { - draft.unshift(trade); + delta.forEach((node) => { + const index = draft.findIndex((edge) => edge.node.id === node.id); + if (index !== -1) { + Object.assign(draft[index].node, node); } else { - draft[index] = trade; + draft.unshift({ node, cursor: '', __typename: 'TradeEdge' }); } }); }); }; -const getData = ( - responseData: Fills -): Fills_party_tradesPaged_edges_node[] | null => - responseData.party?.tradesPaged.edges.map((e) => e.node) || null; +const getData = (responseData: Fills): Fills_party_tradesPaged_edges[] | null => + responseData.party?.tradesPaged.edges || null; + +const getPageInfo = (responseData: Fills): PageInfo | null => + responseData.party?.tradesPaged.pageInfo || null; + +const getTotalCount = (responseData: Fills): number | undefined => + responseData.party?.tradesPaged.totalCount; + const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || []; +const append = ( + data: Fills_party_tradesPaged_edges[] | null, + pageInfo: PageInfo, + insertionData: Fills_party_tradesPaged_edges[] | null, + insertionPageInfo: PageInfo | null, + pagination?: Pagination +) => { + if (data && insertionData && insertionPageInfo) { + if (pagination?.after) { + if (data[data.length - 1].cursor === pagination.after) { + return { + data: [...data, ...insertionData], + pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor }, + }; + } else { + const cursors = data.map((item) => item.cursor); + const startIndex = cursors.lastIndexOf(pagination.after); + if (startIndex !== -1) { + return { + data: [...data.slice(0, startIndex), ...insertionData], + pageInfo: { ...pageInfo, endCursor: insertionPageInfo.endCursor }, + }; + } + } + } + } + return { data, pageInfo }; +}; + export const fillsDataProvider = makeDataProvider( FILLS_QUERY, FILLS_SUB, update, getData, - getDelta + getDelta, + { + getPageInfo, + getTotalCount, + append, + first: 100, + } ); diff --git a/libs/fills/src/lib/fills-manager.tsx b/libs/fills/src/lib/fills-manager.tsx index 0b1727e65..3625d771f 100644 --- a/libs/fills/src/lib/fills-manager.tsx +++ b/libs/fills/src/lib/fills-manager.tsx @@ -1,13 +1,13 @@ import type { AgGridReact } from 'ag-grid-react'; -import { useCallback, useMemo, useRef } from 'react'; -import { FillsTable } from './fills-table'; -import { fillsDataProvider } from './fills-data-provider'; +import { useCallback, useRef, useMemo } from 'react'; import { useDataProvider } from '@vegaprotocol/react-helpers'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; -import type { FillsVariables } from './__generated__/Fills'; -import type { FillFields } from './__generated__/FillFields'; +import { FillsTable } from './fills-table'; +import type { IGetRowsParams } from 'ag-grid-community'; + +import { fillsDataProvider as dataProvider } from './fills-data-provider'; +import type { Fills_party_tradesPaged_edges } from './__generated__/Fills'; import type { FillsSub_trades } from './__generated__/FillsSub'; -import isEqual from 'lodash/isEqual'; interface FillsManagerProps { partyId: string; @@ -15,66 +15,78 @@ interface FillsManagerProps { export const FillsManager = ({ partyId }: FillsManagerProps) => { const gridRef = useRef(null); - const variables = useMemo( - () => ({ - partyId, - pagination: { - last: 300, - }, - }), - [partyId] - ); - const update = useCallback((delta: FillsSub_trades[]) => { - if (!gridRef.current) { - return false; - } - const updateRows: FillFields[] = []; - const add: FillFields[] = []; + const dataRef = useRef(null); + const totalCountRef = useRef(undefined); - delta.forEach((d) => { + const update = useCallback( + ({ data }: { data: Fills_party_tradesPaged_edges[] }) => { if (!gridRef.current?.api) { - return; + return false; } - - const rowNode = gridRef.current.api.getRowNode(d.id); - - if (rowNode) { - if (!isEqual(d, rowNode.data)) { - updateRows.push(d); - } - } else { - add.push(d); - } - }); - - if (updateRows.length || add.length) { - gridRef.current.api.applyTransactionAsync({ - update: updateRows, - add, - addIndex: 0, - }); - } - - return true; - }, []); - - const { data, loading, error } = useDataProvider( - fillsDataProvider, - update, - variables + dataRef.current = data; + gridRef.current.api.refreshInfiniteCache(); + return true; + }, + [] ); - const fills = useMemo(() => { - if (!data?.length) { - return []; - } + const insert = useCallback( + ({ + data, + totalCount, + }: { + data: Fills_party_tradesPaged_edges[]; + totalCount?: number; + }) => { + dataRef.current = data; + totalCountRef.current = totalCount; + return true; + }, + [] + ); - return data; - }, [data]); + const variables = useMemo(() => ({ partyId }), [partyId]); + + const { data, error, loading, load, totalCount } = useDataProvider< + Fills_party_tradesPaged_edges[], + FillsSub_trades[] + >({ dataProvider, update, insert, variables }); + totalCountRef.current = totalCount; + dataRef.current = data; + + const getRows = async ({ + successCallback, + failCallback, + startRow, + endRow, + }: IGetRowsParams) => { + try { + if (dataRef.current && dataRef.current.length < endRow) { + await load({ + first: endRow - startRow, + after: dataRef.current[dataRef.current.length - 1].cursor, + }); + } + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow).map((edge) => edge.node) + : []; + let lastRow = -1; + if (totalCountRef.current !== undefined) { + if (!totalCountRef.current) { + lastRow = 0; + } else if (totalCountRef.current <= endRow) { + lastRow = totalCountRef.current; + } + } + successCallback(rowsThisBlock, lastRow); + } catch (e) { + failCallback(); + } + }; return ( - - + + ); }; diff --git a/libs/fills/src/lib/fills-table.spec.tsx b/libs/fills/src/lib/fills-table.spec.tsx index df98acf11..1b00c2a34 100644 --- a/libs/fills/src/lib/fills-table.spec.tsx +++ b/libs/fills/src/lib/fills-table.spec.tsx @@ -1,12 +1,25 @@ -import { render, act, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { getDateTimeFormat } from '@vegaprotocol/react-helpers'; import { Side } from '@vegaprotocol/types'; import type { PartialDeep } from 'type-fest'; import { FillsTable } from './fills-table'; -import { generateFill } from './test-helpers'; +import { generateFill, makeGetRows } from './test-helpers'; import type { FillFields } from './__generated__/FillFields'; +const waitForGridToBeInTheDOM = () => { + return waitFor(() => { + expect(document.querySelector('.ag-root-wrapper')).toBeInTheDocument(); + }); +}; + +// since our grid starts with no data, when the overlay has gone, data has loaded +const waitForDataToHaveLoaded = () => { + return waitFor(() => { + expect(document.querySelector('.ag-overlay-no-rows-center')).toBeNull(); + }); +}; + describe('FillsTable', () => { let defaultFill: PartialDeep; @@ -34,9 +47,14 @@ describe('FillsTable', () => { }); it('correct columns are rendered', async () => { - await act(async () => { - render(); - }); + render( + + ); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); const headers = screen.getAllByRole('columnheader'); expect(headers).toHaveLength(7); @@ -66,14 +84,14 @@ describe('FillsTable', () => { }, }); - const { container } = render( - + render( + ); - - // Check grid has been rendered - await waitFor(() => { - expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); - }); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); const cells = screen.getAllByRole('gridcell'); const expectedValues = [ @@ -108,14 +126,14 @@ describe('FillsTable', () => { }, }); - const { container } = render( - + render( + ); - - // Check grid has been rendered - await waitFor(() => { - expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); - }); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); const cells = screen.getAllByRole('gridcell'); const expectedValues = [ @@ -144,14 +162,14 @@ describe('FillsTable', () => { aggressor: Side.Sell, }); - const { container, rerender } = render( - + const { rerender } = render( + ); - - // Check grid has been rendered - await waitFor(() => { - expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument(); - }); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); expect( screen @@ -166,7 +184,14 @@ describe('FillsTable', () => { aggressor: Side.Buy, }); - rerender(); + rerender( + + ); + await waitForGridToBeInTheDOM(); + await waitForDataToHaveLoaded(); expect( screen diff --git a/libs/fills/src/lib/fills-table.stories.tsx b/libs/fills/src/lib/fills-table.stories.tsx index d24a8b69c..4c61518c3 100644 --- a/libs/fills/src/lib/fills-table.stories.tsx +++ b/libs/fills/src/lib/fills-table.stories.tsx @@ -1,7 +1,7 @@ import type { Story, Meta } from '@storybook/react'; import type { FillsTableProps } from './fills-table'; import { FillsTable } from './fills-table'; -import { generateFills } from './test-helpers'; +import { generateFills, makeGetRows } from './test-helpers'; export default { component: FillsTable, @@ -14,5 +14,9 @@ export const Default = Template.bind({}); const fills = generateFills(); Default.args = { partyId: 'party-id', - fills: fills.party?.tradesPaged.edges.map((e) => e.node), + datasource: { + getRows: makeGetRows( + fills.party?.tradesPaged.edges.map((e) => e.node) || [] + ), + }, }; diff --git a/libs/fills/src/lib/fills-table.tsx b/libs/fills/src/lib/fills-table.tsx index c8199e1ee..52ad384a2 100644 --- a/libs/fills/src/lib/fills-table.tsx +++ b/libs/fills/src/lib/fills-table.tsx @@ -10,25 +10,26 @@ import { AgGridColumn } from 'ag-grid-react'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { forwardRef } from 'react'; import type { FillFields } from './__generated__/FillFields'; -import type { ValueFormatterParams } from 'ag-grid-community'; +import type { ValueFormatterParams, IDatasource } from 'ag-grid-community'; import BigNumber from 'bignumber.js'; import { Side } from '@vegaprotocol/types'; export interface FillsTableProps { partyId: string; - fills: FillFields[]; + datasource: IDatasource; } export const FillsTable = forwardRef( - ({ partyId, fills }, ref) => { + ({ partyId, datasource }, ref) => { return ( data.id} + getRowId={({ data }) => data?.id} + rowModelType="infinite" + datasource={datasource} > ( field="size" cellClass={({ data }: { data: FillFields }) => { let className = ''; - if (data.buyer.id === partyId) { + if (data?.buyer.id === partyId) { className = 'text-vega-green'; - } else if (data.seller.id) { + } else if (data?.seller.id) { className = 'text-vega-red'; } return className; @@ -69,6 +70,9 @@ export const FillsTable = forwardRef( headerName={t('Date')} field="createdAt" valueFormatter={({ value }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } return getDateTimeFormat().format(new Date(value)); }} /> @@ -78,56 +82,68 @@ export const FillsTable = forwardRef( ); const formatPrice = ({ value, data }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } const asset = - data.market.tradableInstrument.instrument.product.settlementAsset.symbol; + data?.market.tradableInstrument.instrument.product.settlementAsset.symbol; const valueFormatted = addDecimalsFormatNumber( value, - data.market.decimalPlaces + data?.market.decimalPlaces ); return `${valueFormatted} ${asset}`; }; const formatSize = (partyId: string) => { return ({ value, data }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } let prefix; - if (data.buyer.id === partyId) { + if (data?.buyer.id === partyId) { prefix = '+'; - } else if (data.seller.id) { + } else if (data?.seller.id) { prefix = '-'; } const size = addDecimalsFormatNumber( value, - data.market.positionDecimalPlaces + data?.market.positionDecimalPlaces ); return `${prefix}${size}`; }; }; const formatTotal = ({ value, data }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } const asset = - data.market.tradableInstrument.instrument.product.settlementAsset.symbol; + data?.market.tradableInstrument.instrument.product.settlementAsset.symbol; const size = new BigNumber( - addDecimal(data.size, data.market.positionDecimalPlaces) + addDecimal(data?.size, data?.market.positionDecimalPlaces) ); - const price = new BigNumber(addDecimal(value, data.market.decimalPlaces)); + const price = new BigNumber(addDecimal(value, data?.market.decimalPlaces)); const total = size.times(price).toString(); - const valueFormatted = formatNumber(total, data.market.decimalPlaces); + const valueFormatted = formatNumber(total, data?.market.decimalPlaces); return `${valueFormatted} ${asset}`; }; const formatRole = (partyId: string) => { return ({ value, data }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } const taker = t('Taker'); const maker = t('Maker'); - if (data.buyer.id === partyId) { + if (data?.buyer.id === partyId) { if (value === Side.Buy) { return taker; } else { return maker; } - } else if (data.seller.id === partyId) { + } else if (data?.seller.id === partyId) { if (value === Side.Sell) { return taker; } else { @@ -141,12 +157,15 @@ const formatRole = (partyId: string) => { const formatFee = (partyId: string) => { return ({ value, data }: ValueFormatterParams) => { + if (value === undefined) { + return value; + } const asset = value.settlementAsset; let feesObj; - if (data.buyer.id === partyId) { - feesObj = data.buyerFee; - } else if (data.seller.id === partyId) { - feesObj = data.sellerFee; + if (data?.buyer.id === partyId) { + feesObj = data?.buyerFee; + } else if (data?.seller.id === partyId) { + feesObj = data?.sellerFee; } else { return '-'; } diff --git a/libs/fills/src/lib/test-helpers.ts b/libs/fills/src/lib/test-helpers.ts index 19c3b3506..9070406a9 100644 --- a/libs/fills/src/lib/test-helpers.ts +++ b/libs/fills/src/lib/test-helpers.ts @@ -1,5 +1,6 @@ import { Side } from '@vegaprotocol/types'; import merge from 'lodash/merge'; +import type { IGetRowsParams } from 'ag-grid-community'; import type { PartialDeep } from 'type-fest'; import type { Fills, @@ -132,3 +133,9 @@ export const generateFill = ( return merge(defaultFill, override); }; + +export const makeGetRows = + (data: Fills_party_tradesPaged_edges_node[]) => + ({ successCallback }: IGetRowsParams) => { + successCallback(data, data.length); + }; diff --git a/libs/market-depth/src/lib/depth-chart.tsx b/libs/market-depth/src/lib/depth-chart.tsx index 4285498cb..5868ebd61 100644 --- a/libs/market-depth/src/lib/depth-chart.tsx +++ b/libs/market-depth/src/lib/depth-chart.tsx @@ -6,7 +6,7 @@ import { addDecimal, ThemeContext, } from '@vegaprotocol/react-helpers'; -import { marketDepthDataProvider } from './market-depth-data-provider'; +import dataProvider from './market-depth-data-provider'; import { useCallback, useEffect, @@ -87,7 +87,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { // Apply updates to the table const update = useCallback( - (delta: MarketDepthSubscription_marketDepthUpdate) => { + ({ delta }: { delta: MarketDepthSubscription_marketDepthUpdate }) => { if (!dataRef.current) { return false; } @@ -122,11 +122,11 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { [] ); - const { data, error, loading } = useDataProvider( - marketDepthDataProvider, + const { data, error, loading } = useDataProvider({ + dataProvider, update, - variables - ); + variables, + }); useEffect(() => { if (!data) { diff --git a/libs/market-depth/src/lib/market-depth-data-provider.ts b/libs/market-depth/src/lib/market-depth-data-provider.ts index f82e70fe9..737d91ae3 100644 --- a/libs/market-depth/src/lib/market-depth-data-provider.ts +++ b/libs/market-depth/src/lib/market-depth-data-provider.ts @@ -131,3 +131,5 @@ export const marketDepthDataProvider = makeDataProvider( getData, getDelta ); + +export default marketDepthDataProvider; diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index f39604669..8894e6a5a 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -150,9 +150,9 @@ export const compactRows = ( groupedByLevel[price].pop() as PartialOrderbookRowData ); row.price = price; - let subRow: PartialOrderbookRowData | undefined; - // eslint-disable-next-line no-cond-assign - while ((subRow = groupedByLevel[price].pop())) { + let subRow: PartialOrderbookRowData | undefined = + groupedByLevel[price].pop(); + while (subRow) { row.ask += subRow.ask; row.bid += subRow.bid; if (subRow.ask) { @@ -161,6 +161,7 @@ export const compactRows = ( if (subRow.bid) { row.bidByLevel[subRow.price] = subRow.bid; } + subRow = groupedByLevel[price].pop(); } orderbookData.push(row); }); diff --git a/libs/market-depth/src/lib/orderbook-manager.tsx b/libs/market-depth/src/lib/orderbook-manager.tsx index af814d05d..3be836f4c 100644 --- a/libs/market-depth/src/lib/orderbook-manager.tsx +++ b/libs/market-depth/src/lib/orderbook-manager.tsx @@ -2,7 +2,7 @@ import throttle from 'lodash/throttle'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { Orderbook } from './orderbook'; import { useDataProvider } from '@vegaprotocol/react-helpers'; -import { marketDepthDataProvider } from './market-depth-data-provider'; +import dataProvider from './market-depth-data-provider'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MarketDepthSubscription_marketDepthUpdate } from './__generated__/MarketDepthSubscription'; import { @@ -46,7 +46,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { ); const update = useCallback( - (delta: MarketDepthSubscription_marketDepthUpdate) => { + ({ delta }: { delta: MarketDepthSubscription_marketDepthUpdate }) => { if (!dataRef.current.rows) { return false; } @@ -76,11 +76,11 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => { [] ); - const { data, error, loading, flush } = useDataProvider( - marketDepthDataProvider, + const { data, error, loading, flush } = useDataProvider({ + dataProvider, update, - variables - ); + variables, + }); useEffect(() => { if (!data) { diff --git a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx index b4956ce01..e866397dc 100644 --- a/libs/market-list/src/lib/components/markets-container/market-list-table.tsx +++ b/libs/market-list/src/lib/components/markets-container/market-list-table.tsx @@ -1,5 +1,5 @@ import { forwardRef } from 'react'; -import type { ValueFormatterParams } from 'ag-grid-community'; +import type { IDatasource, ValueFormatterParams } from 'ag-grid-community'; import { PriceFlashCell, addDecimalsFormatNumber, @@ -8,30 +8,22 @@ import { import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridColumn } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react'; -import type { - Markets_markets, - Markets_markets_data_market, -} from '../__generated__/Markets'; +import type { Markets_markets } from '../__generated__/Markets'; interface MarketListTableProps { - data: Markets_markets[] | null; + datasource: IDatasource; onRowClicked: (marketId: string) => void; } -export const getRowId = ({ - data, -}: { - data: Markets_markets | Markets_markets_data_market; -}) => data.id; - export const MarketListTable = forwardRef( - ({ data, onRowClicked }, ref) => { + ({ datasource, onRowClicked }, ref) => { return ( data?.id} ref={ref} defaultColDef={{ flex: 1, @@ -55,7 +47,9 @@ export const MarketListTable = forwardRef( headerName={t('State')} field="data" valueFormatter={({ value }: ValueFormatterParams) => - `${value.market.state} (${value.market.tradingMode})` + value === undefined + ? value + : `${value.market.state} (${value.market.tradingMode})` } /> ( type="rightAligned" cellRenderer="PriceFlashCell" valueFormatter={({ value, data }: ValueFormatterParams) => - addDecimalsFormatNumber(value, data.decimalPlaces) + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) } /> ( field="data.bestOfferPrice" type="rightAligned" valueFormatter={({ value, data }: ValueFormatterParams) => - addDecimalsFormatNumber(value, data.decimalPlaces) + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) } cellRenderer="PriceFlashCell" /> @@ -82,7 +80,9 @@ export const MarketListTable = forwardRef( type="rightAligned" cellRenderer="PriceFlashCell" valueFormatter={({ value, data }: ValueFormatterParams) => - addDecimalsFormatNumber(value, data.decimalPlaces) + value === undefined + ? value + : addDecimalsFormatNumber(value, data.decimalPlaces) } /> diff --git a/libs/market-list/src/lib/components/markets-container/markets-container.tsx b/libs/market-list/src/lib/components/markets-container/markets-container.tsx index e04d6024c..54d482560 100644 --- a/libs/market-list/src/lib/components/markets-container/markets-container.tsx +++ b/libs/market-list/src/lib/components/markets-container/markets-container.tsx @@ -1,61 +1,51 @@ import { useRef, useCallback } from 'react'; -import { produce } from 'immer'; -import merge from 'lodash/merge'; import { useRouter } from 'next/router'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; -import { MarketListTable, getRowId } from './market-list-table'; +import { MarketListTable } from './market-list-table'; import { useDataProvider } from '@vegaprotocol/react-helpers'; import type { AgGridReact } from 'ag-grid-react'; +import type { IGetRowsParams } from 'ag-grid-community'; import type { Markets_markets, Markets_markets_data, } from '../../components/__generated__/Markets'; -import { marketsDataProvider } from './markets-data-provider'; +import { marketsDataProvider as dataProvider } from './markets-data-provider'; export const MarketsContainer = () => { const { push } = useRouter(); const gridRef = useRef(null); - const update = useCallback( - (delta: Markets_markets_data) => { - const update: Markets_markets[] = []; - const add: Markets_markets[] = []; - if (!gridRef.current?.api) { - return false; - } - const rowNode = gridRef.current.api.getRowNode( - getRowId({ data: delta.market }) - ); - if (rowNode) { - const updatedData = produce( - rowNode.data.data, - (draft: Markets_markets_data) => merge(draft, delta) - ); - if (updatedData !== rowNode.data.data) { - update.push({ ...rowNode.data, data: updatedData }); - } - } - // @TODO - else add new market - if (update.length || add.length) { - gridRef.current.api.applyTransactionAsync({ - update, - add, - addIndex: 0, - }); - } - return true; - }, - [gridRef] - ); + const dataRef = useRef(null); + const update = useCallback(({ data }: { data: Markets_markets[] }) => { + if (!gridRef.current?.api) { + return false; + } + dataRef.current = data; + gridRef.current.api.refreshInfiniteCache(); + return true; + }, []); const { data, error, loading } = useDataProvider< Markets_markets[], Markets_markets_data - >(marketsDataProvider, update); + >({ dataProvider, update }); + dataRef.current = data; + + const getRows = async ({ + successCallback, + startRow, + endRow, + }: IGetRowsParams) => { + const rowsThisBlock = dataRef.current + ? dataRef.current.slice(startRow, endRow) + : []; + const lastRow = dataRef.current?.length ?? -1; + successCallback(rowsThisBlock, lastRow); + }; return ( push(`/markets/${id}`)} /> diff --git a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts index 79d612e52..8fd3ff22e 100644 --- a/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts +++ b/libs/market-list/src/lib/components/markets-container/markets-data-provider.ts @@ -91,14 +91,16 @@ const MARKET_DATA_SUB = gql` } `; -const update = (data: Markets_markets[], delta: MarketDataSub_marketData) => - produce(data, (draft) => { +const update = (data: Markets_markets[], delta: MarketDataSub_marketData) => { + return produce(data, (draft) => { const index = draft.findIndex((m) => m.id === delta.market.id); if (index !== -1) { draft[index].data = delta; } // @TODO - else push new market to draft }); +}; + const getData = (responseData: Markets): Markets_markets[] | null => responseData.markets; const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData => diff --git a/libs/order-list/src/lib/components/order-data-provider/order-data-provider.ts b/libs/order-list/src/lib/components/order-data-provider/order-data-provider.ts index 75e011c39..334f835ab 100644 --- a/libs/order-list/src/lib/components/order-data-provider/order-data-provider.ts +++ b/libs/order-list/src/lib/components/order-data-provider/order-data-provider.ts @@ -79,8 +79,8 @@ export const prepareIncomingOrders = (delta: OrderFields[]) => { return incoming; }; -const update = (data: OrderFields[], delta: OrderFields[]) => - produce(data, (draft) => { +const update = (data: OrderFields[], delta: OrderFields[]) => { + return produce(data, (draft) => { const incoming = prepareIncomingOrders(delta); // Add or update incoming orders @@ -93,6 +93,7 @@ const update = (data: OrderFields[], delta: OrderFields[]) => } }); }); +}; const getData = (responseData: Orders): Orders_party_orders[] | null => responseData?.party?.orders || null; diff --git a/libs/order-list/src/lib/components/order-list-manager/order-list-manager.tsx b/libs/order-list/src/lib/components/order-list-manager/order-list-manager.tsx index 307a98ee4..3adb2247a 100644 --- a/libs/order-list/src/lib/components/order-list-manager/order-list-manager.tsx +++ b/libs/order-list/src/lib/components/order-list-manager/order-list-manager.tsx @@ -3,7 +3,7 @@ import { OrderList } from '../order-list'; import type { OrderFields } from '../__generated__/OrderFields'; import { useDataProvider } from '@vegaprotocol/react-helpers'; import { - ordersDataProvider, + ordersDataProvider as dataProvider, prepareIncomingOrders, sortOrders, } from '../order-data-provider'; @@ -21,7 +21,7 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => { const variables = useMemo(() => ({ partyId }), [partyId]); // Apply updates to the table - const update = useCallback((delta: OrderSub_orders[]) => { + const update = useCallback(({ delta }: { delta: OrderSub_orders[] }) => { if (!gridRef.current) { return false; } @@ -57,11 +57,11 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => { return true; }, []); - const { data, error, loading } = useDataProvider( - ordersDataProvider, + const { data, error, loading } = useDataProvider({ + dataProvider, update, - variables - ); + variables, + }); const orders = useMemo(() => { if (!data) { diff --git a/libs/positions/src/lib/positions-data-provider.ts b/libs/positions/src/lib/positions-data-provider.ts index e030eb437..cd2adc9ca 100644 --- a/libs/positions/src/lib/positions-data-provider.ts +++ b/libs/positions/src/lib/positions-data-provider.ts @@ -78,8 +78,8 @@ export const POSITIONS_SUB = gql` const update = ( data: Positions_party_positions[], delta: PositionSubscribe_positions -) => - produce(data, (draft) => { +) => { + return produce(data, (draft) => { const index = draft.findIndex((m) => m.market.id === delta.market.id); if (index !== -1) { draft[index] = delta; @@ -87,6 +87,8 @@ const update = ( draft.push(delta); } }); +}; + const getData = (responseData: Positions): Positions_party_positions[] | null => responseData.party ? responseData.party.positions : null; const getDelta = ( diff --git a/libs/positions/src/lib/positions-manager.tsx b/libs/positions/src/lib/positions-manager.tsx index a589196db..b948fed41 100644 --- a/libs/positions/src/lib/positions-manager.tsx +++ b/libs/positions/src/lib/positions-manager.tsx @@ -18,7 +18,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => { const gridRef = useRef(null); const variables = useMemo(() => ({ partyId }), [partyId]); const update = useCallback( - (delta: PositionSubscribe_positions) => { + ({ delta }: { delta: PositionSubscribe_positions }) => { const update: Positions_party_positions[] = []; const add: Positions_party_positions[] = []; if (!gridRef.current?.api) { @@ -52,7 +52,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => { const { data, error, loading } = useDataProvider< Positions_party_positions[], PositionSubscribe_positions - >(positionsDataProvider, update, variables); + >({ dataProvider: positionsDataProvider, update, variables }); return ( diff --git a/libs/react-helpers/src/hooks/use-data-provider.ts b/libs/react-helpers/src/hooks/use-data-provider.ts index c7dade909..5e525ec05 100644 --- a/libs/react-helpers/src/hooks/use-data-provider.ts +++ b/libs/react-helpers/src/hooks/use-data-provider.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; import type { OperationVariables } from '@apollo/client'; -import type { Subscribe } from '../lib/generic-data-provider'; +import type { Subscribe, Pagination, Load } from '../lib/generic-data-provider'; /** * @@ -10,17 +10,33 @@ import type { Subscribe } from '../lib/generic-data-provider'; * @param variables optional * @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}}; */ -export function useDataProvider( - dataProvider: Subscribe, - update?: (delta: Delta) => boolean, - variables?: OperationVariables -) { +export function useDataProvider({ + dataProvider, + update, + insert, + variables, +}: { + dataProvider: Subscribe; + update?: ({ delta, data }: { delta: Delta; data: Data }) => boolean; + insert?: ({ + insertionData, + data, + totalCount, + }: { + insertionData: Data; + data: Data; + totalCount?: number; + }) => boolean; + variables?: OperationVariables; +}) { const client = useApolloClient(); const [data, setData] = useState(null); + const [totalCount, setTotalCount] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(undefined); const flushRef = useRef<(() => void) | undefined>(undefined); const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined); + const loadRef = useRef | undefined>(undefined); const initialized = useRef(false); const flush = useCallback(() => { if (flushRef.current) { @@ -32,30 +48,48 @@ export function useDataProvider( reloadRef.current(force); } }, []); + const load = useCallback((pagination: Pagination) => { + if (loadRef.current) { + return loadRef.current(pagination); + } + return Promise.reject(); + }, []); const callback = useCallback( - ({ data, error, loading, delta }) => { + ({ data, error, loading, delta, insertionData, totalCount }) => { setError(error); setLoading(loading); if (!error && !loading) { - // if update 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 - if (!initialized.current || !delta || !update || !update(delta)) { - initialized.current = true; - setData(data); + if (initialized.current) { + if (delta && update && update({ delta, data })) { + return; + } + if ( + insertionData && + insert && + insert({ insertionData, data, totalCount }) + ) { + return; + } } + initialized.current = true; + setTotalCount(totalCount); + setData(data); } }, - [update] + [update, insert] ); useEffect(() => { - const { unsubscribe, flush, reload } = dataProvider( + const { unsubscribe, flush, reload, load } = dataProvider( callback, client, variables ); flushRef.current = flush; reloadRef.current = reload; + loadRef.current = load; return unsubscribe; }, [client, initialized, dataProvider, callback, variables]); - return { data, loading, error, flush, reload }; + return { data, loading, error, flush, reload, load, totalCount }; } diff --git a/libs/react-helpers/src/lib/generic-data-provider.ts b/libs/react-helpers/src/lib/generic-data-provider.ts index 41113c61b..d7601e257 100644 --- a/libs/react-helpers/src/lib/generic-data-provider.ts +++ b/libs/react-helpers/src/lib/generic-data-provider.ts @@ -13,9 +13,30 @@ export interface UpdateCallback { data: Data | null; error?: Error; loading: boolean; + pageInfo: PageInfo | null; delta?: Delta; + insertionData?: Data | null; + totalCount?: number; }): void; } + +export interface Load { + (pagination: Pagination): Promise; +} + +export interface Pagination { + first?: number; + after?: string; + last?: number; + before?: string; +} + +export interface PageInfo { + startCursor?: string; + endCursor?: string; + hasNextPage?: boolean; + hasPreviousPage?: boolean; +} export interface Subscribe { ( callback: UpdateCallback, @@ -25,6 +46,7 @@ export interface Subscribe { unsubscribe: () => void; reload: (forceReset?: boolean) => void; flush: () => void; + load: Load; }; } @@ -35,8 +57,29 @@ export interface Update { (data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data; } +export interface Append { + ( + data: Data | null, + pageInfo: PageInfo, + insertionData: Data | null, + insertionPageInfo: PageInfo | null, + pagination?: Pagination + ): { + data: Data | null; + pageInfo: PageInfo; + }; +} + interface GetData { - (subscriptionData: QueryData): Data | null; + (queryData: QueryData): Data | null; +} + +interface GetPageInfo { + (queryData: QueryData): PageInfo | null; +} + +interface GetTotalCount { + (queryData: QueryData): number | undefined; } interface GetDelta { @@ -57,6 +100,12 @@ function makeDataProviderInternal( update: Update, getData: GetData, getDelta: GetDelta, + pagination?: { + getPageInfo: GetPageInfo; + getTotalCount: GetTotalCount; + append: Append; + first: number; + }, fetchPolicy: FetchPolicy = 'no-cache' ): Subscribe { // list of callbacks passed through subscribe call @@ -70,20 +119,60 @@ function makeDataProviderInternal( let loading = false; let client: ApolloClient | undefined = undefined; let subscription: Subscription | undefined = undefined; + let pageInfo: PageInfo | null = null; + let totalCount: number | undefined; // notify single callback about current state, delta is passes optionally only if notify was invoked onNext - const notify = (callback: UpdateCallback, delta?: Delta) => { + const notify = ( + callback: UpdateCallback, + dataUpdate?: { delta?: Delta; insertionData?: Data | null } + ) => { callback({ data, error, loading, - delta, + pageInfo, + totalCount, + ...dataUpdate, }); }; // notify all callbacks - const notifyAll = (delta?: Delta) => { - callbacks.forEach((callback) => notify(callback, delta)); + const notifyAll = (dataUpdate?: { + delta?: Delta; + insertionData?: Data | null; + }) => { + callbacks.forEach((callback) => notify(callback, dataUpdate)); + }; + + const load = async (params?: Pagination) => { + if (!client || !pagination || !pageInfo) { + return Promise.reject(); + } + const paginationVariables: Pagination = params ?? { + first: pagination.first, + after: pageInfo.endCursor, + }; + const res = await client.query({ + query, + variables: { + ...variables, + pagination: paginationVariables, + }, + fetchPolicy, + }); + const insertionData = getData(res.data); + const insertionDataPageInfo = pagination.getPageInfo(res.data); + ({ data, pageInfo } = pagination.append( + data, + pageInfo, + insertionData, + insertionDataPageInfo, + paginationVariables + )); + totalCount = pagination.getTotalCount(res.data); + notifyAll({ insertionData }); + return insertionData; }; const initialFetch = async () => { @@ -93,10 +182,16 @@ function makeDataProviderInternal( try { const res = await client.query({ query, - variables, + variables: pagination + ? { ...variables, pagination: { first: pagination.first } } + : variables, fetchPolicy, }); data = getData(res.data); + if (pagination) { + pageInfo = pagination.getPageInfo(res.data); + totalCount = pagination.getTotalCount(res.data); + } // if there was some updates received from subscription during initial query loading apply them on just received data if (data && updateQueue && updateQueue.length > 0) { while (updateQueue.length) { @@ -165,7 +260,7 @@ function makeDataProviderInternal( return; } data = newData; - notifyAll(delta); + notifyAll({ delta }); } }, () => reload() @@ -205,6 +300,7 @@ function makeDataProviderInternal( unsubscribe: () => unsubscribe(callback), reload, flush: () => notify(callback), + load, }; }; } @@ -239,7 +335,7 @@ const memoize = ( * @param update Update function that will be executed on each onNext, it should update data base on delta, it can reload data provider * @param getData transforms received query data to format that will be stored in data provider * @param getDelta transforms delta data to format that will be stored in data provider - * @param fetchPolicy + * @param pagination pagination related functions { getPageInfo, getTotalCount, append, first } * @returns Subscribe subscribe function * @example * const marketMidPriceProvider = makeDataProvider( @@ -263,6 +359,12 @@ export function makeDataProvider( update: Update, getData: GetData, getDelta: GetDelta, + pagination?: { + getPageInfo: GetPageInfo; + getTotalCount: GetTotalCount; + append: Append; + first: number; + }, fetchPolicy: FetchPolicy = 'no-cache' ): Subscribe { const getInstance = memoize(() => @@ -272,6 +374,7 @@ export function makeDataProvider( update, getData, getDelta, + pagination, fetchPolicy ) ); diff --git a/libs/trades/src/lib/trades-container.tsx b/libs/trades/src/lib/trades-container.tsx index af3c186e6..83d0a2da1 100644 --- a/libs/trades/src/lib/trades-container.tsx +++ b/libs/trades/src/lib/trades-container.tsx @@ -6,7 +6,7 @@ import { useCallback, useMemo, useRef } from 'react'; import { MAX_TRADES, sortTrades, - tradesDataProvider, + tradesDataProvider as dataProvider, } from './trades-data-provider'; import { TradesTable } from './trades-table'; import type { TradeFields } from './__generated__/TradeFields'; @@ -22,7 +22,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { () => ({ marketId, maxTrades: MAX_TRADES }), [marketId] ); - const update = useCallback((delta: TradeFields[]) => { + const update = useCallback(({ delta }: { delta: TradeFields[] }) => { if (!gridRef.current?.api) { return false; } @@ -43,11 +43,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => { return true; }, []); - const { data, error, loading } = useDataProvider( - tradesDataProvider, + const { data, error, loading } = useDataProvider({ + dataProvider, update, - variables - ); + variables, + }); return ( { ); }; -const update = (data: TradeFields[], delta: TradeFields[]) => - produce(data, (draft) => { +const update = (data: TradeFields[], delta: TradeFields[]) => { + return produce(data, (draft) => { const incoming = sortTrades(delta); // Add new trades to the top @@ -65,6 +65,7 @@ const update = (data: TradeFields[], delta: TradeFields[]) => draft.splice(MAX_TRADES, draft.length - MAX_TRADES); } }); +}; const getData = (responseData: Trades): TradeFields[] | null => responseData.market ? responseData.market.trades : null; diff --git a/yarn.lock b/yarn.lock index c4f0f83df..c0fafd179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7437,14 +7437,14 @@ aes-js@^3.1.2: integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== ag-grid-community@^27.0.1: - version "27.1.0" - resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-27.1.0.tgz#17f73173444a9efc4faea0f0cd6c5090e698f7ee" - integrity sha512-SWzIJTNa7C6Vinizelcoc1FAJQRt1pDn+A8XHQDO2GTQT+VjBnPL8fg94fLJy0EEvqaN5IhDybNS0nD07SKIQw== + version "27.3.0" + resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-27.3.0.tgz#b1e94a58026aaf2f0cd7920e35833325b5e762c7" + integrity sha512-R5oZMXEHXnOLrmhn91J8lR0bv6IAnRcU6maO+wKLMJxffRWaAYFAuw1jt7bdmcKCv8c65F6LEBx4ykSOALa9vA== ag-grid-react@^27.0.1: - version "27.1.0" - resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-27.1.0.tgz#3b08203b9731a2b2d5431dddd69d68dc640c311e" - integrity sha512-AfRwH6BL/LribvLJ2594Fq0/MfZf/17WebjGj927bM3vABDr2OBX3qgMIaQE+kpV9mABPb51rlWLMmbCvltv2g== + version "27.3.0" + resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-27.3.0.tgz#fe06647653f8b0b349b8e613aab8ea2e07915562" + integrity sha512-2bs9YfJ/shvBZQLLjny4NFvht+ic6VtpTPO0r3bHHOhlL3Fjx2rGvS6AHSwfvu+kJacHCta30PjaEbX8T3UDyw== dependencies: prop-types "^15.8.1" @@ -17018,7 +17018,7 @@ nx@13.8.1: object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== object-copy@^0.1.0: version "0.1.0"