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 = (
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 = (

View File

@ -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<SimpleMarketsType> = useMarketsFilterData(
data || [],
params

View File

@ -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 = (

View File

@ -22,7 +22,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(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 (
<AsyncRenderer loading={loading} error={error} data={data}>
<AccountsTable ref={gridRef} data={data} />

View File

@ -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,
}
);

View File

@ -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<AgGridReact | null>(null);
const variables = useMemo<FillsVariables>(
() => ({
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<Fills_party_tradesPaged_edges[] | null>(null);
const totalCountRef = useRef<number | undefined>(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 (
<AsyncRenderer data={fills} loading={loading} error={error}>
<FillsTable ref={gridRef} partyId={partyId} fills={fills} />
<AsyncRenderer loading={loading} error={error} data={data}>
<FillsTable ref={gridRef} partyId={partyId} datasource={{ getRows }} />
</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 { 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<FillFields>;
@ -34,9 +47,14 @@ describe('FillsTable', () => {
});
it('correct columns are rendered', async () => {
await act(async () => {
render(<FillsTable partyId="party-id" fills={[generateFill()]} />);
});
render(
<FillsTable
partyId="party-id"
datasource={{ getRows: makeGetRows([generateFill()]) }}
/>
);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(7);
@ -66,14 +84,14 @@ describe('FillsTable', () => {
},
});
const { container } = render(
<FillsTable partyId={partyId} fills={[buyerFill]} />
render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
);
// 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(
<FillsTable partyId={partyId} fills={[buyerFill]} />
render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([buyerFill]) }}
/>
);
// 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(
<FillsTable partyId={partyId} fills={[takerFill]} />
const { rerender } = render(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([takerFill]) }}
/>
);
// 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(<FillsTable partyId={partyId} fills={[makerFill]} />);
rerender(
<FillsTable
partyId={partyId}
datasource={{ getRows: makeGetRows([makerFill]) }}
/>
);
await waitForGridToBeInTheDOM();
await waitForDataToHaveLoaded();
expect(
screen

View File

@ -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) || []
),
},
};

View File

@ -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<AgGridReact, FillsTableProps>(
({ partyId, fills }, ref) => {
({ partyId, datasource }, ref) => {
return (
<AgGrid
ref={ref}
rowData={fills}
overlayNoRowsTemplate={t('No fills')}
defaultColDef={{ flex: 1, resizable: true }}
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
@ -36,9 +37,9 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
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<AgGridReact, FillsTableProps>(
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<AgGridReact, FillsTableProps>(
);
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 '-';
}

View File

@ -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);
};

View File

@ -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) {

View File

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

View File

@ -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);
});

View File

@ -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) {

View File

@ -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<AgGridReact, MarketListTableProps>(
({ data, onRowClicked }, ref) => {
({ datasource, onRowClicked }, ref) => {
return (
<AgGrid
style={{ width: '100%', height: '100%' }}
overlayNoRowsTemplate={t('No markets')}
rowData={data}
getRowId={getRowId}
rowModelType="infinite"
datasource={datasource}
getRowId={({ data }) => data?.id}
ref={ref}
defaultColDef={{
flex: 1,
@ -55,7 +47,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
headerName={t('State')}
field="data"
valueFormatter={({ value }: ValueFormatterParams) =>
`${value.market.state} (${value.market.tradingMode})`
value === undefined
? value
: `${value.market.state} (${value.market.tradingMode})`
}
/>
<AgGridColumn
@ -64,7 +58,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
addDecimalsFormatNumber(value, data.decimalPlaces)
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn
@ -72,7 +68,9 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
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<AgGridReact, MarketListTableProps>(
type="rightAligned"
cellRenderer="PriceFlashCell"
valueFormatter={({ value, data }: ValueFormatterParams) =>
addDecimalsFormatNumber(value, data.decimalPlaces)
value === undefined
? value
: addDecimalsFormatNumber(value, data.decimalPlaces)
}
/>
<AgGridColumn headerName={t('Description')} field="name" />

View File

@ -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<AgGridReact | null>(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<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;
},
[gridRef]
);
const dataRef = useRef<Markets_markets[] | null>(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 (
<AsyncRenderer loading={loading} error={error} data={data}>
<MarketListTable
datasource={{ getRows }}
ref={gridRef}
data={data}
onRowClicked={(id) => push(`/markets/${id}`)}
/>
</AsyncRenderer>

View File

@ -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 =>

View File

@ -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;

View File

@ -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) {

View File

@ -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 = (

View File

@ -18,7 +18,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
const gridRef = useRef<AgGridReact | null>(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 (
<AsyncRenderer loading={loading} error={error} data={data}>
<PositionsTable ref={gridRef} data={data} />

View File

@ -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<Data, Delta>(
dataProvider: Subscribe<Data, Delta>,
update?: (delta: Delta) => boolean,
variables?: OperationVariables
) {
export function useDataProvider<Data, Delta>({
dataProvider,
update,
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 [data, setData] = useState<Data | null>(null);
const [totalCount, setTotalCount] = useState<number>();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<Error | undefined>(undefined);
const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
const initialized = useRef<boolean>(false);
const flush = useCallback(() => {
if (flushRef.current) {
@ -32,30 +48,48 @@ export function useDataProvider<Data, Delta>(
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 };
}

View File

@ -13,9 +13,30 @@ export interface UpdateCallback<Data, Delta> {
data: Data | null;
error?: Error;
loading: boolean;
pageInfo: PageInfo | null;
delta?: Delta;
insertionData?: Data | null;
totalCount?: number;
}): 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> {
(
callback: UpdateCallback<Data, Delta>,
@ -25,6 +46,7 @@ export interface Subscribe<Data, Delta> {
unsubscribe: () => void;
reload: (forceReset?: boolean) => void;
flush: () => void;
load: Load<Data>;
};
}
@ -35,8 +57,29 @@ export interface Update<Data, Delta> {
(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> {
(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> {
@ -57,6 +100,12 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
update: Update<Data, Delta>,
getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
fetchPolicy: FetchPolicy = 'no-cache'
): Subscribe<Data, Delta> {
// list of callbacks passed through subscribe call
@ -70,20 +119,60 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
let loading = false;
let client: ApolloClient<object> | 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<Data, Delta>, delta?: Delta) => {
const notify = (
callback: UpdateCallback<Data, Delta>,
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<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 () => {
@ -93,10 +182,16 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
try {
const res = await client.query<QueryData>({
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<QueryData, Data, SubscriptionData, Delta>(
return;
}
data = newData;
notifyAll(delta);
notifyAll({ delta });
}
},
() => reload()
@ -205,6 +300,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
unsubscribe: () => unsubscribe(callback),
reload,
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 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<Data, Delta> subscribe function
* @example
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
@ -263,6 +359,12 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
update: Update<Data, Delta>,
getData: GetData<QueryData, Data>,
getDelta: GetDelta<SubscriptionData, Delta>,
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount: GetTotalCount<QueryData>;
append: Append<Data>;
first: number;
},
fetchPolicy: FetchPolicy = 'no-cache'
): Subscribe<Data, Delta> {
const getInstance = memoize<Data, Delta>(() =>
@ -272,6 +374,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
update,
getData,
getDelta,
pagination,
fetchPolicy
)
);

View File

@ -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 (
<AsyncRenderer

View File

@ -53,8 +53,8 @@ export const sortTrades = (trades: TradeFields[]) => {
);
};
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;

View File

@ -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"