Feature/218 trades scrolling (#740)
* feat(#218): add skip pagination support to data provider * feat(#218): add new rows after user scroll top * feat(#218): add pagination/scroll scenarios to storybook * feat(#218): switch fills to infinite scroll mode * feat(#218): switch trades to infinite scroll mode * feat(#218): fix e2e tests * feat(#218): set rowModelType to infinite * feat(#218): fix markets-list * feat(#218): remove totalCount * feat(#218): remove totalCount from mocks, revert cypress config * feat(#218): allow null data in data-provider whan using pagination * feat(#218): add missing hasNextPage, handle only one page scenario * feat(#218): improve typing in fills, trades ann market-list tables
This commit is contained in:
parent
4fe3c916be
commit
556be89bfd
@ -51,7 +51,6 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
id: 'buyer-id',
|
id: 'buyer-id',
|
||||||
tradesConnection: {
|
tradesConnection: {
|
||||||
__typename: 'TradeConnection',
|
__typename: 'TradeConnection',
|
||||||
totalCount: 1,
|
|
||||||
edges: fills.map((f) => {
|
edges: fills.map((f) => {
|
||||||
return {
|
return {
|
||||||
__typename: 'TradeEdge',
|
__typename: 'TradeEdge',
|
||||||
@ -63,6 +62,8 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
__typename: 'PageInfo',
|
__typename: 'PageInfo',
|
||||||
startCursor: '1',
|
startCursor: '1',
|
||||||
endCursor: '2',
|
endCursor: '2',
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
import type { Trades, Trades_market_trades } from '@vegaprotocol/trades';
|
import type {
|
||||||
|
Trades,
|
||||||
|
Trades_market_tradesConnection_edges_node,
|
||||||
|
} from '@vegaprotocol/trades';
|
||||||
|
|
||||||
export const generateTrades = (override?: PartialDeep<Trades>): Trades => {
|
export const generateTrades = (override?: PartialDeep<Trades>): Trades => {
|
||||||
const trades: Trades_market_trades[] = [
|
const trades: Trades_market_tradesConnection_edges_node[] = [
|
||||||
{
|
{
|
||||||
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
|
id: 'FFFFBC80005C517A10ACF481F7E6893769471098E696D0CC407F18134044CB16',
|
||||||
price: '17116898',
|
price: '17116898',
|
||||||
@ -44,10 +47,26 @@ export const generateTrades = (override?: PartialDeep<Trades>): Trades => {
|
|||||||
__typename: 'Trade',
|
__typename: 'Trade',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const defaultResult = {
|
const defaultResult: Trades = {
|
||||||
market: {
|
market: {
|
||||||
id: 'market-0',
|
id: 'market-0',
|
||||||
trades,
|
tradesConnection: {
|
||||||
|
__typename: 'TradeConnection',
|
||||||
|
edges: trades.map((node, i) => {
|
||||||
|
return {
|
||||||
|
__typename: 'TradeEdge',
|
||||||
|
node,
|
||||||
|
cursor: (i + 1).toString(),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
pageInfo: {
|
||||||
|
__typename: 'PageInfo',
|
||||||
|
startCursor: '0',
|
||||||
|
endCursor: trades.length.toString(),
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
__typename: 'Market',
|
__typename: 'Market',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
6
libs/fills/src/lib/__generated__/Fills.ts
generated
6
libs/fills/src/lib/__generated__/Fills.ts
generated
@ -206,14 +206,12 @@ export interface Fills_party_tradesConnection_pageInfo {
|
|||||||
__typename: "PageInfo";
|
__typename: "PageInfo";
|
||||||
startCursor: string;
|
startCursor: string;
|
||||||
endCursor: string;
|
endCursor: string;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fills_party_tradesConnection {
|
export interface Fills_party_tradesConnection {
|
||||||
__typename: "TradeConnection";
|
__typename: "TradeConnection";
|
||||||
/**
|
|
||||||
* The total number of trades in this connection
|
|
||||||
*/
|
|
||||||
totalCount: number;
|
|
||||||
/**
|
/**
|
||||||
* The trade in this connection
|
* The trade in this connection
|
||||||
*/
|
*/
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
import type { PageInfo, Pagination } from '@vegaprotocol/react-helpers';
|
makeDataProvider,
|
||||||
|
defaultAppend as append,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
||||||
import type { FillFields } from './__generated__/FillFields';
|
import type { FillFields } from './__generated__/FillFields';
|
||||||
import type {
|
import type {
|
||||||
Fills,
|
Fills,
|
||||||
Fills_party_tradesConnection_edges,
|
Fills_party_tradesConnection_edges,
|
||||||
|
Fills_party_tradesConnection_edges_node,
|
||||||
} from './__generated__/Fills';
|
} from './__generated__/Fills';
|
||||||
import type { FillsSub } from './__generated__/FillsSub';
|
import type { FillsSub } from './__generated__/FillsSub';
|
||||||
|
|
||||||
@ -64,7 +69,6 @@ export const FILLS_QUERY = gql`
|
|||||||
party(id: $partyId) {
|
party(id: $partyId) {
|
||||||
id
|
id
|
||||||
tradesConnection(marketId: $marketId, pagination: $pagination) {
|
tradesConnection(marketId: $marketId, pagination: $pagination) {
|
||||||
totalCount
|
|
||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
...FillFields
|
...FillFields
|
||||||
@ -74,6 +78,8 @@ export const FILLS_QUERY = gql`
|
|||||||
pageInfo {
|
pageInfo {
|
||||||
startCursor
|
startCursor
|
||||||
endCursor
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,16 +96,24 @@ export const FILLS_SUB = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const update = (
|
const update = (
|
||||||
data: Fills_party_tradesConnection_edges[],
|
data: (Fills_party_tradesConnection_edges | null)[],
|
||||||
delta: FillFields[]
|
delta: FillFields[]
|
||||||
) => {
|
) => {
|
||||||
return produce(data, (draft) => {
|
return produce(data, (draft) => {
|
||||||
delta.forEach((node) => {
|
orderBy(delta, 'createdAt').forEach((node) => {
|
||||||
const index = draft.findIndex((edge) => edge.node.id === node.id);
|
const index = draft.findIndex((edge) => edge?.node.id === node.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
Object.assign(draft[index].node, node);
|
if (draft[index]?.node) {
|
||||||
|
Object.assign(
|
||||||
|
draft[index]?.node as Fills_party_tradesConnection_edges_node,
|
||||||
|
node
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
|
const firstNode = draft[0]?.node;
|
||||||
|
if (firstNode && node.createdAt >= firstNode.createdAt) {
|
||||||
|
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -113,40 +127,8 @@ const getData = (
|
|||||||
const getPageInfo = (responseData: Fills): PageInfo | null =>
|
const getPageInfo = (responseData: Fills): PageInfo | null =>
|
||||||
responseData.party?.tradesConnection.pageInfo || null;
|
responseData.party?.tradesConnection.pageInfo || null;
|
||||||
|
|
||||||
const getTotalCount = (responseData: Fills): number | undefined =>
|
|
||||||
responseData.party?.tradesConnection.totalCount;
|
|
||||||
|
|
||||||
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
|
const getDelta = (subscriptionData: FillsSub) => subscriptionData.trades || [];
|
||||||
|
|
||||||
const append = (
|
|
||||||
data: Fills_party_tradesConnection_edges[] | null,
|
|
||||||
pageInfo: PageInfo,
|
|
||||||
insertionData: Fills_party_tradesConnection_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,
|
||||||
@ -155,7 +137,6 @@ export const fillsDataProvider = makeDataProvider(
|
|||||||
getDelta,
|
getDelta,
|
||||||
{
|
{
|
||||||
getPageInfo,
|
getPageInfo,
|
||||||
getTotalCount,
|
|
||||||
append,
|
append,
|
||||||
first: 100,
|
first: 100,
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,11 @@ import { useCallback, useRef, useMemo } from 'react';
|
|||||||
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 { FillsTable } from './fills-table';
|
import { FillsTable } from './fills-table';
|
||||||
import type { IGetRowsParams } from 'ag-grid-community';
|
import type {
|
||||||
|
IGetRowsParams,
|
||||||
|
BodyScrollEvent,
|
||||||
|
BodyScrollEndEvent,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
|
||||||
import { fillsDataProvider as dataProvider } from './fills-data-provider';
|
import { fillsDataProvider as dataProvider } from './fills-data-provider';
|
||||||
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
|
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
|
||||||
@ -15,14 +19,46 @@ 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 dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
|
const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const totalCountRef = useRef<number | undefined>(undefined);
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
|
||||||
|
const addNewRows = useCallback(() => {
|
||||||
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
|
totalCountRef.current += newRows.current;
|
||||||
|
}
|
||||||
|
newRows.current = 0;
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
({ data }: { data: Fills_party_tradesConnection_edges[] }) => {
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: (Fills_party_tradesConnection_edges | null)[];
|
||||||
|
delta: FillsSub_trades[];
|
||||||
|
}) => {
|
||||||
if (!gridRef.current?.api) {
|
if (!gridRef.current?.api) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
return true;
|
return true;
|
||||||
@ -35,7 +71,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
data,
|
data,
|
||||||
totalCount,
|
totalCount,
|
||||||
}: {
|
}: {
|
||||||
data: Fills_party_tradesConnection_edges[];
|
data: (Fills_party_tradesConnection_edges | null)[];
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}) => {
|
}) => {
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
@ -48,7 +84,7 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
|
||||||
const { data, error, loading, load, totalCount } = useDataProvider<
|
const { data, error, loading, load, totalCount } = useDataProvider<
|
||||||
Fills_party_tradesConnection_edges[],
|
(Fills_party_tradesConnection_edges | null)[],
|
||||||
FillsSub_trades[]
|
FillsSub_trades[]
|
||||||
>({ dataProvider, update, insert, variables });
|
>({ dataProvider, update, insert, variables });
|
||||||
totalCountRef.current = totalCount;
|
totalCountRef.current = totalCount;
|
||||||
@ -60,15 +96,14 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
startRow,
|
startRow,
|
||||||
endRow,
|
endRow,
|
||||||
}: IGetRowsParams) => {
|
}: IGetRowsParams) => {
|
||||||
|
startRow += newRows.current;
|
||||||
|
endRow += newRows.current;
|
||||||
try {
|
try {
|
||||||
if (dataRef.current && dataRef.current.length < endRow) {
|
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
|
||||||
await load({
|
await load();
|
||||||
first: endRow - startRow,
|
|
||||||
after: dataRef.current[dataRef.current.length - 1].cursor,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const rowsThisBlock = dataRef.current
|
const rowsThisBlock = dataRef.current
|
||||||
? dataRef.current.slice(startRow, endRow).map((edge) => edge.node)
|
? dataRef.current.slice(startRow, endRow).map((edge) => edge?.node)
|
||||||
: [];
|
: [];
|
||||||
let lastRow = -1;
|
let lastRow = -1;
|
||||||
if (totalCountRef.current !== undefined) {
|
if (totalCountRef.current !== undefined) {
|
||||||
@ -77,6 +112,8 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
} else if (totalCountRef.current <= endRow) {
|
} else if (totalCountRef.current <= endRow) {
|
||||||
lastRow = totalCountRef.current;
|
lastRow = totalCountRef.current;
|
||||||
}
|
}
|
||||||
|
} else if (rowsThisBlock.length < endRow - startRow) {
|
||||||
|
lastRow = rowsThisBlock.length;
|
||||||
}
|
}
|
||||||
successCallback(rowsThisBlock, lastRow);
|
successCallback(rowsThisBlock, lastRow);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -84,9 +121,26 @@ export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
<FillsTable ref={gridRef} partyId={partyId} datasource={{ getRows }} />
|
<FillsTable
|
||||||
|
ref={gridRef}
|
||||||
|
partyId={partyId}
|
||||||
|
datasource={{ getRows }}
|
||||||
|
rowModelType="infinite"
|
||||||
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
/>
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ 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, makeGetRows } from './test-helpers';
|
import { generateFill } from './test-helpers';
|
||||||
import type { FillFields } from './__generated__/FillFields';
|
import type { FillFields } from './__generated__/FillFields';
|
||||||
|
|
||||||
const waitForGridToBeInTheDOM = () => {
|
const waitForGridToBeInTheDOM = () => {
|
||||||
@ -47,12 +47,7 @@ describe('FillsTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('correct columns are rendered', async () => {
|
it('correct columns are rendered', async () => {
|
||||||
render(
|
render(<FillsTable partyId="party-id" rowData={[generateFill()]} />);
|
||||||
<FillsTable
|
|
||||||
partyId="party-id"
|
|
||||||
datasource={{ getRows: makeGetRows([generateFill()]) }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await waitForGridToBeInTheDOM();
|
await waitForGridToBeInTheDOM();
|
||||||
await waitForDataToHaveLoaded();
|
await waitForDataToHaveLoaded();
|
||||||
|
|
||||||
@ -84,12 +79,7 @@ describe('FillsTable', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||||
<FillsTable
|
|
||||||
partyId={partyId}
|
|
||||||
datasource={{ getRows: makeGetRows([buyerFill]) }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await waitForGridToBeInTheDOM();
|
await waitForGridToBeInTheDOM();
|
||||||
await waitForDataToHaveLoaded();
|
await waitForDataToHaveLoaded();
|
||||||
|
|
||||||
@ -126,12 +116,7 @@ describe('FillsTable', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||||
<FillsTable
|
|
||||||
partyId={partyId}
|
|
||||||
datasource={{ getRows: makeGetRows([buyerFill]) }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await waitForGridToBeInTheDOM();
|
await waitForGridToBeInTheDOM();
|
||||||
await waitForDataToHaveLoaded();
|
await waitForDataToHaveLoaded();
|
||||||
|
|
||||||
@ -163,10 +148,7 @@ describe('FillsTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { rerender } = render(
|
const { rerender } = render(
|
||||||
<FillsTable
|
<FillsTable partyId={partyId} rowData={[takerFill]} />
|
||||||
partyId={partyId}
|
|
||||||
datasource={{ getRows: makeGetRows([takerFill]) }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
await waitForGridToBeInTheDOM();
|
await waitForGridToBeInTheDOM();
|
||||||
await waitForDataToHaveLoaded();
|
await waitForDataToHaveLoaded();
|
||||||
@ -184,12 +166,7 @@ describe('FillsTable', () => {
|
|||||||
aggressor: Side.Buy,
|
aggressor: Side.Buy,
|
||||||
});
|
});
|
||||||
|
|
||||||
rerender(
|
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
|
||||||
<FillsTable
|
|
||||||
partyId={partyId}
|
|
||||||
datasource={{ getRows: makeGetRows([makerFill]) }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await waitForGridToBeInTheDOM();
|
await waitForGridToBeInTheDOM();
|
||||||
await waitForDataToHaveLoaded();
|
await waitForDataToHaveLoaded();
|
||||||
|
|
||||||
|
@ -1,22 +1,397 @@
|
|||||||
import type { Story, Meta } from '@storybook/react';
|
import type { Story, Meta } from '@storybook/react';
|
||||||
import type { FillsTableProps } from './fills-table';
|
import type { Props } from './fills-table';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
import { FillsTable } from './fills-table';
|
import { FillsTable } from './fills-table';
|
||||||
import { generateFills, makeGetRows } from './test-helpers';
|
import { generateFills, generateFill } from './test-helpers';
|
||||||
|
import type { Fills_party_tradesConnection_edges } from './__generated__/Fills';
|
||||||
|
import type { FillsSub_trades } from './__generated__/FillsSub';
|
||||||
|
import type {
|
||||||
|
IGetRowsParams,
|
||||||
|
BodyScrollEvent,
|
||||||
|
BodyScrollEndEvent,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: FillsTable,
|
component: FillsTable,
|
||||||
title: 'FillsTable',
|
title: 'FillsTable',
|
||||||
} as Meta;
|
} as Meta;
|
||||||
|
|
||||||
const Template: Story<FillsTableProps> = (args) => <FillsTable {...args} />;
|
const Template: Story<Props> = (args) => <FillsTable {...args} />;
|
||||||
|
|
||||||
export const Default = Template.bind({});
|
export const Default = Template.bind({});
|
||||||
|
|
||||||
|
const createdAt = new Date('2005-04-02 21:37:00').getTime();
|
||||||
const fills = generateFills();
|
const fills = generateFills();
|
||||||
Default.args = {
|
Default.args = {
|
||||||
partyId: 'party-id',
|
partyId: 'party-id',
|
||||||
datasource: {
|
rowData: fills.party?.tradesConnection.edges.map((e) => e.node) || [],
|
||||||
getRows: makeGetRows(
|
|
||||||
fills.party?.tradesConnection.edges.map((e) => e.node) || []
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getData = (
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
): Fills_party_tradesConnection_edges[] =>
|
||||||
|
new Array(end - start).fill(null).map((v, i) => ({
|
||||||
|
__typename: 'TradeEdge',
|
||||||
|
node: generateFill({
|
||||||
|
id: (start + i).toString(),
|
||||||
|
createdAt: new Date(createdAt - 1000 * (start + i)).toISOString(),
|
||||||
|
}),
|
||||||
|
cursor: (start + i).toString(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalCount = 550;
|
||||||
|
const partyId = 'partyId';
|
||||||
|
|
||||||
|
const useDataProvider = ({
|
||||||
|
insert,
|
||||||
|
}: {
|
||||||
|
insert: ({
|
||||||
|
insertionData,
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
insertionData: Fills_party_tradesConnection_edges[];
|
||||||
|
data: Fills_party_tradesConnection_edges[];
|
||||||
|
totalCount?: number;
|
||||||
|
}) => boolean;
|
||||||
|
}) => {
|
||||||
|
const data = [...getData(0, 100), ...new Array(totalCount - 100).fill(null)];
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
load: (start?: number, end?: number) => {
|
||||||
|
if (start === undefined) {
|
||||||
|
start = data.findIndex((v) => !v);
|
||||||
|
}
|
||||||
|
if (end === undefined) {
|
||||||
|
end = start + 100;
|
||||||
|
}
|
||||||
|
end = Math.min(end, totalCount);
|
||||||
|
const insertionData = getData(start, end);
|
||||||
|
data.splice(start, end - start, ...insertionData);
|
||||||
|
insert({ data, totalCount, insertionData });
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
totalCount,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PaginationManagerProps {
|
||||||
|
pagination: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationManager = ({ pagination }: PaginationManagerProps) => {
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const dataRef = useRef<Fills_party_tradesConnection_edges[] | null>(null);
|
||||||
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
|
||||||
|
const addNewRows = useCallback(() => {
|
||||||
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
|
totalCountRef.current += newRows.current;
|
||||||
|
}
|
||||||
|
newRows.current = 0;
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: Fills_party_tradesConnection_edges[];
|
||||||
|
delta: FillsSub_trades[];
|
||||||
|
}) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0].node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataRef.current = data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
data: Fills_party_tradesConnection_edges[];
|
||||||
|
totalCount?: number;
|
||||||
|
}) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading, load, totalCount } = useDataProvider({
|
||||||
|
insert,
|
||||||
|
});
|
||||||
|
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const getRows = async ({
|
||||||
|
successCallback,
|
||||||
|
failCallback,
|
||||||
|
startRow,
|
||||||
|
endRow,
|
||||||
|
}: IGetRowsParams) => {
|
||||||
|
startRow += newRows.current;
|
||||||
|
endRow += newRows.current;
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
dataRef.current &&
|
||||||
|
dataRef.current.slice(startRow, endRow).some((i) => !i)
|
||||||
|
) {
|
||||||
|
await load(startRow, endRow);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
lastRow = totalCountRef.current;
|
||||||
|
}
|
||||||
|
} else if (rowsThisBlock.length < endRow - startRow) {
|
||||||
|
lastRow = rowsThisBlock.length;
|
||||||
|
}
|
||||||
|
successCallback(rowsThisBlock, lastRow);
|
||||||
|
} catch (e) {
|
||||||
|
failCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// id and onclick is needed only for mocked data
|
||||||
|
let id = 0;
|
||||||
|
const onClick = () => {
|
||||||
|
if (!dataRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = generateFill({
|
||||||
|
id: (--id).toString(),
|
||||||
|
createdAt: new Date(createdAt - 1000 * id).toISOString(),
|
||||||
|
});
|
||||||
|
update({
|
||||||
|
data: [
|
||||||
|
{ cursor: '0', node, __typename: 'TradeEdge' },
|
||||||
|
...dataRef.current,
|
||||||
|
],
|
||||||
|
delta: [node],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={onClick}>Add row on top</Button>
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<FillsTable
|
||||||
|
rowModelType="infinite"
|
||||||
|
pagination={pagination}
|
||||||
|
ref={gridRef}
|
||||||
|
partyId={partyId}
|
||||||
|
datasource={{ getRows }}
|
||||||
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaginationTemplate: Story<PaginationManagerProps> = (args) => (
|
||||||
|
<PaginationManager {...args} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Pagination = PaginationTemplate.bind({});
|
||||||
|
Pagination.args = { pagination: true };
|
||||||
|
|
||||||
|
export const PaginationScroll = PaginationTemplate.bind({});
|
||||||
|
PaginationScroll.args = { pagination: false };
|
||||||
|
|
||||||
|
const InfiniteScrollManager = () => {
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const dataRef = useRef<(Fills_party_tradesConnection_edges | null)[] | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
|
||||||
|
const addNewRows = useCallback(() => {
|
||||||
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
|
totalCountRef.current += newRows.current;
|
||||||
|
}
|
||||||
|
newRows.current = 0;
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: (Fills_party_tradesConnection_edges | null)[];
|
||||||
|
delta: FillsSub_trades[];
|
||||||
|
}) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataRef.current = data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
data: Fills_party_tradesConnection_edges[];
|
||||||
|
totalCount?: number;
|
||||||
|
}) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading, load, totalCount } = useDataProvider({
|
||||||
|
insert,
|
||||||
|
});
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const getRows = async ({
|
||||||
|
successCallback,
|
||||||
|
failCallback,
|
||||||
|
startRow,
|
||||||
|
endRow,
|
||||||
|
}: IGetRowsParams) => {
|
||||||
|
startRow += newRows.current;
|
||||||
|
endRow += newRows.current;
|
||||||
|
try {
|
||||||
|
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} else if (rowsThisBlock.length < endRow - startRow) {
|
||||||
|
lastRow = rowsThisBlock.length;
|
||||||
|
}
|
||||||
|
successCallback(rowsThisBlock, lastRow);
|
||||||
|
} catch (e) {
|
||||||
|
failCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// id and onclick is needed only for mocked data
|
||||||
|
let id = 0;
|
||||||
|
const onClick = () => {
|
||||||
|
if (!dataRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = generateFill({
|
||||||
|
id: (--id).toString(),
|
||||||
|
createdAt: new Date(createdAt - 1000 * id).toISOString(),
|
||||||
|
});
|
||||||
|
update({
|
||||||
|
data: [
|
||||||
|
{ cursor: '0', node, __typename: 'TradeEdge' },
|
||||||
|
...dataRef.current,
|
||||||
|
],
|
||||||
|
delta: [node],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button onClick={onClick}>Add row on top</Button>
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<FillsTable
|
||||||
|
ref={gridRef}
|
||||||
|
partyId={partyId}
|
||||||
|
datasource={{ getRows }}
|
||||||
|
rowModelType="infinite"
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InfiniteScrollTemplate: Story<Record<string, never>> = () => (
|
||||||
|
<InfiniteScrollManager />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InfiniteScroll = InfiniteScrollTemplate.bind({});
|
||||||
|
@ -6,21 +6,32 @@ import {
|
|||||||
getDateTimeFormat,
|
getDateTimeFormat,
|
||||||
t,
|
t,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { Side } from '@vegaprotocol/types';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
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 { 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 type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
|
||||||
|
import type {
|
||||||
|
FillFields,
|
||||||
|
FillFields_market_tradableInstrument_instrument_product,
|
||||||
|
} from './__generated__/FillFields';
|
||||||
|
import type { Fills_party_tradesConnection_edges_node } from './__generated__/Fills';
|
||||||
|
|
||||||
export interface FillsTableProps {
|
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
datasource: IDatasource;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
|
type AccountsTableValueFormatterParams = Omit<
|
||||||
({ partyId, datasource }, ref) => {
|
ValueFormatterParams,
|
||||||
|
'data' | 'value'
|
||||||
|
> & {
|
||||||
|
data: Fills_party_tradesConnection_edges_node | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||||
|
({ partyId, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -28,8 +39,7 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
|
|||||||
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"
|
{...props}
|
||||||
datasource={datasource}
|
|
||||||
>
|
>
|
||||||
<AgGridColumn headerName={t('Market')} field="market.name" />
|
<AgGridColumn headerName={t('Market')} field="market.name" />
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
@ -69,7 +79,11 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
|
|||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Date')}
|
headerName={t('Date')}
|
||||||
field="createdAt"
|
field="createdAt"
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value: Fills_party_tradesConnection_edges_node['createdAt'];
|
||||||
|
}) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -81,9 +95,14 @@ export const FillsTable = forwardRef<AgGridReact, FillsTableProps>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatPrice = ({ value, data }: ValueFormatterParams) => {
|
const formatPrice = ({
|
||||||
if (value === undefined) {
|
value,
|
||||||
return value;
|
data,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value?: Fills_party_tradesConnection_edges_node['price'];
|
||||||
|
}) => {
|
||||||
|
if (value === undefined || !data) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
const asset =
|
const asset =
|
||||||
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||||
@ -95,9 +114,14 @@ const formatPrice = ({ value, data }: ValueFormatterParams) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatSize = (partyId: string) => {
|
const formatSize = (partyId: string) => {
|
||||||
return ({ value, data }: ValueFormatterParams) => {
|
return ({
|
||||||
if (value === undefined) {
|
value,
|
||||||
return value;
|
data,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value?: Fills_party_tradesConnection_edges_node['size'];
|
||||||
|
}) => {
|
||||||
|
if (value === undefined || !data) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
let prefix;
|
let prefix;
|
||||||
if (data?.buyer.id === partyId) {
|
if (data?.buyer.id === partyId) {
|
||||||
@ -114,9 +138,14 @@ const formatSize = (partyId: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTotal = ({ value, data }: ValueFormatterParams) => {
|
const formatTotal = ({
|
||||||
if (value === undefined) {
|
value,
|
||||||
return value;
|
data,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value?: Fills_party_tradesConnection_edges_node['price'];
|
||||||
|
}) => {
|
||||||
|
if (value === undefined || !data) {
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
const asset =
|
const asset =
|
||||||
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
|
||||||
@ -131,7 +160,12 @@ const formatTotal = ({ value, data }: ValueFormatterParams) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatRole = (partyId: string) => {
|
const formatRole = (partyId: string) => {
|
||||||
return ({ value, data }: ValueFormatterParams) => {
|
return ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value?: Fills_party_tradesConnection_edges_node['aggressor'];
|
||||||
|
}) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@ -156,7 +190,12 @@ const formatRole = (partyId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatFee = (partyId: string) => {
|
const formatFee = (partyId: string) => {
|
||||||
return ({ value, data }: ValueFormatterParams) => {
|
return ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: AccountsTableValueFormatterParams & {
|
||||||
|
value?: FillFields_market_tradableInstrument_instrument_product;
|
||||||
|
}) => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
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,
|
||||||
@ -52,7 +51,6 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
id: 'buyer-id',
|
id: 'buyer-id',
|
||||||
tradesConnection: {
|
tradesConnection: {
|
||||||
__typename: 'TradeConnection',
|
__typename: 'TradeConnection',
|
||||||
totalCount: 1,
|
|
||||||
edges: fills.map((f) => {
|
edges: fills.map((f) => {
|
||||||
return {
|
return {
|
||||||
__typename: 'TradeEdge',
|
__typename: 'TradeEdge',
|
||||||
@ -64,6 +62,8 @@ export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
|||||||
__typename: 'PageInfo',
|
__typename: 'PageInfo',
|
||||||
startCursor: '1',
|
startCursor: '1',
|
||||||
endCursor: '2',
|
endCursor: '2',
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
__typename: 'Party',
|
__typename: 'Party',
|
||||||
@ -79,7 +79,7 @@ export const generateFill = (
|
|||||||
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||||
__typename: 'Trade',
|
__typename: 'Trade',
|
||||||
id: '0',
|
id: '0',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: '2005-04-02T19:37:00.000Z',
|
||||||
price: '10000000',
|
price: '10000000',
|
||||||
size: '50000',
|
size: '50000',
|
||||||
buyOrder: 'buy-order',
|
buyOrder: 'buy-order',
|
||||||
@ -133,9 +133,3 @@ export const generateFill = (
|
|||||||
|
|
||||||
return merge(defaultFill, override);
|
return merge(defaultFill, override);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const makeGetRows =
|
|
||||||
(data: Fills_party_tradesConnection_edges_node[]) =>
|
|
||||||
({ successCallback }: IGetRowsParams) => {
|
|
||||||
successCallback(data, data.length);
|
|
||||||
};
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL fragment: MarketDataFields
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface MarketDataFields_market {
|
|
||||||
__typename: "Market";
|
|
||||||
/**
|
|
||||||
* Market ID
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Current state of the market
|
|
||||||
*/
|
|
||||||
state: MarketState;
|
|
||||||
/**
|
|
||||||
* Current mode of execution of the market
|
|
||||||
*/
|
|
||||||
tradingMode: MarketTradingMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarketDataFields {
|
|
||||||
__typename: "MarketData";
|
|
||||||
/**
|
|
||||||
* market id of the associated mark price
|
|
||||||
*/
|
|
||||||
market: MarketDataFields_market;
|
|
||||||
/**
|
|
||||||
* the highest price level on an order book for buy orders.
|
|
||||||
*/
|
|
||||||
bestBidPrice: string;
|
|
||||||
/**
|
|
||||||
* the lowest price level on an order book for offer orders.
|
|
||||||
*/
|
|
||||||
bestOfferPrice: string;
|
|
||||||
/**
|
|
||||||
* the mark price (actually an unsigned int)
|
|
||||||
*/
|
|
||||||
markPrice: string;
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL subscription operation: MarketDataSub
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface MarketDataSub_marketData_market {
|
|
||||||
__typename: "Market";
|
|
||||||
/**
|
|
||||||
* Market ID
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Current state of the market
|
|
||||||
*/
|
|
||||||
state: MarketState;
|
|
||||||
/**
|
|
||||||
* Current mode of execution of the market
|
|
||||||
*/
|
|
||||||
tradingMode: MarketTradingMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarketDataSub_marketData {
|
|
||||||
__typename: "MarketData";
|
|
||||||
/**
|
|
||||||
* market id of the associated mark price
|
|
||||||
*/
|
|
||||||
market: MarketDataSub_marketData_market;
|
|
||||||
/**
|
|
||||||
* the highest price level on an order book for buy orders.
|
|
||||||
*/
|
|
||||||
bestBidPrice: string;
|
|
||||||
/**
|
|
||||||
* the lowest price level on an order book for offer orders.
|
|
||||||
*/
|
|
||||||
bestOfferPrice: string;
|
|
||||||
/**
|
|
||||||
* the mark price (actually an unsigned int)
|
|
||||||
*/
|
|
||||||
markPrice: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MarketDataSub {
|
|
||||||
/**
|
|
||||||
* Subscribe to the mark price changes
|
|
||||||
*/
|
|
||||||
marketData: MarketDataSub_marketData;
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: Markets
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface Markets_markets_data_market {
|
|
||||||
__typename: "Market";
|
|
||||||
/**
|
|
||||||
* Market ID
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Current state of the market
|
|
||||||
*/
|
|
||||||
state: MarketState;
|
|
||||||
/**
|
|
||||||
* Current mode of execution of the market
|
|
||||||
*/
|
|
||||||
tradingMode: MarketTradingMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets_data {
|
|
||||||
__typename: "MarketData";
|
|
||||||
/**
|
|
||||||
* market id of the associated mark price
|
|
||||||
*/
|
|
||||||
market: Markets_markets_data_market;
|
|
||||||
/**
|
|
||||||
* the highest price level on an order book for buy orders.
|
|
||||||
*/
|
|
||||||
bestBidPrice: string;
|
|
||||||
/**
|
|
||||||
* the lowest price level on an order book for offer orders.
|
|
||||||
*/
|
|
||||||
bestOfferPrice: string;
|
|
||||||
/**
|
|
||||||
* the mark price (actually an unsigned int)
|
|
||||||
*/
|
|
||||||
markPrice: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {
|
|
||||||
__typename: "Asset";
|
|
||||||
/**
|
|
||||||
* The symbol of the asset (e.g: GBP)
|
|
||||||
*/
|
|
||||||
symbol: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets_tradableInstrument_instrument_product {
|
|
||||||
__typename: "Future";
|
|
||||||
/**
|
|
||||||
* The name of the asset (string)
|
|
||||||
*/
|
|
||||||
settlementAsset: Markets_markets_tradableInstrument_instrument_product_settlementAsset;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets_tradableInstrument_instrument {
|
|
||||||
__typename: "Instrument";
|
|
||||||
/**
|
|
||||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
|
||||||
*/
|
|
||||||
code: string;
|
|
||||||
/**
|
|
||||||
* A reference to or instance of a fully specified product, including all required product parameters for that product (Product union)
|
|
||||||
*/
|
|
||||||
product: Markets_markets_tradableInstrument_instrument_product;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets_tradableInstrument {
|
|
||||||
__typename: "TradableInstrument";
|
|
||||||
/**
|
|
||||||
* An instance of or reference to a fully specified instrument.
|
|
||||||
*/
|
|
||||||
instrument: Markets_markets_tradableInstrument_instrument;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets_markets {
|
|
||||||
__typename: "Market";
|
|
||||||
/**
|
|
||||||
* Market ID
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Market full name
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
|
||||||
* number denominated in the currency of the Market. (uint64)
|
|
||||||
*
|
|
||||||
* Examples:
|
|
||||||
* Currency Balance decimalPlaces Real Balance
|
|
||||||
* GBP 100 0 GBP 100
|
|
||||||
* GBP 100 2 GBP 1.00
|
|
||||||
* GBP 100 4 GBP 0.01
|
|
||||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
|
||||||
*
|
|
||||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
|
||||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
|
||||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
|
||||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
|
||||||
*/
|
|
||||||
decimalPlaces: number;
|
|
||||||
/**
|
|
||||||
* marketData for the given market
|
|
||||||
*/
|
|
||||||
data: Markets_markets_data | null;
|
|
||||||
/**
|
|
||||||
* An instance of or reference to a tradable instrument.
|
|
||||||
*/
|
|
||||||
tradableInstrument: Markets_markets_tradableInstrument;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Markets {
|
|
||||||
/**
|
|
||||||
* One or more instruments that are trading on the VEGA network
|
|
||||||
*/
|
|
||||||
markets: Markets_markets[] | null;
|
|
||||||
}
|
|
@ -1,3 +1,2 @@
|
|||||||
export * from './__generated__';
|
|
||||||
export * from './landing';
|
export * from './landing';
|
||||||
export * from './markets-container';
|
export * from './markets-container';
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export * from './MarketDataFields';
|
export * from './MarketDataFields';
|
||||||
export * from './MarketDataSub';
|
export * from './MarketDataSub';
|
||||||
|
export * from './MarketList';
|
||||||
export * from './Markets';
|
export * from './Markets';
|
@ -2,4 +2,4 @@ export * from './market-list-table';
|
|||||||
export * from './markets-container';
|
export * from './markets-container';
|
||||||
export * from './markets-data-provider';
|
export * from './markets-data-provider';
|
||||||
export * from './summary-cell';
|
export * from './summary-cell';
|
||||||
export * from './__generated__/MarketList';
|
export * from './__generated__';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import type { IDatasource, ValueFormatterParams } from 'ag-grid-community';
|
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||||
import {
|
import {
|
||||||
PriceFlashCell,
|
PriceFlashCell,
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
@ -8,95 +8,119 @@ import {
|
|||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
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 {
|
||||||
import type { Markets_markets } from '../__generated__/Markets';
|
AgGridReact,
|
||||||
|
AgGridReactProps,
|
||||||
|
AgReactUiProps,
|
||||||
|
} from 'ag-grid-react';
|
||||||
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
|
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
|
||||||
|
import type {
|
||||||
|
Markets_markets,
|
||||||
|
Markets_markets_data,
|
||||||
|
} from './__generated__/Markets';
|
||||||
|
|
||||||
interface MarketListTableProps {
|
type Props = AgGridReactProps | AgReactUiProps;
|
||||||
datasource: IDatasource;
|
|
||||||
onRowClicked: (marketId: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
type MarketListTableValueFormatterParams = Omit<
|
||||||
({ datasource, onRowClicked }, ref) => {
|
ValueFormatterParams,
|
||||||
return (
|
'data' | 'value'
|
||||||
<AgGrid
|
> & {
|
||||||
style={{ width: '100%', height: '100%' }}
|
data: Markets_markets;
|
||||||
overlayNoRowsTemplate={t('No markets')}
|
};
|
||||||
rowModelType="infinite"
|
|
||||||
datasource={datasource}
|
export const MarketListTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||||
getRowId={({ data }) => data?.id}
|
return (
|
||||||
ref={ref}
|
<AgGrid
|
||||||
defaultColDef={{
|
style={{ width: '100%', height: '100%' }}
|
||||||
flex: 1,
|
overlayNoRowsTemplate={t('No markets')}
|
||||||
resizable: true,
|
getRowId={({ data }) => data?.id}
|
||||||
|
ref={ref}
|
||||||
|
defaultColDef={{
|
||||||
|
flex: 1,
|
||||||
|
resizable: true,
|
||||||
|
}}
|
||||||
|
suppressCellFocus={true}
|
||||||
|
components={{ PriceFlashCell }}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Market')}
|
||||||
|
field="tradableInstrument.instrument.code"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Settlement asset')}
|
||||||
|
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName={t('Trading mode')}
|
||||||
|
field="data"
|
||||||
|
minWidth={200}
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: MarketListTableValueFormatterParams & {
|
||||||
|
value?: Markets_markets_data;
|
||||||
|
}) => {
|
||||||
|
if (!value) return value;
|
||||||
|
const { market, trigger } = value;
|
||||||
|
return market &&
|
||||||
|
market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
||||||
|
trigger &&
|
||||||
|
trigger !== AuctionTrigger.Unspecified
|
||||||
|
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
|
||||||
|
: formatLabel(market?.tradingMode);
|
||||||
}}
|
}}
|
||||||
suppressCellFocus={true}
|
/>
|
||||||
onRowClicked={({ data }: { data: Markets_markets }) =>
|
<AgGridColumn
|
||||||
onRowClicked(data.id)
|
headerName={t('Best bid')}
|
||||||
|
field="data.bestBidPrice"
|
||||||
|
type="rightAligned"
|
||||||
|
cellRenderer="PriceFlashCell"
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: MarketListTableValueFormatterParams & {
|
||||||
|
value?: Markets_markets_data['bestBidPrice'];
|
||||||
|
}) =>
|
||||||
|
value === undefined
|
||||||
|
? value
|
||||||
|
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
||||||
}
|
}
|
||||||
components={{ PriceFlashCell }}
|
/>
|
||||||
>
|
<AgGridColumn
|
||||||
<AgGridColumn
|
headerName={t('Best offer')}
|
||||||
headerName={t('Market')}
|
field="data.bestOfferPrice"
|
||||||
field="tradableInstrument.instrument.code"
|
type="rightAligned"
|
||||||
/>
|
valueFormatter={({
|
||||||
<AgGridColumn
|
value,
|
||||||
headerName={t('Settlement asset')}
|
data,
|
||||||
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
}: MarketListTableValueFormatterParams & {
|
||||||
/>
|
value?: Markets_markets_data['bestOfferPrice'];
|
||||||
<AgGridColumn
|
}) =>
|
||||||
headerName={t('Trading mode')}
|
value === undefined
|
||||||
field="data"
|
? value
|
||||||
minWidth={200}
|
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
}
|
||||||
if (!value) return value;
|
cellRenderer="PriceFlashCell"
|
||||||
const { market, trigger } = value;
|
/>
|
||||||
return market &&
|
<AgGridColumn
|
||||||
market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
headerName={t('Mark price')}
|
||||||
trigger &&
|
field="data.markPrice"
|
||||||
trigger !== AuctionTrigger.Unspecified
|
type="rightAligned"
|
||||||
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
|
cellRenderer="PriceFlashCell"
|
||||||
: formatLabel(market?.tradingMode);
|
valueFormatter={({
|
||||||
}}
|
value,
|
||||||
/>
|
data,
|
||||||
<AgGridColumn
|
}: MarketListTableValueFormatterParams & {
|
||||||
headerName={t('Best bid')}
|
value?: Markets_markets_data['markPrice'];
|
||||||
field="data.bestBidPrice"
|
}) =>
|
||||||
type="rightAligned"
|
value === undefined
|
||||||
cellRenderer="PriceFlashCell"
|
? value
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
||||||
value === undefined
|
}
|
||||||
? value
|
/>
|
||||||
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
<AgGridColumn headerName={t('Description')} field="name" />
|
||||||
}
|
</AgGrid>
|
||||||
/>
|
);
|
||||||
<AgGridColumn
|
});
|
||||||
headerName={t('Best offer')}
|
|
||||||
field="data.bestOfferPrice"
|
|
||||||
type="rightAligned"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
|
||||||
value === undefined
|
|
||||||
? value
|
|
||||||
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
|
||||||
}
|
|
||||||
cellRenderer="PriceFlashCell"
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
headerName={t('Mark price')}
|
|
||||||
field="data.markPrice"
|
|
||||||
type="rightAligned"
|
|
||||||
cellRenderer="PriceFlashCell"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
|
||||||
value === undefined
|
|
||||||
? value
|
|
||||||
: addDecimalsFormatNumber(value, data.decimalPlaces)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<AgGridColumn headerName={t('Description')} field="name" />
|
|
||||||
</AgGrid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export default MarketListTable;
|
export default MarketListTable;
|
||||||
|
@ -8,7 +8,7 @@ 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 './__generated__/Markets';
|
||||||
import { marketsDataProvider as dataProvider } from './markets-data-provider';
|
import { marketsDataProvider as dataProvider } from './markets-data-provider';
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
|
|
||||||
@ -42,13 +42,15 @@ export const MarketsContainer = () => {
|
|||||||
const lastRow = dataRef.current?.length ?? -1;
|
const lastRow = dataRef.current?.length ?? -1;
|
||||||
successCallback(rowsThisBlock, lastRow);
|
successCallback(rowsThisBlock, lastRow);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
<MarketListTable
|
<MarketListTable
|
||||||
|
rowModelType="infinite"
|
||||||
datasource={{ getRows }}
|
datasource={{ getRows }}
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
onRowClicked={(id) => push(`/markets/${id}`)}
|
onRowClicked={({ data }: { data: Markets_markets }) =>
|
||||||
|
push(`/markets/${data.id}`)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
|
@ -3,13 +3,10 @@ import { gql } from '@apollo/client';
|
|||||||
import type {
|
import type {
|
||||||
Markets,
|
Markets,
|
||||||
Markets_markets,
|
Markets_markets,
|
||||||
} from '../../components/__generated__/Markets';
|
|
||||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
MarketDataSub,
|
MarketDataSub,
|
||||||
MarketDataSub_marketData,
|
MarketDataSub_marketData,
|
||||||
} from '../../components/__generated__/MarketDataSub';
|
} from './';
|
||||||
|
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
const MARKET_DATA_FRAGMENT = gql`
|
const MARKET_DATA_FRAGMENT = gql`
|
||||||
fragment MarketDataFields on MarketData {
|
fragment MarketDataFields on MarketData {
|
||||||
|
@ -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, Pagination, Load } from '../lib/generic-data-provider';
|
import type { Subscribe, Load } from '../lib/generic-data-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -48,9 +48,9 @@ export function useDataProvider<Data, Delta>({
|
|||||||
reloadRef.current(force);
|
reloadRef.current(force);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const load = useCallback((pagination: Pagination) => {
|
const load = useCallback<Load<Data>>((...args) => {
|
||||||
if (loadRef.current) {
|
if (loadRef.current) {
|
||||||
return loadRef.current(pagination);
|
return loadRef.current(...args);
|
||||||
}
|
}
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}, []);
|
}, []);
|
||||||
|
445
libs/react-helpers/src/lib/generic-data-provider.spec.ts
Normal file
445
libs/react-helpers/src/lib/generic-data-provider.spec.ts
Normal file
@ -0,0 +1,445 @@
|
|||||||
|
import { makeDataProvider, defaultAppend } from './generic-data-provider';
|
||||||
|
import type {
|
||||||
|
Query,
|
||||||
|
UpdateCallback,
|
||||||
|
Update,
|
||||||
|
PageInfo,
|
||||||
|
} from './generic-data-provider';
|
||||||
|
import type {
|
||||||
|
ApolloClient,
|
||||||
|
FetchResult,
|
||||||
|
SubscriptionOptions,
|
||||||
|
OperationVariables,
|
||||||
|
ApolloQueryResult,
|
||||||
|
QueryOptions,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import type { Subscription, Observable } from 'zen-observable-ts';
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
cursor: string;
|
||||||
|
node: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type Data = Item[];
|
||||||
|
type QueryData = {
|
||||||
|
data: Data;
|
||||||
|
pageInfo?: PageInfo;
|
||||||
|
totalCount?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubscriptionData = QueryData;
|
||||||
|
type Delta = Data;
|
||||||
|
|
||||||
|
describe('data provider', () => {
|
||||||
|
const update = jest.fn<
|
||||||
|
ReturnType<Update<Data, Delta>>,
|
||||||
|
Parameters<Update<Data, Delta>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const callback = jest.fn<
|
||||||
|
ReturnType<UpdateCallback<Data, Delta>>,
|
||||||
|
Parameters<UpdateCallback<Data, Delta>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
const query: Query<QueryData> = {
|
||||||
|
kind: 'Document',
|
||||||
|
definitions: [],
|
||||||
|
};
|
||||||
|
const subscriptionQuery: Query<SubscriptionData> = query;
|
||||||
|
|
||||||
|
const subscribe = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
||||||
|
query,
|
||||||
|
subscriptionQuery,
|
||||||
|
update,
|
||||||
|
(r) => r.data,
|
||||||
|
(r) => r.data
|
||||||
|
);
|
||||||
|
|
||||||
|
const first = 100;
|
||||||
|
const paginatedSubscribe = makeDataProvider<
|
||||||
|
QueryData,
|
||||||
|
Data,
|
||||||
|
SubscriptionData,
|
||||||
|
Delta
|
||||||
|
>(
|
||||||
|
query,
|
||||||
|
subscriptionQuery,
|
||||||
|
update,
|
||||||
|
(r) => r.data,
|
||||||
|
(r) => r.data,
|
||||||
|
{
|
||||||
|
first,
|
||||||
|
append: defaultAppend,
|
||||||
|
getPageInfo: (r) => r?.pageInfo ?? null,
|
||||||
|
getTotalCount: (r) => r?.totalCount,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateData = (start = 0, size = first) => {
|
||||||
|
return new Array(size).fill(null).map((v, i) => ({
|
||||||
|
cursor: (i + start + 1).toString(),
|
||||||
|
node: {
|
||||||
|
id: (i + start + 1).toString(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientSubscribeUnsubscribe = jest.fn();
|
||||||
|
const clientSubscribeSubscribe = jest.fn<
|
||||||
|
Subscription,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[(value: FetchResult<SubscriptionData>) => void, (error: any) => void]
|
||||||
|
>(() => ({
|
||||||
|
unsubscribe: clientSubscribeUnsubscribe,
|
||||||
|
closed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const clientSubscribe = jest.fn<
|
||||||
|
Observable<FetchResult<SubscriptionData>>,
|
||||||
|
[SubscriptionOptions<OperationVariables, SubscriptionData>]
|
||||||
|
>(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
subscribe: clientSubscribeSubscribe,
|
||||||
|
} as unknown as Observable<FetchResult<SubscriptionData>>)
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientQueryPromise: {
|
||||||
|
resolve?: (
|
||||||
|
value:
|
||||||
|
| ApolloQueryResult<QueryData>
|
||||||
|
| PromiseLike<ApolloQueryResult<QueryData>>
|
||||||
|
) => void;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
reject?: (reason?: any) => void;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const clientQuery = jest.fn<
|
||||||
|
Promise<ApolloQueryResult<QueryData>>,
|
||||||
|
[QueryOptions<OperationVariables, QueryData>]
|
||||||
|
>(() => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
clientQueryPromise.resolve = resolve;
|
||||||
|
clientQueryPromise.reject = reject;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
query: clientQuery,
|
||||||
|
subscribe: clientSubscribe,
|
||||||
|
} as unknown as ApolloClient<object>;
|
||||||
|
|
||||||
|
const resolveQuery = async (data: QueryData) => {
|
||||||
|
if (clientQueryPromise.resolve) {
|
||||||
|
await clientQueryPromise.resolve({
|
||||||
|
data,
|
||||||
|
loading: false,
|
||||||
|
networkStatus: 8,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('memoize instance and unsubscribe if no subscribers', () => {
|
||||||
|
const subscription1 = subscribe(jest.fn(), client);
|
||||||
|
const subscription2 = subscribe(jest.fn(), client);
|
||||||
|
expect(clientSubscribeSubscribe.mock.calls.length).toEqual(1);
|
||||||
|
subscription1.unsubscribe();
|
||||||
|
expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(0);
|
||||||
|
subscription2.unsubscribe();
|
||||||
|
expect(clientSubscribeUnsubscribe.mock.calls.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls callback before and after initial fetch', async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
expect(callback.mock.calls.length).toBe(1);
|
||||||
|
expect(callback.mock.calls[0][0].data).toBe(null);
|
||||||
|
expect(callback.mock.calls[0][0].loading).toBe(true);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
expect(callback.mock.calls.length).toBe(2);
|
||||||
|
expect(callback.mock.calls[1][0].data).toBe(data);
|
||||||
|
expect(callback.mock.calls[1][0].loading).toBe(false);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls update and callback on each update', async () => {
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
const delta: Item[] = [];
|
||||||
|
update.mockImplementationOnce((data, delta) => [...data, ...delta]);
|
||||||
|
// calling onNext from client.subscribe({ query }).subscribe(onNext)
|
||||||
|
await clientSubscribeSubscribe.mock.calls[
|
||||||
|
clientSubscribeSubscribe.mock.calls.length - 1
|
||||||
|
][0]({ data: { data: delta } });
|
||||||
|
expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data);
|
||||||
|
expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta);
|
||||||
|
expect(callback.mock.calls[callback.mock.calls.length - 1][0].delta).toBe(
|
||||||
|
delta
|
||||||
|
);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("don't calls callback on update if data doesn't", async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
const delta: Item[] = [];
|
||||||
|
update.mockImplementationOnce((data, delta) => data);
|
||||||
|
const callbackCallsLength = callback.mock.calls.length;
|
||||||
|
// calling onNext from client.subscribe({ query }).subscribe(onNext)
|
||||||
|
await clientSubscribeSubscribe.mock.calls[
|
||||||
|
clientSubscribeSubscribe.mock.calls.length - 1
|
||||||
|
][0]({ data: { data: delta } });
|
||||||
|
expect(update.mock.calls[update.mock.calls.length - 1][0]).toBe(data);
|
||||||
|
expect(update.mock.calls[update.mock.calls.length - 1][1]).toBe(delta);
|
||||||
|
expect(callback.mock.calls.length).toBe(callbackCallsLength);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refetch data on reload', async () => {
|
||||||
|
clientQuery.mockClear();
|
||||||
|
clientSubscribeUnsubscribe.mockClear();
|
||||||
|
clientSubscribeSubscribe.mockClear();
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
subscription.reload();
|
||||||
|
await resolveQuery({ data });
|
||||||
|
expect(clientQuery.mock.calls.length).toBe(2);
|
||||||
|
expect(clientSubscribeSubscribe.mock.calls.length).toBe(1);
|
||||||
|
expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(0);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refetch data and restart subscription on reload with force', async () => {
|
||||||
|
clientQuery.mockClear();
|
||||||
|
clientSubscribeUnsubscribe.mockClear();
|
||||||
|
clientSubscribeSubscribe.mockClear();
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
subscription.reload(true);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
expect(clientQuery.mock.calls.length).toBe(2);
|
||||||
|
expect(clientSubscribeSubscribe.mock.calls.length).toBe(2);
|
||||||
|
expect(clientSubscribeUnsubscribe.mock.calls.length).toBe(1);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls callback on flush', async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const data: Item[] = [];
|
||||||
|
const subscription = subscribe(callback, client);
|
||||||
|
await resolveQuery({ data });
|
||||||
|
const callbackCallsLength = callback.mock.calls.length;
|
||||||
|
subscription.flush();
|
||||||
|
expect(callback.mock.calls.length).toBe(callbackCallsLength + 1);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fills data with nulls if paginaton is enabled', async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const totalCount = 1000;
|
||||||
|
const data: Item[] = new Array(first).fill(null).map((v, i) => ({
|
||||||
|
cursor: i.toString(),
|
||||||
|
node: {
|
||||||
|
id: i.toString(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const subscription = paginatedSubscribe(callback, client);
|
||||||
|
await resolveQuery({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(callback.mock.calls[1][0].data?.length).toBe(totalCount);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads requested data blocks and inserts data with total count', async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const totalCount = 1000;
|
||||||
|
const subscription = paginatedSubscribe(callback, client);
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(),
|
||||||
|
totalCount,
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '100',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// load next page
|
||||||
|
subscription.load();
|
||||||
|
let lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '100',
|
||||||
|
first,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(100),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '200',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// load page with skip
|
||||||
|
subscription.load(500, 600);
|
||||||
|
lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '200',
|
||||||
|
first,
|
||||||
|
skip: 300,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(500),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// load in the gap
|
||||||
|
subscription.load(400, 500);
|
||||||
|
lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '200',
|
||||||
|
first,
|
||||||
|
skip: 200,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(400),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// load page after last block
|
||||||
|
subscription.load(700, 800);
|
||||||
|
lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '600',
|
||||||
|
first,
|
||||||
|
skip: 100,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(700),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '800',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// load last page shorter than expected
|
||||||
|
subscription.load(950, 1050);
|
||||||
|
lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '800',
|
||||||
|
first,
|
||||||
|
skip: 150,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(950, 20),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
endCursor: '970',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(970);
|
||||||
|
|
||||||
|
// load next page when pageInfo.hasNextPage === false
|
||||||
|
const clientQueryCallsLength = clientQuery.mock.calls.length;
|
||||||
|
subscription.load();
|
||||||
|
expect(clientQuery.mock.calls.length).toBe(clientQueryCallsLength);
|
||||||
|
|
||||||
|
// load last page longer than expected
|
||||||
|
subscription.load(960, 1000);
|
||||||
|
lastQueryArgs =
|
||||||
|
clientQuery.mock.calls[clientQuery.mock.calls.length - 1][0];
|
||||||
|
expect(lastQueryArgs?.variables?.pagination).toEqual({
|
||||||
|
after: '960',
|
||||||
|
first,
|
||||||
|
});
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(960, 40),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '1000',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(1000);
|
||||||
|
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads requested data blocks and inserts data without totalCount', async () => {
|
||||||
|
callback.mockClear();
|
||||||
|
const totalCount = undefined;
|
||||||
|
const subscription = paginatedSubscribe(callback, client);
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(),
|
||||||
|
totalCount,
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '100',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
|
||||||
|
|
||||||
|
// load next page
|
||||||
|
subscription.load();
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(100),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: true,
|
||||||
|
endCursor: '200',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(undefined);
|
||||||
|
|
||||||
|
// load last page
|
||||||
|
subscription.load();
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(200, 50),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
endCursor: '250',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lastCallbackArgs = callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(250);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets total count when first page has no next page', async () => {
|
||||||
|
const subscription = paginatedSubscribe(callback, client);
|
||||||
|
await resolveQuery({
|
||||||
|
data: generateData(),
|
||||||
|
pageInfo: {
|
||||||
|
hasNextPage: false,
|
||||||
|
endCursor: '100',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const lastCallbackArgs =
|
||||||
|
callback.mock.calls[callback.mock.calls.length - 1];
|
||||||
|
expect(lastCallbackArgs[0].totalCount).toBe(100);
|
||||||
|
subscription.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
@ -7,6 +7,7 @@ import type {
|
|||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
import type { Subscription } from 'zen-observable-ts';
|
import type { Subscription } from 'zen-observable-ts';
|
||||||
import isEqual from 'lodash/isEqual';
|
import isEqual from 'lodash/isEqual';
|
||||||
|
import type { Pagination as PaginationWithoutSkip } from '@vegaprotocol/types';
|
||||||
|
|
||||||
export interface UpdateCallback<Data, Delta> {
|
export interface UpdateCallback<Data, Delta> {
|
||||||
(arg: {
|
(arg: {
|
||||||
@ -21,15 +22,12 @@ export interface UpdateCallback<Data, Delta> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Load<Data> {
|
export interface Load<Data> {
|
||||||
(pagination: Pagination): Promise<Data | null>;
|
(start?: number, end?: number): Promise<Data | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Pagination {
|
type Pagination = PaginationWithoutSkip & {
|
||||||
first?: number;
|
skip?: number;
|
||||||
after?: string;
|
};
|
||||||
last?: number;
|
|
||||||
before?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageInfo {
|
export interface PageInfo {
|
||||||
startCursor?: string;
|
startCursor?: string;
|
||||||
@ -51,7 +49,7 @@ export interface Subscribe<Data, Delta> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
export type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
||||||
|
|
||||||
export interface Update<Data, Delta> {
|
export interface Update<Data, Delta> {
|
||||||
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
|
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
|
||||||
@ -60,13 +58,13 @@ export interface Update<Data, Delta> {
|
|||||||
export interface Append<Data> {
|
export interface Append<Data> {
|
||||||
(
|
(
|
||||||
data: Data | null,
|
data: Data | null,
|
||||||
pageInfo: PageInfo,
|
|
||||||
insertionData: Data | null,
|
insertionData: Data | null,
|
||||||
insertionPageInfo: PageInfo | null,
|
insertionPageInfo: PageInfo | null,
|
||||||
pagination?: Pagination
|
pagination?: Pagination,
|
||||||
|
totalCount?: number
|
||||||
): {
|
): {
|
||||||
data: Data | null;
|
data: Data | null;
|
||||||
pageInfo: PageInfo;
|
totalCount?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +84,46 @@ interface GetDelta<SubscriptionData, Delta> {
|
|||||||
(subscriptionData: SubscriptionData): Delta;
|
(subscriptionData: SubscriptionData): Delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function defaultAppend<Data>(
|
||||||
|
data: Data | null,
|
||||||
|
insertionData: Data | null,
|
||||||
|
insertionPageInfo: PageInfo | null,
|
||||||
|
pagination?: Pagination,
|
||||||
|
totalCount?: number
|
||||||
|
) {
|
||||||
|
if (data && insertionData && insertionPageInfo) {
|
||||||
|
if (!(data instanceof Array) || !(insertionData instanceof Array)) {
|
||||||
|
throw new Error(
|
||||||
|
'data needs to be instance of { cursor: string }[] when using pagination'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (pagination?.after) {
|
||||||
|
const cursors = data.map((item) => item && item.cursor);
|
||||||
|
const startIndex = cursors.lastIndexOf(pagination.after);
|
||||||
|
if (startIndex !== -1) {
|
||||||
|
const start = startIndex + 1 + (pagination.skip ?? 0);
|
||||||
|
const end = start + insertionData.length;
|
||||||
|
let updatedData = [
|
||||||
|
...data.slice(0, start),
|
||||||
|
...insertionData,
|
||||||
|
...data.slice(end),
|
||||||
|
];
|
||||||
|
if (!insertionPageInfo.hasNextPage && end !== (totalCount ?? 0)) {
|
||||||
|
// adjust totalCount if last page is shorter or longer than expected
|
||||||
|
totalCount = end;
|
||||||
|
updatedData = updatedData.slice(0, end);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: updatedData,
|
||||||
|
// increase totalCount if last page is longer than expected
|
||||||
|
totalCount: totalCount && Math.max(updatedData.length, totalCount),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { data, totalCount };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param subscriptionQuery query that will be used for subscription
|
* @param subscriptionQuery query that will be used for subscription
|
||||||
* @param update function that will be execued on each onNext, it should update data base on delta, it can reload data provider
|
* @param update function that will be execued on each onNext, it should update data base on delta, it can reload data provider
|
||||||
@ -102,7 +140,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
getDelta: GetDelta<SubscriptionData, Delta>,
|
getDelta: GetDelta<SubscriptionData, Delta>,
|
||||||
pagination?: {
|
pagination?: {
|
||||||
getPageInfo: GetPageInfo<QueryData>;
|
getPageInfo: GetPageInfo<QueryData>;
|
||||||
getTotalCount: GetTotalCount<QueryData>;
|
getTotalCount?: GetTotalCount<QueryData>;
|
||||||
append: Append<Data>;
|
append: Append<Data>;
|
||||||
first: number;
|
first: number;
|
||||||
},
|
},
|
||||||
@ -125,7 +163,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
// 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 = (
|
const notify = (
|
||||||
callback: UpdateCallback<Data, Delta>,
|
callback: UpdateCallback<Data, Delta>,
|
||||||
dataUpdate?: { delta?: Delta; insertionData?: Data | null }
|
updateData?: { delta?: Delta; insertionData?: Data | null }
|
||||||
) => {
|
) => {
|
||||||
callback({
|
callback({
|
||||||
data,
|
data,
|
||||||
@ -133,26 +171,50 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
loading,
|
loading,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
totalCount,
|
totalCount,
|
||||||
...dataUpdate,
|
...updateData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// notify all callbacks
|
// notify all callbacks
|
||||||
const notifyAll = (dataUpdate?: {
|
const notifyAll = (updateData?: {
|
||||||
delta?: Delta;
|
delta?: Delta;
|
||||||
insertionData?: Data | null;
|
insertionData?: Data | null;
|
||||||
}) => {
|
}) => {
|
||||||
callbacks.forEach((callback) => notify(callback, dataUpdate));
|
callbacks.forEach((callback) => notify(callback, updateData));
|
||||||
};
|
};
|
||||||
|
|
||||||
const load = async (params?: Pagination) => {
|
const load = async (start?: number, end?: number) => {
|
||||||
if (!client || !pagination || !pageInfo) {
|
if (!client || !pagination || !pageInfo || !(data instanceof Array)) {
|
||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
const paginationVariables: Pagination = params ?? {
|
const paginationVariables: Pagination = {
|
||||||
first: pagination.first,
|
first: pagination.first,
|
||||||
after: pageInfo.endCursor,
|
after: pageInfo.endCursor,
|
||||||
};
|
};
|
||||||
|
if (start !== undefined) {
|
||||||
|
if (!start) {
|
||||||
|
paginationVariables.after = undefined;
|
||||||
|
} else if (data && data[start - 1]) {
|
||||||
|
paginationVariables.after = (
|
||||||
|
data[start - 1] as { cursor: string }
|
||||||
|
).cursor;
|
||||||
|
} else {
|
||||||
|
let skip = 1;
|
||||||
|
while (!data[start - 1 - skip] && skip <= start) {
|
||||||
|
skip += 1;
|
||||||
|
}
|
||||||
|
paginationVariables.skip = skip;
|
||||||
|
if (skip === start) {
|
||||||
|
paginationVariables.after = undefined;
|
||||||
|
} else {
|
||||||
|
paginationVariables.after = (
|
||||||
|
data[start - 1 - skip] as { cursor: string }
|
||||||
|
).cursor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!pageInfo.hasNextPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const res = await client.query<QueryData>({
|
const res = await client.query<QueryData>({
|
||||||
query,
|
query,
|
||||||
variables: {
|
variables: {
|
||||||
@ -162,15 +224,18 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
});
|
});
|
||||||
const insertionData = getData(res.data);
|
const insertionData = getData(res.data);
|
||||||
const insertionDataPageInfo = pagination.getPageInfo(res.data);
|
const insertionPageInfo = pagination.getPageInfo(res.data);
|
||||||
({ data, pageInfo } = pagination.append(
|
({ data, totalCount } = pagination.append(
|
||||||
data,
|
data,
|
||||||
pageInfo,
|
|
||||||
insertionData,
|
insertionData,
|
||||||
insertionDataPageInfo,
|
insertionPageInfo,
|
||||||
paginationVariables
|
paginationVariables,
|
||||||
|
totalCount
|
||||||
));
|
));
|
||||||
totalCount = pagination.getTotalCount(res.data);
|
pageInfo = insertionPageInfo;
|
||||||
|
totalCount =
|
||||||
|
(pagination.getTotalCount && pagination.getTotalCount(res.data)) ??
|
||||||
|
totalCount;
|
||||||
notifyAll({ insertionData });
|
notifyAll({ insertionData });
|
||||||
return insertionData;
|
return insertionData;
|
||||||
};
|
};
|
||||||
@ -188,9 +253,23 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
});
|
});
|
||||||
data = getData(res.data);
|
data = getData(res.data);
|
||||||
if (pagination) {
|
if (data && pagination) {
|
||||||
|
if (!(data instanceof Array)) {
|
||||||
|
throw new Error(
|
||||||
|
'data needs to be instance of { cursor: string }[] when using pagination'
|
||||||
|
);
|
||||||
|
}
|
||||||
pageInfo = pagination.getPageInfo(res.data);
|
pageInfo = pagination.getPageInfo(res.data);
|
||||||
totalCount = pagination.getTotalCount(res.data);
|
if (pageInfo && !pageInfo.hasNextPage) {
|
||||||
|
totalCount = data.length;
|
||||||
|
} else {
|
||||||
|
totalCount =
|
||||||
|
pagination.getTotalCount && pagination.getTotalCount(res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && totalCount && data.length < totalCount) {
|
||||||
|
data.push(...new Array(totalCount - data.length).fill(null));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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) {
|
||||||
@ -255,11 +334,11 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
updateQueue.push(delta);
|
updateQueue.push(delta);
|
||||||
} else {
|
} else {
|
||||||
const newData = update(data, delta, reload);
|
const updatedData = update(data, delta, reload);
|
||||||
if (newData === data) {
|
if (updatedData === data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = newData;
|
data = updatedData;
|
||||||
notifyAll({ delta });
|
notifyAll({ delta });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -361,7 +440,7 @@ export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
getDelta: GetDelta<SubscriptionData, Delta>,
|
getDelta: GetDelta<SubscriptionData, Delta>,
|
||||||
pagination?: {
|
pagination?: {
|
||||||
getPageInfo: GetPageInfo<QueryData>;
|
getPageInfo: GetPageInfo<QueryData>;
|
||||||
getTotalCount: GetTotalCount<QueryData>;
|
getTotalCount?: GetTotalCount<QueryData>;
|
||||||
append: Append<Data>;
|
append: Append<Data>;
|
||||||
first: number;
|
first: number;
|
||||||
},
|
},
|
||||||
|
41
libs/trades/src/lib/__generated__/Trades.ts
generated
41
libs/trades/src/lib/__generated__/Trades.ts
generated
@ -3,11 +3,13 @@
|
|||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { Pagination } from "@vegaprotocol/types";
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: Trades
|
// GraphQL query operation: Trades
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface Trades_market_trades_market {
|
export interface Trades_market_tradesConnection_edges_node_market {
|
||||||
__typename: "Market";
|
__typename: "Market";
|
||||||
/**
|
/**
|
||||||
* Market ID
|
* Market ID
|
||||||
@ -38,7 +40,7 @@ export interface Trades_market_trades_market {
|
|||||||
positionDecimalPlaces: number;
|
positionDecimalPlaces: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trades_market_trades {
|
export interface Trades_market_tradesConnection_edges_node {
|
||||||
__typename: "Trade";
|
__typename: "Trade";
|
||||||
/**
|
/**
|
||||||
* The hash of the trade data
|
* The hash of the trade data
|
||||||
@ -59,7 +61,33 @@ export interface Trades_market_trades {
|
|||||||
/**
|
/**
|
||||||
* The market the trade occurred on
|
* The market the trade occurred on
|
||||||
*/
|
*/
|
||||||
market: Trades_market_trades_market;
|
market: Trades_market_tradesConnection_edges_node_market;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trades_market_tradesConnection_edges {
|
||||||
|
__typename: "TradeEdge";
|
||||||
|
node: Trades_market_tradesConnection_edges_node;
|
||||||
|
cursor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trades_market_tradesConnection_pageInfo {
|
||||||
|
__typename: "PageInfo";
|
||||||
|
startCursor: string;
|
||||||
|
endCursor: string;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPreviousPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Trades_market_tradesConnection {
|
||||||
|
__typename: "TradeConnection";
|
||||||
|
/**
|
||||||
|
* The trade in this connection
|
||||||
|
*/
|
||||||
|
edges: Trades_market_tradesConnection_edges[];
|
||||||
|
/**
|
||||||
|
* The pagination information
|
||||||
|
*/
|
||||||
|
pageInfo: Trades_market_tradesConnection_pageInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trades_market {
|
export interface Trades_market {
|
||||||
@ -68,10 +96,7 @@ export interface Trades_market {
|
|||||||
* Market ID
|
* Market ID
|
||||||
*/
|
*/
|
||||||
id: string;
|
id: string;
|
||||||
/**
|
tradesConnection: Trades_market_tradesConnection;
|
||||||
* Trades on a market
|
|
||||||
*/
|
|
||||||
trades: Trades_market_trades[] | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trades {
|
export interface Trades {
|
||||||
@ -83,5 +108,5 @@ export interface Trades {
|
|||||||
|
|
||||||
export interface TradesVariables {
|
export interface TradesVariables {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
maxTrades: number;
|
pagination?: Pagination | null;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
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 { GridApi } from 'ag-grid-community';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import type {
|
||||||
|
IGetRowsParams,
|
||||||
|
BodyScrollEvent,
|
||||||
|
BodyScrollEndEvent,
|
||||||
|
} from 'ag-grid-community';
|
||||||
import {
|
import {
|
||||||
MAX_TRADES,
|
MAX_TRADES,
|
||||||
sortTrades,
|
|
||||||
tradesDataProvider as dataProvider,
|
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';
|
||||||
import type { TradesVariables } from './__generated__/Trades';
|
import type {
|
||||||
|
TradesVariables,
|
||||||
|
Trades_market_tradesConnection_edges,
|
||||||
|
} from './__generated__/Trades';
|
||||||
|
|
||||||
interface TradesContainerProps {
|
interface TradesContainerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
@ -18,51 +24,132 @@ interface TradesContainerProps {
|
|||||||
|
|
||||||
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const dataRef = useRef<
|
||||||
|
(Trades_market_tradesConnection_edges | null)[] | null
|
||||||
|
>(null);
|
||||||
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
|
||||||
const variables = useMemo<TradesVariables>(
|
const variables = useMemo<TradesVariables>(
|
||||||
() => ({ marketId, maxTrades: MAX_TRADES }),
|
() => ({ marketId, maxTrades: MAX_TRADES }),
|
||||||
[marketId]
|
[marketId]
|
||||||
);
|
);
|
||||||
const update = useCallback(({ delta }: { delta: TradeFields[] }) => {
|
|
||||||
if (!gridRef.current?.api) {
|
const addNewRows = useCallback(() => {
|
||||||
return false;
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
const incoming = sortTrades(delta);
|
totalCountRef.current += newRows.current;
|
||||||
const currentRows = getAllRows(gridRef.current.api);
|
}
|
||||||
// Create array of trades whose index is now greater than the max so we
|
newRows.current = 0;
|
||||||
// can remove them from the grid
|
if (!gridRef.current?.api) {
|
||||||
const outgoing = [...incoming, ...currentRows].filter(
|
return;
|
||||||
(r, i) => i > MAX_TRADES - 1
|
}
|
||||||
);
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
|
||||||
gridRef.current.api.applyTransactionAsync({
|
|
||||||
add: incoming,
|
|
||||||
remove: outgoing,
|
|
||||||
addIndex: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, []);
|
}, []);
|
||||||
const { data, error, loading } = useDataProvider({
|
|
||||||
|
const update = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: (Trades_market_tradesConnection_edges | null)[];
|
||||||
|
delta: TradeFields[];
|
||||||
|
}) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataRef.current = data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
data: (Trades_market_tradesConnection_edges | null)[];
|
||||||
|
totalCount?: number;
|
||||||
|
}) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading, load, totalCount } = useDataProvider({
|
||||||
dataProvider,
|
dataProvider,
|
||||||
update,
|
update,
|
||||||
|
insert,
|
||||||
variables,
|
variables,
|
||||||
});
|
});
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const getRows = async ({
|
||||||
|
successCallback,
|
||||||
|
failCallback,
|
||||||
|
startRow,
|
||||||
|
endRow,
|
||||||
|
}: IGetRowsParams) => {
|
||||||
|
startRow += newRows.current;
|
||||||
|
endRow += newRows.current;
|
||||||
|
try {
|
||||||
|
if (dataRef.current && dataRef.current.indexOf(null) < endRow) {
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} else if (rowsThisBlock.length < endRow - startRow) {
|
||||||
|
lastRow = rowsThisBlock.length;
|
||||||
|
}
|
||||||
|
successCallback(rowsThisBlock, lastRow);
|
||||||
|
} catch (e) {
|
||||||
|
failCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
loading={loading}
|
<TradesTable
|
||||||
error={error}
|
ref={gridRef}
|
||||||
data={data}
|
rowModelType="infinite"
|
||||||
render={(data) => <TradesTable ref={gridRef} data={data} />}
|
datasource={{ getRows }}
|
||||||
/>
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllRows = (api: GridApi) => {
|
|
||||||
const rows: TradeFields[] = [];
|
|
||||||
api.forEachNode((node) => {
|
|
||||||
rows.push(node.data);
|
|
||||||
});
|
|
||||||
return rows;
|
|
||||||
};
|
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
import {
|
||||||
|
makeDataProvider,
|
||||||
|
defaultAppend as append,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type { PageInfo } from '@vegaprotocol/react-helpers';
|
||||||
import type { TradeFields } from './__generated__/TradeFields';
|
import type { TradeFields } from './__generated__/TradeFields';
|
||||||
import type { Trades } from './__generated__/Trades';
|
import type {
|
||||||
|
Trades,
|
||||||
|
Trades_market_tradesConnection_edges,
|
||||||
|
Trades_market_tradesConnection_edges_node,
|
||||||
|
} from './__generated__/Trades';
|
||||||
import type { TradesSub } from './__generated__/TradesSub';
|
import type { TradesSub } from './__generated__/TradesSub';
|
||||||
import orderBy from 'lodash/orderBy';
|
import orderBy from 'lodash/orderBy';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
@ -24,11 +32,22 @@ const TRADES_FRAGMENT = gql`
|
|||||||
|
|
||||||
export const TRADES_QUERY = gql`
|
export const TRADES_QUERY = gql`
|
||||||
${TRADES_FRAGMENT}
|
${TRADES_FRAGMENT}
|
||||||
query Trades($marketId: ID!, $maxTrades: Int!) {
|
query Trades($marketId: ID!, $pagination: Pagination) {
|
||||||
market(id: $marketId) {
|
market(id: $marketId) {
|
||||||
id
|
id
|
||||||
trades(last: $maxTrades) {
|
tradesConnection(pagination: $pagination) {
|
||||||
...TradeFields
|
edges {
|
||||||
|
node {
|
||||||
|
...TradeFields
|
||||||
|
}
|
||||||
|
cursor
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
hasNextPage
|
||||||
|
hasPreviousPage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,40 +62,50 @@ export const TRADES_SUB = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const sortTrades = (trades: TradeFields[]) => {
|
const update = (
|
||||||
return orderBy(
|
data: (Trades_market_tradesConnection_edges | null)[],
|
||||||
trades,
|
delta: TradeFields[]
|
||||||
(t) => {
|
) => {
|
||||||
return new Date(t.createdAt).getTime();
|
|
||||||
},
|
|
||||||
'desc'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const update = (data: TradeFields[], delta: TradeFields[]) => {
|
|
||||||
return produce(data, (draft) => {
|
return produce(data, (draft) => {
|
||||||
const incoming = sortTrades(delta);
|
orderBy(delta, 'createdAt', 'desc').forEach((node) => {
|
||||||
|
const index = draft.findIndex((edge) => edge?.node.id === node.id);
|
||||||
// Add new trades to the top
|
if (index !== -1) {
|
||||||
draft.unshift(...incoming);
|
if (draft[index]?.node) {
|
||||||
|
Object.assign(
|
||||||
// Remove old trades from the bottom
|
draft[index]?.node as Trades_market_tradesConnection_edges_node,
|
||||||
if (draft.length > MAX_TRADES) {
|
node
|
||||||
draft.splice(MAX_TRADES, draft.length - MAX_TRADES);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const firstNode = draft[0]?.node;
|
||||||
|
if (firstNode && node.createdAt >= firstNode.createdAt) {
|
||||||
|
draft.unshift({ node, cursor: '', __typename: 'TradeEdge' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getData = (responseData: Trades): TradeFields[] | null =>
|
const getData = (
|
||||||
responseData.market ? responseData.market.trades : null;
|
responseData: Trades
|
||||||
|
): Trades_market_tradesConnection_edges[] | null =>
|
||||||
|
responseData.market ? responseData.market.tradesConnection.edges : null;
|
||||||
|
|
||||||
const getDelta = (subscriptionData: TradesSub): TradeFields[] =>
|
const getDelta = (subscriptionData: TradesSub): TradeFields[] =>
|
||||||
subscriptionData?.trades || [];
|
subscriptionData?.trades || [];
|
||||||
|
|
||||||
|
const getPageInfo = (responseData: Trades): PageInfo | null =>
|
||||||
|
responseData.market?.tradesConnection.pageInfo || null;
|
||||||
|
|
||||||
export const tradesDataProvider = makeDataProvider(
|
export const tradesDataProvider = makeDataProvider(
|
||||||
TRADES_QUERY,
|
TRADES_QUERY,
|
||||||
TRADES_SUB,
|
TRADES_SUB,
|
||||||
update,
|
update,
|
||||||
getData,
|
getData,
|
||||||
getDelta
|
getDelta,
|
||||||
|
{
|
||||||
|
getPageInfo,
|
||||||
|
append,
|
||||||
|
first: 100,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
@ -19,7 +19,7 @@ const trade: TradeFields = {
|
|||||||
|
|
||||||
it('Correct columns are rendered', async () => {
|
it('Correct columns are rendered', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<TradesTable data={[trade]} />);
|
render(<TradesTable rowData={[trade]} />);
|
||||||
});
|
});
|
||||||
const expectedHeaders = ['Price', 'Size', 'Created at'];
|
const expectedHeaders = ['Price', 'Size', 'Created at'];
|
||||||
const headers = screen.getAllByRole('columnheader');
|
const headers = screen.getAllByRole('columnheader');
|
||||||
@ -29,7 +29,7 @@ it('Correct columns are rendered', async () => {
|
|||||||
|
|
||||||
it('Number and data columns are formatted', async () => {
|
it('Number and data columns are formatted', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<TradesTable data={[trade]} />);
|
render(<TradesTable rowData={[trade]} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
@ -51,7 +51,7 @@ it('Price and size columns are formatted', async () => {
|
|||||||
size: (Number(trade.size) - 10).toString(),
|
size: (Number(trade.size) - 10).toString(),
|
||||||
};
|
};
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<TradesTable data={[trade2, trade]} />);
|
render(<TradesTable rowData={[trade2, trade]} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
import { AgGridColumn } from 'ag-grid-react';
|
||||||
import { forwardRef, useMemo } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { TradeFields } from './__generated__/TradeFields';
|
|
||||||
import {
|
import {
|
||||||
addDecimal,
|
addDecimal,
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
@ -10,8 +9,9 @@ import {
|
|||||||
t,
|
t,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community';
|
import type { CellClassParams, ValueFormatterParams } from 'ag-grid-community';
|
||||||
|
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
|
||||||
|
import type { Trades_market_tradesConnection_edges_node } from './__generated__/Trades';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { sortTrades } from './trades-data-provider';
|
|
||||||
|
|
||||||
export const UP_CLASS = 'text-vega-green';
|
export const UP_CLASS = 'text-vega-green';
|
||||||
export const DOWN_CLASS = 'text-vega-red';
|
export const DOWN_CLASS = 'text-vega-red';
|
||||||
@ -37,58 +37,72 @@ const changeCellClass =
|
|||||||
return ['font-mono', colorClass].join(' ');
|
return ['font-mono', colorClass].join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TradesTableProps {
|
type Props = AgGridReactProps | AgReactUiProps;
|
||||||
data: TradeFields[] | null;
|
type TradesTableValueFormatterParams = Omit<
|
||||||
}
|
ValueFormatterParams,
|
||||||
|
'data' | 'value'
|
||||||
|
> & {
|
||||||
|
data: Trades_market_tradesConnection_edges_node | null;
|
||||||
|
};
|
||||||
|
|
||||||
export const TradesTable = forwardRef<AgGridReact, TradesTableProps>(
|
export const TradesTable = forwardRef<AgGridReact, Props>((props, ref) => {
|
||||||
({ data }, ref) => {
|
return (
|
||||||
// Sort initial trades
|
<AgGrid
|
||||||
const trades = useMemo(() => {
|
style={{ width: '100%', height: '100%' }}
|
||||||
if (!data) {
|
overlayNoRowsTemplate={t('No trades')}
|
||||||
return null;
|
getRowId={({ data }) => data.id}
|
||||||
}
|
ref={ref}
|
||||||
return sortTrades(data);
|
defaultColDef={{
|
||||||
}, [data]);
|
resizable: true,
|
||||||
|
}}
|
||||||
return (
|
{...props}
|
||||||
<AgGrid
|
>
|
||||||
style={{ width: '100%', height: '100%' }}
|
<AgGridColumn
|
||||||
overlayNoRowsTemplate={t('No trades')}
|
headerName={t('Price')}
|
||||||
rowData={trades}
|
field="price"
|
||||||
getRowId={({ data }) => data.id}
|
width={130}
|
||||||
ref={ref}
|
cellClass={changeCellClass('price')}
|
||||||
defaultColDef={{
|
valueFormatter={({
|
||||||
resizable: true,
|
value,
|
||||||
|
data,
|
||||||
|
}: TradesTableValueFormatterParams & {
|
||||||
|
value: Trades_market_tradesConnection_edges_node['price'];
|
||||||
|
}) => {
|
||||||
|
if (!data?.market) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Price')}
|
headerName={t('Size')}
|
||||||
field="price"
|
field="size"
|
||||||
width={130}
|
width={125}
|
||||||
cellClass={changeCellClass('price')}
|
valueFormatter={({
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
value,
|
||||||
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
data,
|
||||||
}}
|
}: TradesTableValueFormatterParams & {
|
||||||
/>
|
value: Trades_market_tradesConnection_edges_node['size'];
|
||||||
<AgGridColumn
|
}) => {
|
||||||
headerName={t('Size')}
|
if (!data?.market) {
|
||||||
field="size"
|
return null;
|
||||||
width={125}
|
}
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
return addDecimal(value, data.market.positionDecimalPlaces);
|
||||||
return addDecimal(value, data.market.positionDecimalPlaces);
|
}}
|
||||||
}}
|
cellClass={changeCellClass('size')}
|
||||||
cellClass={changeCellClass('size')}
|
/>
|
||||||
/>
|
<AgGridColumn
|
||||||
<AgGridColumn
|
headerName={t('Created at')}
|
||||||
headerName={t('Created at')}
|
field="createdAt"
|
||||||
field="createdAt"
|
width={170}
|
||||||
width={170}
|
valueFormatter={({
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
value,
|
||||||
return getDateTimeFormat().format(new Date(value));
|
}: TradesTableValueFormatterParams & {
|
||||||
}}
|
value: Trades_market_tradesConnection_edges_node['createdAt'];
|
||||||
/>
|
}) => {
|
||||||
</AgGrid>
|
return value && getDateTimeFormat().format(new Date(value));
|
||||||
);
|
}}
|
||||||
}
|
/>
|
||||||
);
|
</AgGrid>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"start": "nx serve",
|
"start": "nx serve",
|
||||||
"build": "nx build",
|
"build": "nx build",
|
||||||
"test": "nx test",
|
"test": "nx test",
|
||||||
"postinstall": "husky install && yarn tsc -b tools/executors/**"
|
"postinstall": "husky install && yarn tsc -b tools/executors/next && yarn tsc -b tools/executors/webpack"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.14.0"
|
"node": ">=16.14.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user