Add pagination support to generic-data-provider (#691)

* feat(#638): add pagination support to data-provider

* feat(#638): use infinite rowModelType in market list table

* chore(#638): code style fixes

* feat(#638): fix data provider post update callbacks, handle market list table empty data

* feat(#638): amend variable names to improve code readability
This commit is contained in:
Bartłomiej Głownia 2022-07-05 15:33:50 +02:00 committed by GitHub
parent db050c6560
commit b9aef78447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 505 additions and 280 deletions

View File

@ -91,14 +91,15 @@ export const FILTERS_QUERY = gql`
const update = ( const update = (
data: SimpleMarkets_markets[], data: SimpleMarkets_markets[],
delta: SimpleMarketDataSub_marketData delta: SimpleMarketDataSub_marketData
) => ) => {
produce(data, (draft) => { return produce(data, (draft) => {
const index = draft.findIndex((m) => m.id === delta.market.id); const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index].data = delta; draft[index].data = delta;
} }
// @TODO - else push new market to draft // @TODO - else push new market to draft
}); });
};
const getData = (responseData: SimpleMarkets) => responseData.markets; const getData = (responseData: SimpleMarkets) => responseData.markets;
const getDelta = ( const getDelta = (

View File

@ -16,11 +16,11 @@ import { ThemeContext } from '@vegaprotocol/react-helpers';
import type { MarketState } from '@vegaprotocol/types'; import type { MarketState } from '@vegaprotocol/types';
import useMarketsFilterData from '../../hooks/use-markets-filter-data'; import useMarketsFilterData from '../../hooks/use-markets-filter-data';
import useColumnDefinitions from '../../hooks/use-column-definitions'; import useColumnDefinitions from '../../hooks/use-column-definitions';
import DataProvider from './data-provider'; import dataProvider from './data-provider';
import * as constants from './constants'; import * as constants from './constants';
import SimpleMarketToolbar from './simple-market-toolbar'; import SimpleMarketToolbar from './simple-market-toolbar';
import type { SimpleMarkets_markets } from './__generated__/SimpleMarkets'; import type { SimpleMarkets_markets } from './__generated__/SimpleMarkets';
import type { SimpleMarketDataSub_marketData } from './__generated__/SimpleMarketDataSub';
export type SimpleMarketsType = SimpleMarkets_markets & { export type SimpleMarketsType = SimpleMarkets_markets & {
percentChange?: number | '-'; percentChange?: number | '-';
}; };
@ -44,15 +44,16 @@ const SimpleMarketList = () => {
[] []
); );
const update = useCallback( 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] [statusesRef]
); );
const { data, error, loading } = useDataProvider( const { data, error, loading } = useDataProvider({
DataProvider, dataProvider,
update, update,
variables variables,
); });
const localData: Array<SimpleMarketsType> = useMarketsFilterData( const localData: Array<SimpleMarketsType> = useMarketsFilterData(
data || [], data || [],
params params

View File

@ -55,8 +55,8 @@ export const getId = (
const update = ( const update = (
data: Accounts_party_accounts[], data: Accounts_party_accounts[],
delta: AccountSubscribe_accounts delta: AccountSubscribe_accounts
) => ) => {
produce(data, (draft) => { return produce(data, (draft) => {
const id = getId(delta); const id = getId(delta);
const index = draft.findIndex((a) => getId(a) === id); const index = draft.findIndex((a) => getId(a) === id);
if (index !== -1) { if (index !== -1) {
@ -65,6 +65,8 @@ const update = (
draft.push(delta); draft.push(delta);
} }
}); });
};
const getData = (responseData: Accounts): Accounts_party_accounts[] | null => const getData = (responseData: Accounts): Accounts_party_accounts[] | null =>
responseData.party ? responseData.party.accounts : null; responseData.party ? responseData.party.accounts : null;
const getDelta = ( const getDelta = (

View File

@ -22,7 +22,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
const update = useCallback( const update = useCallback(
(delta: AccountSubscribe_accounts) => { ({ delta }: { delta: AccountSubscribe_accounts }) => {
const update: Accounts_party_accounts[] = []; const update: Accounts_party_accounts[] = [];
const add: Accounts_party_accounts[] = []; const add: Accounts_party_accounts[] = [];
if (!gridRef.current) { if (!gridRef.current) {
@ -64,7 +64,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
const { data, error, loading } = useDataProvider< const { data, error, loading } = useDataProvider<
Accounts_party_accounts[], Accounts_party_accounts[],
AccountSubscribe_accounts AccountSubscribe_accounts
>(accountsDataProvider, update, variables); >({ dataProvider: accountsDataProvider, update, variables });
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={loading} error={error} data={data}>
<AccountsTable ref={gridRef} data={data} /> <AccountsTable ref={gridRef} data={data} />

View File

@ -1,10 +1,11 @@
import produce from 'immer';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import { makeDataProvider } from '@vegaprotocol/react-helpers'; 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 { FillFields } from './__generated__/FillFields';
import type { import type {
Fills, Fills,
Fills_party_tradesPaged_edges_node, Fills_party_tradesPaged_edges,
} from './__generated__/Fills'; } from './__generated__/Fills';
import type { FillsSub } from './__generated__/FillsSub'; import type { FillsSub } from './__generated__/FillsSub';
@ -16,41 +17,19 @@ const FILL_FRAGMENT = gql`
size size
buyOrder buyOrder
sellOrder sellOrder
aggressor
buyer { buyer {
id id
} }
seller { seller {
id id
} }
buyerFee {
makerFee
infrastructureFee
liquidityFee
}
sellerFee {
makerFee
infrastructureFee
liquidityFee
}
market { market {
id id
name
decimalPlaces decimalPlaces
positionDecimalPlaces
tradableInstrument { tradableInstrument {
instrument { instrument {
id id
code code
product {
... on Future {
settlementAsset {
id
symbol
decimals
}
}
}
} }
} }
} }
@ -88,30 +67,69 @@ export const FILLS_SUB = gql`
} }
`; `;
const update = (data: FillFields[], delta: FillFields[]) => { const update = (data: Fills_party_tradesPaged_edges[], delta: FillFields[]) => {
// Add or update incoming trades
return produce(data, (draft) => { return produce(data, (draft) => {
delta.forEach((trade) => { delta.forEach((node) => {
const index = draft.findIndex((t) => t.id === trade.id); const index = draft.findIndex((edge) => edge.node.id === node.id);
if (index === -1) { if (index !== -1) {
draft.unshift(trade); Object.assign(draft[index].node, node);
} else { } else {
draft[index] = trade; draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
} }
}); });
}); });
}; };
const getData = ( const getData = (responseData: Fills): Fills_party_tradesPaged_edges[] | null =>
responseData: Fills responseData.party?.tradesPaged.edges || null;
): Fills_party_tradesPaged_edges_node[] | null =>
responseData.party?.tradesPaged.edges.map((e) => e.node) || 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 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( export const fillsDataProvider = makeDataProvider(
FILLS_QUERY, FILLS_QUERY,
FILLS_SUB, FILLS_SUB,
update, update,
getData, getData,
getDelta getDelta,
{
getPageInfo,
getTotalCount,
append,
first: 100,
}
); );

View File

@ -1,13 +1,13 @@
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useMemo, useRef } from 'react'; import { useCallback, useRef, useMemo } from 'react';
import { FillsTable } from './fills-table';
import { fillsDataProvider } from './fills-data-provider';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { FillsVariables } from './__generated__/Fills'; import { FillsTable } from './fills-table';
import type { FillFields } from './__generated__/FillFields'; 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 type { FillsSub_trades } from './__generated__/FillsSub';
import isEqual from 'lodash/isEqual';
interface FillsManagerProps { interface FillsManagerProps {
partyId: string; partyId: string;
@ -15,66 +15,78 @@ interface FillsManagerProps {
export const FillsManager = ({ partyId }: FillsManagerProps) => { export const FillsManager = ({ partyId }: FillsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo<FillsVariables>( const dataRef = useRef<Fills_party_tradesPaged_edges[] | null>(null);
() => ({ const totalCountRef = useRef<number | undefined>(undefined);
partyId,
pagination: { const update = useCallback(
last: 300, ({ data }: { data: Fills_party_tradesPaged_edges[] }) => {
}, if (!gridRef.current?.api) {
}),
[partyId]
);
const update = useCallback((delta: FillsSub_trades[]) => {
if (!gridRef.current) {
return false; return false;
} }
const updateRows: FillFields[] = []; dataRef.current = data;
const add: FillFields[] = []; gridRef.current.api.refreshInfiniteCache();
delta.forEach((d) => {
if (!gridRef.current?.api) {
return;
}
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; return true;
}, []); },
[]
const { data, loading, error } = useDataProvider(
fillsDataProvider,
update,
variables
); );
const fills = useMemo(() => { const insert = useCallback(
if (!data?.length) { ({
return []; data,
} totalCount,
}: {
data: Fills_party_tradesPaged_edges[];
totalCount?: number;
}) => {
dataRef.current = data;
totalCountRef.current = totalCount;
return true;
},
[]
);
return data; const variables = useMemo(() => ({ partyId }), [partyId]);
}, [data]);
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 ( return (
<AsyncRenderer data={fills} loading={loading} error={error}> <AsyncRenderer loading={loading} error={error} data={data}>
<FillsTable ref={gridRef} partyId={partyId} fills={fills} /> <FillsTable ref={gridRef} partyId={partyId} datasource={{ getRows }} />
</AsyncRenderer> </AsyncRenderer>
); );
}; };

View File

@ -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 { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import { generateFill } from './test-helpers'; import { generateFill, makeGetRows } from './test-helpers';
import type { FillFields } from './__generated__/FillFields'; 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', () => { describe('FillsTable', () => {
let defaultFill: PartialDeep<FillFields>; let defaultFill: PartialDeep<FillFields>;
@ -34,9 +47,14 @@ describe('FillsTable', () => {
}); });
it('correct columns are rendered', async () => { it('correct columns are rendered', async () => {
await act(async () => { render(
render(<FillsTable partyId="party-id" fills={[generateFill()]} />); <FillsTable
}); partyId="party-id"
datasource={{ getRows: makeGetRows([generateFill()]) }}
/>
);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
const headers = screen.getAllByRole('columnheader'); const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(7); expect(headers).toHaveLength(7);
@ -66,14 +84,14 @@ describe('FillsTable', () => {
}, },
}); });
const { container } = render( render(
<FillsTable partyId={partyId} fills={[buyerFill]} /> <FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
); );
await waitForGridToBeInTheDOM();
// Check grid has been rendered await waitForDataToHaveLoaded();
await waitFor(() => {
expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument();
});
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const expectedValues = [ const expectedValues = [
@ -108,14 +126,14 @@ describe('FillsTable', () => {
}, },
}); });
const { container } = render( render(
<FillsTable partyId={partyId} fills={[buyerFill]} /> <FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
); );
await waitForGridToBeInTheDOM();
// Check grid has been rendered await waitForDataToHaveLoaded();
await waitFor(() => {
expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument();
});
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const expectedValues = [ const expectedValues = [
@ -144,14 +162,14 @@ describe('FillsTable', () => {
aggressor: Side.Sell, aggressor: Side.Sell,
}); });
const { container, rerender } = render( const { rerender } = render(
<FillsTable partyId={partyId} fills={[takerFill]} /> <FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([takerFill]) }}
/>
); );
await waitForGridToBeInTheDOM();
// Check grid has been rendered await waitForDataToHaveLoaded();
await waitFor(() => {
expect(container.querySelector('.ag-root-wrapper')).toBeInTheDocument();
});
expect( expect(
screen screen
@ -166,7 +184,14 @@ describe('FillsTable', () => {
aggressor: Side.Buy, aggressor: Side.Buy,
}); });
rerender(<FillsTable partyId={partyId} fills={[makerFill]} />); rerender(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([makerFill]) }}
/>
);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
expect( expect(
screen screen

View File

@ -1,7 +1,7 @@
import type { Story, Meta } from '@storybook/react'; import type { Story, Meta } from '@storybook/react';
import type { FillsTableProps } from './fills-table'; import type { FillsTableProps } from './fills-table';
import { FillsTable } from './fills-table'; import { FillsTable } from './fills-table';
import { generateFills } from './test-helpers'; import { generateFills, makeGetRows } from './test-helpers';
export default { export default {
component: FillsTable, component: FillsTable,
@ -14,5 +14,9 @@ export const Default = Template.bind({});
const fills = generateFills(); const fills = generateFills();
Default.args = { Default.args = {
partyId: 'party-id', partyId: 'party-id',
fills: fills.party?.tradesPaged.edges.map((e) => e.node), datasource: {
getRows: makeGetRows(
fills.party?.tradesPaged.edges.map((e) => e.node) || []
),
},
}; };

View File

@ -10,25 +10,26 @@ import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { FillFields } from './__generated__/FillFields'; 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 BigNumber from 'bignumber.js';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
export interface FillsTableProps { export interface FillsTableProps {
partyId: string; partyId: string;
fills: FillFields[]; datasource: IDatasource;
} }
export const FillsTable = forwardRef<AgGridReact, FillsTableProps>( export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
({ partyId, fills }, ref) => { ({ partyId, datasource }, ref) => {
return ( return (
<AgGrid <AgGrid
ref={ref} ref={ref}
rowData={fills}
overlayNoRowsTemplate={t('No fills')} overlayNoRowsTemplate={t('No fills')}
defaultColDef={{ flex: 1, resizable: true }} defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id} getRowId={({ data }) => data?.id}
rowModelType="infinite"
datasource={datasource}
> >
<AgGridColumn headerName={t('Market')} field="market.name" /> <AgGridColumn headerName={t('Market')} field="market.name" />
<AgGridColumn <AgGridColumn
@ -36,9 +37,9 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
field="size" field="size"
cellClass={({ data }: { data: FillFields }) => { cellClass={({ data }: { data: FillFields }) => {
let className = ''; let className = '';
if (data.buyer.id === partyId) { if (data?.buyer.id === partyId) {
className = 'text-vega-green'; className = 'text-vega-green';
} else if (data.seller.id) { } else if (data?.seller.id) {
className = 'text-vega-red'; className = 'text-vega-red';
} }
return className; return className;
@ -69,6 +70,9 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
headerName={t('Date')} headerName={t('Date')}
field="createdAt" field="createdAt"
valueFormatter={({ value }: ValueFormatterParams) => { valueFormatter={({ value }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
return getDateTimeFormat().format(new Date(value)); return getDateTimeFormat().format(new Date(value));
}} }}
/> />
@ -78,56 +82,68 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
); );
const formatPrice = ({ value, data }: ValueFormatterParams) => { const formatPrice = ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
const asset = const asset =
data.market.tradableInstrument.instrument.product.settlementAsset.symbol; data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
const valueFormatted = addDecimalsFormatNumber( const valueFormatted = addDecimalsFormatNumber(
value, value,
data.market.decimalPlaces data?.market.decimalPlaces
); );
return `${valueFormatted} ${asset}`; return `${valueFormatted} ${asset}`;
}; };
const formatSize = (partyId: string) => { const formatSize = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => { return ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
let prefix; let prefix;
if (data.buyer.id === partyId) { if (data?.buyer.id === partyId) {
prefix = '+'; prefix = '+';
} else if (data.seller.id) { } else if (data?.seller.id) {
prefix = '-'; prefix = '-';
} }
const size = addDecimalsFormatNumber( const size = addDecimalsFormatNumber(
value, value,
data.market.positionDecimalPlaces data?.market.positionDecimalPlaces
); );
return `${prefix}${size}`; return `${prefix}${size}`;
}; };
}; };
const formatTotal = ({ value, data }: ValueFormatterParams) => { const formatTotal = ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
const asset = const asset =
data.market.tradableInstrument.instrument.product.settlementAsset.symbol; data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
const size = new BigNumber( 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 total = size.times(price).toString();
const valueFormatted = formatNumber(total, data.market.decimalPlaces); const valueFormatted = formatNumber(total, data?.market.decimalPlaces);
return `${valueFormatted} ${asset}`; return `${valueFormatted} ${asset}`;
}; };
const formatRole = (partyId: string) => { const formatRole = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => { return ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
const taker = t('Taker'); const taker = t('Taker');
const maker = t('Maker'); const maker = t('Maker');
if (data.buyer.id === partyId) { if (data?.buyer.id === partyId) {
if (value === Side.Buy) { if (value === Side.Buy) {
return taker; return taker;
} else { } else {
return maker; return maker;
} }
} else if (data.seller.id === partyId) { } else if (data?.seller.id === partyId) {
if (value === Side.Sell) { if (value === Side.Sell) {
return taker; return taker;
} else { } else {
@ -141,12 +157,15 @@ const formatRole = (partyId: string) => {
const formatFee = (partyId: string) => { const formatFee = (partyId: string) => {
return ({ value, data }: ValueFormatterParams) => { return ({ value, data }: ValueFormatterParams) => {
if (value === undefined) {
return value;
}
const asset = value.settlementAsset; const asset = value.settlementAsset;
let feesObj; let feesObj;
if (data.buyer.id === partyId) { if (data?.buyer.id === partyId) {
feesObj = data.buyerFee; feesObj = data?.buyerFee;
} else if (data.seller.id === partyId) { } else if (data?.seller.id === partyId) {
feesObj = data.sellerFee; feesObj = data?.sellerFee;
} else { } else {
return '-'; return '-';
} }

View File

@ -1,5 +1,6 @@
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import merge from 'lodash/merge'; import merge from 'lodash/merge';
import type { IGetRowsParams } from 'ag-grid-community';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { import type {
Fills, Fills,
@ -132,3 +133,9 @@ export const generateFill = (
return merge(defaultFill, override); return merge(defaultFill, override);
}; };
export const makeGetRows =
(data: Fills_party_tradesPaged_edges_node[]) =>
({ successCallback }: IGetRowsParams) => {
successCallback(data, data.length);
};

View File

@ -6,7 +6,7 @@ import {
addDecimal, addDecimal,
ThemeContext, ThemeContext,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { marketDepthDataProvider } from './market-depth-data-provider'; import dataProvider from './market-depth-data-provider';
import { import {
useCallback, useCallback,
useEffect, useEffect,
@ -87,7 +87,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
// Apply updates to the table // Apply updates to the table
const update = useCallback( const update = useCallback(
(delta: MarketDepthSubscription_marketDepthUpdate) => { ({ delta }: { delta: MarketDepthSubscription_marketDepthUpdate }) => {
if (!dataRef.current) { if (!dataRef.current) {
return false; return false;
} }
@ -122,11 +122,11 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
[] []
); );
const { data, error, loading } = useDataProvider( const { data, error, loading } = useDataProvider({
marketDepthDataProvider, dataProvider,
update, update,
variables variables,
); });
useEffect(() => { useEffect(() => {
if (!data) { if (!data) {

View File

@ -131,3 +131,5 @@ export const marketDepthDataProvider = makeDataProvider(
getData, getData,
getDelta getDelta
); );
export default marketDepthDataProvider;

View File

@ -150,9 +150,9 @@ export const compactRows = (
groupedByLevel[price].pop() as PartialOrderbookRowData groupedByLevel[price].pop() as PartialOrderbookRowData
); );
row.price = price; row.price = price;
let subRow: PartialOrderbookRowData | undefined; let subRow: PartialOrderbookRowData | undefined =
// eslint-disable-next-line no-cond-assign groupedByLevel[price].pop();
while ((subRow = groupedByLevel[price].pop())) { while (subRow) {
row.ask += subRow.ask; row.ask += subRow.ask;
row.bid += subRow.bid; row.bid += subRow.bid;
if (subRow.ask) { if (subRow.ask) {
@ -161,6 +161,7 @@ export const compactRows = (
if (subRow.bid) { if (subRow.bid) {
row.bidByLevel[subRow.price] = subRow.bid; row.bidByLevel[subRow.price] = subRow.bid;
} }
subRow = groupedByLevel[price].pop();
} }
orderbookData.push(row); orderbookData.push(row);
}); });

View File

@ -2,7 +2,7 @@ import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Orderbook } from './orderbook'; import { Orderbook } from './orderbook';
import { useDataProvider } from '@vegaprotocol/react-helpers'; 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { MarketDepthSubscription_marketDepthUpdate } from './__generated__/MarketDepthSubscription'; import type { MarketDepthSubscription_marketDepthUpdate } from './__generated__/MarketDepthSubscription';
import { import {
@ -46,7 +46,7 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
); );
const update = useCallback( const update = useCallback(
(delta: MarketDepthSubscription_marketDepthUpdate) => { ({ delta }: { delta: MarketDepthSubscription_marketDepthUpdate }) => {
if (!dataRef.current.rows) { if (!dataRef.current.rows) {
return false; return false;
} }
@ -76,11 +76,11 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
[] []
); );
const { data, error, loading, flush } = useDataProvider( const { data, error, loading, flush } = useDataProvider({
marketDepthDataProvider, dataProvider,
update, update,
variables variables,
); });
useEffect(() => { useEffect(() => {
if (!data) { if (!data) {

View File

@ -1,5 +1,5 @@
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import type { ValueFormatterParams } from 'ag-grid-community'; import type { IDatasource, ValueFormatterParams } from 'ag-grid-community';
import { import {
PriceFlashCell, PriceFlashCell,
addDecimalsFormatNumber, addDecimalsFormatNumber,
@ -8,30 +8,22 @@ import {
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { import type { Markets_markets } from '../__generated__/Markets';
Markets_markets,
Markets_markets_data_market,
} from '../__generated__/Markets';
interface MarketListTableProps { interface MarketListTableProps {
data: Markets_markets[] | null; datasource: IDatasource;
onRowClicked: (marketId: string) => void; onRowClicked: (marketId: string) => void;
} }
export const getRowId = ({
data,
}: {
data: Markets_markets | Markets_markets_data_market;
}) => data.id;
export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>( export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
({ data, onRowClicked }, ref) => { ({ datasource, onRowClicked }, ref) => {
return ( return (
<AgGrid <AgGrid
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')} overlayNoRowsTemplate={t('No markets')}
rowData={data} rowModelType="infinite"
getRowId={getRowId} datasource={datasource}
getRowId={({ data }) => data?.id}
ref={ref} ref={ref}
defaultColDef={{ defaultColDef={{
flex: 1, flex: 1,
@ -55,7 +47,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
headerName={t('State')} headerName={t('State')}
field="data" field="data"
valueFormatter={({ value }: ValueFormatterParams) => valueFormatter={({ value }: ValueFormatterParams) =>
`${value.market.state} (${value.market.tradingMode})` value === undefined
? value
: `${value.market.state} (${value.market.tradingMode})`
} }
/> />
<AgGridColumn <AgGridColumn
@ -64,7 +58,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) => valueFormatter={({ value, data }: ValueFormatterParams) =>
addDecimalsFormatNumber(value, data.decimalPlaces) value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
} }
/> />
<AgGridColumn <AgGridColumn
@ -72,7 +68,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
field="data.bestOfferPrice" field="data.bestOfferPrice"
type="rightAligned" type="rightAligned"
valueFormatter={({ value, data }: ValueFormatterParams) => valueFormatter={({ value, data }: ValueFormatterParams) =>
addDecimalsFormatNumber(value, data.decimalPlaces) value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
} }
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
/> />
@ -82,7 +80,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
type="rightAligned" type="rightAligned"
cellRenderer="PriceFlashCell" cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) => valueFormatter={({ value, data }: ValueFormatterParams) =>
addDecimalsFormatNumber(value, data.decimalPlaces) value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
} }
/> />
<AgGridColumn headerName={t('Description')} field="name" /> <AgGridColumn headerName={t('Description')} field="name" />

View File

@ -1,61 +1,51 @@
import { useRef, useCallback } from 'react'; import { useRef, useCallback } from 'react';
import { produce } from 'immer';
import merge from 'lodash/merge';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; 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 { useDataProvider } from '@vegaprotocol/react-helpers';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { IGetRowsParams } from 'ag-grid-community';
import type { import type {
Markets_markets, Markets_markets,
Markets_markets_data, Markets_markets_data,
} from '../../components/__generated__/Markets'; } from '../../components/__generated__/Markets';
import { marketsDataProvider } from './markets-data-provider'; import { marketsDataProvider as dataProvider } from './markets-data-provider';
export const MarketsContainer = () => { export const MarketsContainer = () => {
const { push } = useRouter(); const { push } = useRouter();
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const update = useCallback( const dataRef = useRef<Markets_markets[] | null>(null);
(delta: Markets_markets_data) => { const update = useCallback(({ data }: { data: Markets_markets[] }) => {
const update: Markets_markets[] = [];
const add: Markets_markets[] = [];
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
return false; return false;
} }
const rowNode = gridRef.current.api.getRowNode( dataRef.current = data;
getRowId({ data: delta.market }) gridRef.current.api.refreshInfiniteCache();
);
if (rowNode) {
const updatedData = produce<Markets_markets_data>(
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; return true;
}, }, []);
[gridRef]
);
const { data, error, loading } = useDataProvider< const { data, error, loading } = useDataProvider<
Markets_markets[], Markets_markets[],
Markets_markets_data 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 ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={loading} error={error} data={data}>
<MarketListTable <MarketListTable
datasource={{ getRows }}
ref={gridRef} ref={gridRef}
data={data}
onRowClicked={(id) => push(`/markets/${id}`)} onRowClicked={(id) => push(`/markets/${id}`)}
/> />
</AsyncRenderer> </AsyncRenderer>

View File

@ -91,14 +91,16 @@ const MARKET_DATA_SUB = gql`
} }
`; `;
const update = (data: Markets_markets[], delta: MarketDataSub_marketData) => const update = (data: Markets_markets[], delta: MarketDataSub_marketData) => {
produce(data, (draft) => { return produce(data, (draft) => {
const index = draft.findIndex((m) => m.id === delta.market.id); const index = draft.findIndex((m) => m.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index].data = delta; draft[index].data = delta;
} }
// @TODO - else push new market to draft // @TODO - else push new market to draft
}); });
};
const getData = (responseData: Markets): Markets_markets[] | null => const getData = (responseData: Markets): Markets_markets[] | null =>
responseData.markets; responseData.markets;
const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData => const getDelta = (subscriptionData: MarketDataSub): MarketDataSub_marketData =>

View File

@ -79,8 +79,8 @@ export const prepareIncomingOrders = (delta: OrderFields[]) => {
return incoming; return incoming;
}; };
const update = (data: OrderFields[], delta: OrderFields[]) => const update = (data: OrderFields[], delta: OrderFields[]) => {
produce(data, (draft) => { return produce(data, (draft) => {
const incoming = prepareIncomingOrders(delta); const incoming = prepareIncomingOrders(delta);
// Add or update incoming orders // Add or update incoming orders
@ -93,6 +93,7 @@ const update = (data: OrderFields[], delta: OrderFields[]) =>
} }
}); });
}); });
};
const getData = (responseData: Orders): Orders_party_orders[] | null => const getData = (responseData: Orders): Orders_party_orders[] | null =>
responseData?.party?.orders || null; responseData?.party?.orders || null;

View File

@ -3,7 +3,7 @@ import { OrderList } from '../order-list';
import type { OrderFields } from '../__generated__/OrderFields'; import type { OrderFields } from '../__generated__/OrderFields';
import { useDataProvider } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/react-helpers';
import { import {
ordersDataProvider, ordersDataProvider as dataProvider,
prepareIncomingOrders, prepareIncomingOrders,
sortOrders, sortOrders,
} from '../order-data-provider'; } from '../order-data-provider';
@ -21,7 +21,7 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
// Apply updates to the table // Apply updates to the table
const update = useCallback((delta: OrderSub_orders[]) => { const update = useCallback(({ delta }: { delta: OrderSub_orders[] }) => {
if (!gridRef.current) { if (!gridRef.current) {
return false; return false;
} }
@ -57,11 +57,11 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
return true; return true;
}, []); }, []);
const { data, error, loading } = useDataProvider( const { data, error, loading } = useDataProvider({
ordersDataProvider, dataProvider,
update, update,
variables variables,
); });
const orders = useMemo(() => { const orders = useMemo(() => {
if (!data) { if (!data) {

View File

@ -78,8 +78,8 @@ export const POSITIONS_SUB = gql`
const update = ( const update = (
data: Positions_party_positions[], data: Positions_party_positions[],
delta: PositionSubscribe_positions delta: PositionSubscribe_positions
) => ) => {
produce(data, (draft) => { return produce(data, (draft) => {
const index = draft.findIndex((m) => m.market.id === delta.market.id); const index = draft.findIndex((m) => m.market.id === delta.market.id);
if (index !== -1) { if (index !== -1) {
draft[index] = delta; draft[index] = delta;
@ -87,6 +87,8 @@ const update = (
draft.push(delta); draft.push(delta);
} }
}); });
};
const getData = (responseData: Positions): Positions_party_positions[] | null => const getData = (responseData: Positions): Positions_party_positions[] | null =>
responseData.party ? responseData.party.positions : null; responseData.party ? responseData.party.positions : null;
const getDelta = ( const getDelta = (

View File

@ -18,7 +18,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null); const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
const update = useCallback( const update = useCallback(
(delta: PositionSubscribe_positions) => { ({ delta }: { delta: PositionSubscribe_positions }) => {
const update: Positions_party_positions[] = []; const update: Positions_party_positions[] = [];
const add: Positions_party_positions[] = []; const add: Positions_party_positions[] = [];
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
@ -52,7 +52,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const { data, error, loading } = useDataProvider< const { data, error, loading } = useDataProvider<
Positions_party_positions[], Positions_party_positions[],
PositionSubscribe_positions PositionSubscribe_positions
>(positionsDataProvider, update, variables); >({ dataProvider: positionsDataProvider, update, variables });
return ( return (
<AsyncRenderer loading={loading} error={error} data={data}> <AsyncRenderer loading={loading} error={error} data={data}>
<PositionsTable ref={gridRef} data={data} /> <PositionsTable ref={gridRef} data={data} />

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useCallback } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import type { OperationVariables } 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 * @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}}; * @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/ */
export function useDataProvider<Data, Delta>( export function useDataProvider<Data, Delta>({
dataProvider: Subscribe<Data, Delta>, dataProvider,
update?: (delta: Delta) => boolean, update,
variables?: OperationVariables insert,
) { variables,
}: {
dataProvider: Subscribe<Data, Delta>;
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 client = useApolloClient();
const [data, setData] = useState<Data | null>(null); const [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>(undefined); const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined); const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined); const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
const initialized = useRef<boolean>(false); const initialized = useRef<boolean>(false);
const flush = useCallback(() => { const flush = useCallback(() => {
if (flushRef.current) { if (flushRef.current) {
@ -32,30 +48,48 @@ export function useDataProvider<Data, Delta>(
reloadRef.current(force); reloadRef.current(force);
} }
}, []); }, []);
const load = useCallback((pagination: Pagination) => {
if (loadRef.current) {
return loadRef.current(pagination);
}
return Promise.reject();
}, []);
const callback = useCallback( const callback = useCallback(
({ data, error, loading, delta }) => { ({ data, error, loading, delta, insertionData, totalCount }) => {
setError(error); setError(error);
setLoading(loading); setLoading(loading);
if (!error && !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 // component can use flush() which will call callback without delta and cause data state update
if (!initialized.current || !delta || !update || !update(delta)) { if (initialized.current) {
if (delta && update && update({ delta, data })) {
return;
}
if (
insertionData &&
insert &&
insert({ insertionData, data, totalCount })
) {
return;
}
}
initialized.current = true; initialized.current = true;
setTotalCount(totalCount);
setData(data); setData(data);
} }
}
}, },
[update] [update, insert]
); );
useEffect(() => { useEffect(() => {
const { unsubscribe, flush, reload } = dataProvider( const { unsubscribe, flush, reload, load } = dataProvider(
callback, callback,
client, client,
variables variables
); );
flushRef.current = flush; flushRef.current = flush;
reloadRef.current = reload; reloadRef.current = reload;
loadRef.current = load;
return unsubscribe; return unsubscribe;
}, [client, initialized, dataProvider, callback, variables]); }, [client, initialized, dataProvider, callback, variables]);
return { data, loading, error, flush, reload }; return { data, loading, error, flush, reload, load, totalCount };
} }

View File

@ -13,9 +13,30 @@ export interface UpdateCallback<Data, Delta> {
data: Data | null; data: Data | null;
error?: Error; error?: Error;
loading: boolean; loading: boolean;
pageInfo: PageInfo | null;
delta?: Delta; delta?: Delta;
insertionData?: Data | null;
totalCount?: number;
}): void; }): void;
} }
export interface Load<Data> {
(pagination: Pagination): Promise<Data | null>;
}
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<Data, Delta> { export interface Subscribe<Data, Delta> {
( (
callback: UpdateCallback<Data, Delta>, callback: UpdateCallback<Data, Delta>,
@ -25,6 +46,7 @@ export interface Subscribe<Data, Delta> {
unsubscribe: () => void; unsubscribe: () => void;
reload: (forceReset?: boolean) => void; reload: (forceReset?: boolean) => void;
flush: () => void; flush: () => void;
load: Load<Data>;
}; };
} }
@ -35,8 +57,29 @@ export interface Update<Data, Delta> {
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data; (data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
} }
export interface Append<Data> {
(
data: Data | null,
pageInfo: PageInfo,
insertionData: Data | null,
insertionPageInfo: PageInfo | null,
pagination?: Pagination
): {
data: Data | null;
pageInfo: PageInfo;
};
}
interface GetData<QueryData, Data> { interface GetData<QueryData, Data> {
(subscriptionData: QueryData): Data | null; (queryData: QueryData): Data | null;
}
interface GetPageInfo<QueryData> {
(queryData: QueryData): PageInfo | null;
}
interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
} }
interface GetDelta<SubscriptionData, Delta> { interface GetDelta<SubscriptionData, Delta> {
@ -57,6 +100,12 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
update: Update<Data, Delta>, update: Update<Data, Delta>,
getData: GetData<QueryData, Data>, getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>, getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
fetchPolicy: FetchPolicy = 'no-cache' fetchPolicy: FetchPolicy = 'no-cache'
): Subscribe<Data, Delta> { ): Subscribe<Data, Delta> {
// list of callbacks passed through subscribe call // list of callbacks passed through subscribe call
@ -70,20 +119,60 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
let loading = false; let loading = false;
let client: ApolloClient<object> | undefined = undefined; let client: ApolloClient<object> | undefined = undefined;
let subscription: Subscription | 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 // notify single callback about current state, delta is passes optionally only if notify was invoked onNext
const notify = (callback: UpdateCallback<Data, Delta>, delta?: Delta) => { const notify = (
callback: UpdateCallback<Data, Delta>,
dataUpdate?: { delta?: Delta; insertionData?: Data | null }
) => {
callback({ callback({
data, data,
error, error,
loading, loading,
delta, pageInfo,
totalCount,
...dataUpdate,
}); });
}; };
// notify all callbacks // notify all callbacks
const notifyAll = (delta?: Delta) => { const notifyAll = (dataUpdate?: {
callbacks.forEach((callback) => notify(callback, delta)); 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<QueryData>({
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 () => { const initialFetch = async () => {
@ -93,10 +182,16 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
try { try {
const res = await client.query<QueryData>({ const res = await client.query<QueryData>({
query, query,
variables, variables: pagination
? { ...variables, pagination: { first: pagination.first } }
: variables,
fetchPolicy, fetchPolicy,
}); });
data = getData(res.data); 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 there was some updates received from subscription during initial query loading apply them on just received data
if (data && updateQueue && updateQueue.length > 0) { if (data && updateQueue && updateQueue.length > 0) {
while (updateQueue.length) { while (updateQueue.length) {
@ -165,7 +260,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
return; return;
} }
data = newData; data = newData;
notifyAll(delta); notifyAll({ delta });
} }
}, },
() => reload() () => reload()
@ -205,6 +300,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
unsubscribe: () => unsubscribe(callback), unsubscribe: () => unsubscribe(callback),
reload, reload,
flush: () => notify(callback), flush: () => notify(callback),
load,
}; };
}; };
} }
@ -239,7 +335,7 @@ const memoize = <Data, Delta>(
* @param update Update<Data, Delta> function that will be executed on each onNext, it should update data base on delta, it can reload data provider * @param update Update<Data, Delta> 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 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 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<Data, Delta> subscribe function * @returns Subscribe<Data, Delta> subscribe function
* @example * @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>( * const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
@ -263,6 +359,12 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
update: Update<Data, Delta>, update: Update<Data, Delta>,
getData: GetData<QueryData, Data>, getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>, getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
fetchPolicy: FetchPolicy = 'no-cache' fetchPolicy: FetchPolicy = 'no-cache'
): Subscribe<Data, Delta> { ): Subscribe<Data, Delta> {
const getInstance = memoize<Data, Delta>(() => const getInstance = memoize<Data, Delta>(() =>
@ -272,6 +374,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
update, update,
getData, getData,
getDelta, getDelta,
pagination,
fetchPolicy fetchPolicy
) )
); );

View File

@ -6,7 +6,7 @@ import { useCallback, useMemo, useRef } from 'react';
import { import {
MAX_TRADES, MAX_TRADES,
sortTrades, sortTrades,
tradesDataProvider, tradesDataProvider as dataProvider,
} from './trades-data-provider'; } from './trades-data-provider';
import { TradesTable } from './trades-table'; import { TradesTable } from './trades-table';
import type { TradeFields } from './__generated__/TradeFields'; import type { TradeFields } from './__generated__/TradeFields';
@ -22,7 +22,7 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
() => ({ marketId, maxTrades: MAX_TRADES }), () => ({ marketId, maxTrades: MAX_TRADES }),
[marketId] [marketId]
); );
const update = useCallback((delta: TradeFields[]) => { const update = useCallback(({ delta }: { delta: TradeFields[] }) => {
if (!gridRef.current?.api) { if (!gridRef.current?.api) {
return false; return false;
} }
@ -43,11 +43,11 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
return true; return true;
}, []); }, []);
const { data, error, loading } = useDataProvider( const { data, error, loading } = useDataProvider({
tradesDataProvider, dataProvider,
update, update,
variables variables,
); });
return ( return (
<AsyncRenderer <AsyncRenderer

View File

@ -53,8 +53,8 @@ export const sortTrades = (trades: TradeFields[]) => {
); );
}; };
const update = (data: TradeFields[], delta: TradeFields[]) => const update = (data: TradeFields[], delta: TradeFields[]) => {
produce(data, (draft) => { return produce(data, (draft) => {
const incoming = sortTrades(delta); const incoming = sortTrades(delta);
// Add new trades to the top // Add new trades to the top
@ -65,6 +65,7 @@ const update = (data: TradeFields[], delta: TradeFields[]) =>
draft.splice(MAX_TRADES, draft.length - MAX_TRADES); draft.splice(MAX_TRADES, draft.length - MAX_TRADES);
} }
}); });
};
const getData = (responseData: Trades): TradeFields[] | null => const getData = (responseData: Trades): TradeFields[] | null =>
responseData.market ? responseData.market.trades : null; responseData.market ? responseData.market.trades : null;

View File

@ -7437,14 +7437,14 @@ aes-js@^3.1.2:
integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ== integrity sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==
ag-grid-community@^27.0.1: ag-grid-community@^27.0.1:
version "27.1.0" version "27.3.0"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-27.1.0.tgz#17f73173444a9efc4faea0f0cd6c5090e698f7ee" resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-27.3.0.tgz#b1e94a58026aaf2f0cd7920e35833325b5e762c7"
integrity sha512-SWzIJTNa7C6Vinizelcoc1FAJQRt1pDn+A8XHQDO2GTQT+VjBnPL8fg94fLJy0EEvqaN5IhDybNS0nD07SKIQw== integrity sha512-R5oZMXEHXnOLrmhn91J8lR0bv6IAnRcU6maO+wKLMJxffRWaAYFAuw1jt7bdmcKCv8c65F6LEBx4ykSOALa9vA==
ag-grid-react@^27.0.1: ag-grid-react@^27.0.1:
version "27.1.0" version "27.3.0"
resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-27.1.0.tgz#3b08203b9731a2b2d5431dddd69d68dc640c311e" resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-27.3.0.tgz#fe06647653f8b0b349b8e613aab8ea2e07915562"
integrity sha512-AfRwH6BL/LribvLJ2594Fq0/MfZf/17WebjGj927bM3vABDr2OBX3qgMIaQE+kpV9mABPb51rlWLMmbCvltv2g== integrity sha512-2bs9YfJ/shvBZQLLjny4NFvht+ic6VtpTPO0r3bHHOhlL3Fjx2rGvS6AHSwfvu+kJacHCta30PjaEbX8T3UDyw==
dependencies: dependencies:
prop-types "^15.8.1" 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: object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 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: object-copy@^0.1.0:
version "0.1.0" version "0.1.0"