feat(trading): show full history of orders, fills and trades (#5105)

This commit is contained in:
Bartłomiej Głownia 2023-10-27 15:21:26 +02:00 committed by GitHub
parent fbafc726a4
commit c440abc77d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 83 deletions

View File

@ -3,7 +3,12 @@ import throttle from 'lodash/throttle';
import isEqualWith from 'lodash/isEqualWith';
import { useApolloClient } from '@apollo/client';
import type { OperationVariables } from '@apollo/client';
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
import type {
Subscribe,
Load,
UpdateCallback,
PageInfo,
} from './generic-data-provider';
import { variablesIsEqualCustomizer } from './generic-data-provider';
export interface useDataProviderParams<
@ -12,13 +17,23 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
update?: ({
delta,
data,
pageInfo,
}: {
delta?: Delta;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
insert?: ({
insertionData,
data,
pageInfo,
}: {
insertionData?: Data | null;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
@ -30,7 +45,7 @@ export interface useDataProviderParams<
* @param dataProvider subscribe function created by makeDataProvider
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
* @param variables optional
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export const useDataProvider = <
Data,
@ -48,6 +63,7 @@ export const useDataProvider = <
const [data, setData] = useState<Data | null>(null);
const [loading, setLoading] = useState<boolean>(!skip);
const [error, setError] = useState<Error | undefined>(undefined);
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
const flushRef = useRef<(() => void) | undefined>(undefined);
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
const loadRef = useRef<Load<Data> | undefined>(undefined);
@ -93,6 +109,7 @@ export const useDataProvider = <
isInsert,
isUpdate,
loaded,
pageInfo,
} = args;
setError(error);
setLoading(!loaded && loading);
@ -104,21 +121,22 @@ export const useDataProvider = <
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data })))
updateRef.current({ delta, data, pageInfo })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data })
insertRef.current({ insertionData, data, pageInfo })
) {
return;
}
}
setData(data);
setPageInfo(pageInfo);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data });
updateRef.current({ data, pageInfo });
}
}, []);
@ -136,15 +154,13 @@ export const useDataProvider = <
useEffect(() => {
setData(null);
setPageInfo(null);
setError(undefined);
if (updateRef.current) {
updateRef.current({ data: null });
updateRef.current({ data: null, pageInfo: null });
}
if (skip) {
setLoading(false);
if (updateRef.current) {
updateRef.current({ data: null });
}
return;
}
setLoading(true);
@ -165,6 +181,7 @@ export const useDataProvider = <
}, [client, dataProvider, callback, variables, skip]);
return {
data,
pageInfo,
loading,
error,
flush,

View File

@ -109,7 +109,7 @@ describe('DealTicket', () => {
variables: {
partyId: 'pubKey',
filter: { liveOnly: true },
pagination: { first: 5000 },
pagination: { first: 1000 },
},
},
result: {

View File

@ -42,8 +42,12 @@ fragment FillEdge on TradeEdge {
cursor
}
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
query Fills(
$filter: TradesFilter
$pagination: Pagination
$dateRange: DateRange
) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
edges {
...FillEdge
}

View File

@ -12,6 +12,7 @@ export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node:
export type FillsQueryVariables = Types.Exact<{
filter?: Types.InputMaybe<Types.TradesFilter>;
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
@ -95,8 +96,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
}
${TradeFeeFieldsFragmentDoc}`;
export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
edges {
...FillEdge
}
@ -124,6 +125,7 @@ export const FillsDocument = gql`
* variables: {
* filter: // value for 'filter'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* },
* });
*/

View File

@ -1,11 +1,12 @@
import type { AgGridReact } from 'ag-grid-react';
import { useRef } from 'react';
import { useCallback, useRef, useState } from 'react';
import { t } from '@vegaprotocol/i18n';
import { FillsTable } from './fills-table';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import type * as Schema from '@vegaprotocol/types';
import { fillsWithMarketProvider } from './fills-data-provider';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface FillsManagerProps {
partyId: string;
@ -22,26 +23,60 @@ export const FillsManager = ({
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
partyIds: [partyId],
};
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
const { data, error, load, pageInfo } = useDataProvider({
dataProvider: fillsWithMarketProvider,
variables: { filter },
});
return (
<FillsTable
ref={gridRef}
rowData={data}
partyId={partyId}
onMarketClick={onMarketClick}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...gridProps}
/>
<div className="flex flex-col h-full">
<FillsTable
ref={gridRef}
rowData={data}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
partyId={partyId}
onMarketClick={onMarketClick}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No fills matching selected filters')}
</div>
) : null}
</div>
</div>
);
};

View File

@ -20,6 +20,7 @@ import {
negativeClassNames,
MarketNameCell,
COL_DEFS,
DateRangeFilter,
} from '@vegaprotocol/datagrid';
import type {
VegaValueFormatterParams,
@ -120,6 +121,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
},
{
headerName: t('Date'),
filter: DateRangeFilter,
field: 'createdAt',
valueFormatter: ({
value,

View File

@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 5000,
first: 1000,
},
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },

View File

@ -27,6 +27,7 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(generateJsx());
@ -45,6 +46,7 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(generateJsx());
@ -62,6 +64,7 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(

View File

@ -1,7 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import { useCallback, useRef, useState, useEffect } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { FilterChangedEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
@ -12,7 +11,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
export enum Filter {
'Open' = 'Open',
@ -48,33 +47,11 @@ export const OrderListManager = ({
? { partyId, filter: { liveOnly: true } }
: { partyId };
const { data, error } = useDataProvider({
const { data, error, pageInfo, load } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables,
update: ({ data }) => {
if (data && gridRef.current?.api) {
gridRef.current.api.setRowData(data);
return true;
}
return false;
},
});
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
gridProps?.onFilterChanged?.(event);
if (event.api) {
const isEmpty = event.api.getDisplayedRowCount() === 0;
if (isEmpty) {
event.api.showNoRowsOverlay();
} else {
event.api.hideOverlay();
}
}
},
[gridProps]
);
useEffect(() => {
if (!data || !data.length) {
gridRef.current?.api?.showNoRowsOverlay();
@ -96,6 +73,17 @@ export const OrderListManager = ({
[create]
);
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
if (error) {
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
}
@ -103,20 +91,60 @@ export const OrderListManager = ({
return (
<>
<div className="h-full relative">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...gridProps}
onFilterChanged={onFilterChanged}
/>
<div className="flex flex-col h-full">
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={(event) => {
onRowDataUpdated(event);
if (onFilterChanged) {
onFilterChanged(event);
}
}}
onRowDataUpdated={onRowDataUpdated}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{variables.filter?.liveOnly
? null
: t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
{data ? (
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button
size="extra-small"
className="ml-1"
onClick={() => load()}
>
{t('Load more')}
</Button>
) : null}
</div>
) : null}
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No orders matching selected filters')}
</div>
) : null}
</div>
</div>
</div>
{editOrder && (
<OrderEditDialog

View File

@ -30,6 +30,7 @@ describe('StopOrdersManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
await act(async () => {
render(generateJsx());

View File

@ -4,10 +4,13 @@ import { TradesTable } from './trades-table';
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
import { t } from '@vegaprotocol/i18n';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useCallback, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
interface TradesContainerProps {
marketId: string;
gridProps?: ReturnType<typeof useDataGridEvents>;
gridProps: ReturnType<typeof useDataGridEvents>;
}
export const TradesManager = ({
@ -16,19 +19,60 @@ export const TradesManager = ({
}: TradesContainerProps) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const { data, error } = useDataProvider({
const { data, error, load, pageInfo } = useDataProvider({
dataProvider: tradesWithMarketProvider,
variables: { marketId },
});
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
undefined
);
const { onFilterChanged, ...props } = gridProps || {};
const onRowDataUpdated = useCallback(
({ api }: { api: AgGridReact['api'] }) => {
setHasDisplayedRow(!!api.getDisplayedRowCount());
},
[]
);
return (
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...gridProps}
/>
<div className="flex flex-col h-full">
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
onFilterChanged={(event) => {
onRowDataUpdated(event);
onFilterChanged(event);
}}
onRowDataUpdated={onRowDataUpdated}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...props}
/>
<div className="flex justify-between border-t border-default p-1 items-center">
<div className="text-xs">
{t(
'Depending on data node retention you may not be able see the "full" history'
)}
</div>
<div className="flex text-xs items-center">
{data?.length && !pageInfo?.hasNextPage
? t('all %s items loaded', [data.length.toString()])
: t('%s items loaded', [
data?.length ? data.length.toString() : ' ',
])}
{pageInfo?.hasNextPage ? (
<Button size="extra-small" className="ml-1" onClick={() => load()}>
{t('Load more')}
</Button>
) : null}
</div>
{data?.length && hasDisplayedRow === false ? (
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
{t('No trades matching selected filters')}
</div>
) : null}
</div>
</div>
);
};