feat(#1643): orders table filtering (#2000)

* feat(#1643): add grid set filter, amend filters in orders table

* feat(#1643): strictly type variables in orders data provider

* feat(#1643): add date range param to orders query

* feat(#1643): add date range filter

* feat(#1643): handle data provider updates after variables change in ag-grid infinite row model

* feat(#1643): fix unit tests

* feat(#1643): use DateRangeFilter in positions table instead of agDateColumnFilter

* feat(#1643): add date range filter support to orders data provider

* feat(#1643): fix update functions

* feat(#1643): remove sortable from orders list columns

* chore: remove console.log
This commit is contained in:
Bartłomiej Głownia 2022-11-10 20:08:13 +01:00 committed by GitHub
parent 7ba72e3c8c
commit 25699b6283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 638 additions and 145 deletions

View File

@ -58,8 +58,10 @@ export const Last24hVolume = ({
).current;
const update = useCallback(
({ data }: { data: Candle[] }) => {
throttledSetCandles(data);
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetCandles(data);
}
return true;
},
[throttledSetCandles]
@ -80,8 +82,10 @@ export const Last24hVolume = ({
).current;
const updateCandle24hAgo = useCallback(
({ data }: { data: Candle[] }) => {
throttledSetVolumeChange(data);
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetVolumeChange(data);
}
return true;
},
[throttledSetVolumeChange]

View File

@ -48,7 +48,7 @@ export const Liquidity = () => {
});
const update = useCallback(
({ data }: { data: LiquidityProvisionData[] }) => {
({ data }: { data: LiquidityProvisionData[] | null }) => {
if (!gridRef.current?.api) {
return false;
}

View File

@ -85,9 +85,9 @@ export const Market = ({
const marketName = data?.tradableInstrument.instrument.name;
const updateProvider = useCallback(
({ data: marketData }: { data: MarketData }) => {
({ data: marketData }: { data: MarketData | null }) => {
const marketPrice = calculatePrice(
marketData.markPrice,
marketData?.markPrice,
data?.decimalPlaces
);
if (marketName) {

View File

@ -55,8 +55,10 @@ export const Last24hPriceChange = ({ marketId }: { marketId: string }) => {
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data }: { data: Candle[] }) => {
throttledSetCandles(data);
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetCandles(data);
}
return true;
},
[throttledSetCandles]

View File

@ -57,8 +57,10 @@ export const Last24hVolume = ({ marketId }: { marketId: string }) => {
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data }: { data: Candle[] }) => {
throttledSetCandles(data);
({ data }: { data: Candle[] | null }) => {
if (data) {
throttledSetCandles(data);
}
return true;
},
[throttledSetCandles]

View File

@ -33,9 +33,9 @@ export const MarketMarkPrice = ({ marketId }: { marketId: string }) => {
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data: marketData }: { data: MarketData }) => {
({ data: marketData }: { data: MarketData | null }) => {
throttledSetMarketPrice(
marketData.markPrice && data?.decimalPlaces
marketData?.markPrice && data?.decimalPlaces
? addDecimalsFormatNumber(marketData.markPrice, data.decimalPlaces)
: '-'
);

View File

@ -43,13 +43,15 @@ export const MarketTradingModeComponent = ({ marketId, onSelect }: Props) => {
});
const update = useCallback(
({ data: marketData }: { data: MarketData }) => {
setTradingMode(marketData.marketTradingMode);
setTrigger(marketData.trigger);
setMarket({
...data,
data: marketData,
} as TradingModeMarket);
({ data: marketData }: { data: MarketData | null }) => {
if (marketData) {
setTradingMode(marketData.marketTradingMode);
setTrigger(marketData.trigger);
setMarket({
...data,
data: marketData,
} as TradingModeMarket);
}
return true;
},
[data]

View File

@ -34,9 +34,10 @@ export const MarketVolume = ({ marketId }: { marketId: string }) => {
}, constants.DEBOUNCE_UPDATE_TIME)
).current;
const update = useCallback(
({ data: marketData }: { data: MarketData }) => {
({ data: marketData }: { data: MarketData | null }) => {
throttledSetMarketVolume(
marketData.indicativeVolume && data?.positionDecimalPlaces !== undefined
marketData?.indicativeVolume &&
data?.positionDecimalPlaces !== undefined
? addDecimalsFormatNumber(
marketData.indicativeVolume,
data.positionDecimalPlaces

View File

@ -17,7 +17,6 @@ export const PageQueryContainer = <TData, TVariables = OperationVariables>({
}: PageQueryContainerProps<TData, TVariables>) => {
const { data, loading, error } = useQuery<TData, TVariables>(query, {
...options,
errorPolicy: 'ignore',
});
return (

View File

@ -130,10 +130,13 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
variables,
});
const marketDataUpdate = useCallback(({ data }: { data: MarketData }) => {
marketDataRef.current = data;
return true;
}, []);
const marketDataUpdate = useCallback(
({ data }: { data: MarketData | null }) => {
marketDataRef.current = data;
return true;
},
[]
);
const {
data: marketData,

View File

@ -100,11 +100,14 @@ export const OrderbookManager = ({ marketId }: OrderbookManagerProps) => {
variables,
});
const marketDataUpdate = useCallback(({ data }: { data: MarketData }) => {
marketDataRef.current = data;
updateOrderbookData.current();
return true;
}, []);
const marketDataUpdate = useCallback(
({ data }: { data: MarketData | null }) => {
marketDataRef.current = data;
updateOrderbookData.current();
return true;
},
[]
);
const {
data: marketData,

View File

@ -22,10 +22,10 @@ fragment OrderFields on Order {
}
}
query Orders($partyId: ID!, $pagination: Pagination) {
query Orders($partyId: ID!, $pagination: Pagination, $dateRange: DateRange) {
party(id: $partyId) {
id
ordersConnection(pagination: $pagination) {
ordersConnection(pagination: $pagination, dateRange: $dateRange) {
edges {
node {
...OrderFields

View File

@ -8,6 +8,7 @@ export type OrderFieldsFragment = { __typename?: 'Order', id: string, type?: Typ
export type OrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
pagination?: Types.InputMaybe<Types.Pagination>;
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
@ -69,10 +70,10 @@ export const OrderUpdateFieldsFragmentDoc = gql`
}
`;
export const OrdersDocument = gql`
query Orders($partyId: ID!, $pagination: Pagination) {
query Orders($partyId: ID!, $pagination: Pagination, $dateRange: DateRange) {
party(id: $partyId) {
id
ordersConnection(pagination: $pagination) {
ordersConnection(pagination: $pagination, dateRange: $dateRange) {
edges {
node {
...OrderFields
@ -104,6 +105,7 @@ export const OrdersDocument = gql`
* variables: {
* partyId: // value for 'partyId'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* },
* });
*/

View File

@ -25,7 +25,7 @@ describe('order data provider', () => {
id: '0',
createdAt: new Date('2022-01-30').toISOString(),
},
// this one should be dropped because new newer below
// this one should be dropped because newer below
{
id: '1',
updatedAt: new Date('2022-02-01').toISOString(),
@ -49,7 +49,7 @@ describe('order data provider', () => {
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta);
const updatedData = update(data, delta, () => null, { partyId: '0x123' });
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
@ -62,5 +62,69 @@ describe('order data provider', () => {
expect(updatedData && updatedData[1].node.updatedAt).toEqual(
delta[4].updatedAt
);
expect(update([], delta, () => null, { partyId: '0x123' }).length).toEqual(
4
);
});
it('add only data matching date range filter', () => {
const data = [
{
node: {
id: '1',
updatedAt: new Date('2022-01-31').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
},
{
node: {
id: '2',
createdAt: new Date('2022-01-30').toISOString(),
},
},
] as Edge<OrderFieldsFragment>[];
const delta = [
// this one should be ignored because it does not match date range
{
id: '0',
createdAt: new Date('2022-02-02').toISOString(),
},
// this one should be removed because it does not match date range
{
id: '1',
updatedAt: new Date('2022-02-02').toISOString(),
createdAt: new Date('2022-01-29').toISOString(),
},
// this one should be updated
{
id: '2',
updatedAt: new Date('2022-01-31').toISOString(),
createdAt: new Date('2022-01-30').toISOString(),
},
// this should be added
{
id: '4',
createdAt: new Date('2022-01-31').toISOString(),
},
] as OrderUpdateFieldsFragment[];
const updatedData = update(data, delta, () => null, {
partyId: '0x123',
dateRange: { end: new Date('2022-02-01').toISOString() },
});
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[0].id)
).toEqual(-1);
expect(
updatedData?.findIndex((edge) => edge.node.id === delta[1].id)
).toEqual(-1);
expect(updatedData && updatedData[0].node.id).toEqual(delta[2].id);
expect(updatedData && updatedData[0].node.updatedAt).toEqual(
delta[2].updatedAt
);
expect(updatedData && updatedData[1].node.id).toEqual(delta[3].id);
expect(updatedData && updatedData[1].node.updatedAt).toEqual(
delta[3].updatedAt
);
});
});

View File

@ -15,6 +15,7 @@ import type {
OrderFieldsFragment,
OrdersQuery,
OrdersUpdateSubscription,
OrdersQueryVariables,
} from './__generated__/Orders';
import { OrdersDocument, OrdersUpdateDocument } from './__generated__/Orders';
@ -24,7 +25,7 @@ export type Order = Omit<OrderFieldsFragment, 'market'> & {
export type OrderEdge = Edge<Order>;
const getData = (responseData: OrdersQuery) =>
responseData?.party?.ordersConnection?.edges || null;
responseData?.party?.ordersConnection?.edges || [];
const getDelta = (subscriptionData: OrdersUpdateSubscription) =>
subscriptionData.orders || [];
@ -34,7 +35,9 @@ const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
export const update = (
data: ReturnType<typeof getData>,
delta: ReturnType<typeof getDelta>
delta: ReturnType<typeof getDelta>,
reload: () => void,
variables?: OrdersQueryVariables
) => {
if (!data) {
return data;
@ -51,14 +54,36 @@ export const update = (
incoming.reverse().forEach((node) => {
const index = draft.findIndex((edge) => edge.node.id === node.id);
const newer =
draft.length === 0 ||
(node.updatedAt || node.createdAt) >=
(draft[0].node.updatedAt || draft[0].node.createdAt);
(draft[0].node.updatedAt || draft[0].node.createdAt);
let doesFilterPass = true;
if (
doesFilterPass &&
variables?.dateRange?.start &&
new Date(node.updatedAt || node.createdAt) <=
new Date(variables?.dateRange?.start)
) {
doesFilterPass = false;
}
if (
doesFilterPass &&
variables?.dateRange?.end &&
new Date(node.updatedAt || node.createdAt) >=
new Date(variables?.dateRange?.end)
) {
doesFilterPass = false;
}
if (index !== -1) {
Object.assign(draft[index].node, node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
if (doesFilterPass) {
Object.assign(draft[index].node, node);
if (newer) {
draft.unshift(...draft.splice(index, 1));
}
} else {
draft.splice(index, 1);
}
} else if (newer) {
} else if (newer && doesFilterPass) {
const { marketId, liquidityProvisionId, ...order } = node;
// If there is a liquidity provision id add the object to the resulting order
@ -103,7 +128,8 @@ export const ordersProvider = makeDataProvider({
export const ordersWithMarketProvider = makeDerivedDataProvider<
(OrderEdge | null)[],
Order[]
Order[],
OrdersQueryVariables
>(
[ordersProvider, marketsProvider],
(partsData): OrderEdge[] =>

View File

@ -4,6 +4,20 @@ import * as useDataProviderHook from '@vegaprotocol/react-helpers';
import type { OrderFieldsFragment } from '../';
import * as orderListMock from '../order-list/order-list';
import { forwardRef } from 'react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
const generateJsx = () => {
const pubKey = '0x123';
return (
<MockedProvider>
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
<OrderListManager partyId={pubKey} />
</VegaWalletContext.Provider>
</MockedProvider>
);
};
it('Renders a loading state while awaiting orders', () => {
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
@ -15,7 +29,7 @@ it('Renders a loading state while awaiting orders', () => {
load: jest.fn(),
totalCount: 0,
});
render(<OrderListManager partyId="0x123" />);
render(generateJsx());
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
@ -30,7 +44,7 @@ it('Renders an error state', () => {
load: jest.fn(),
totalCount: undefined,
});
render(<OrderListManager partyId="0x123" />);
render(generateJsx());
expect(
screen.getByText(`Something went wrong: ${errorMsg}`)
).toBeInTheDocument();
@ -49,6 +63,6 @@ it('Renders the order list if orders provided', async () => {
load: jest.fn(),
totalCount: undefined,
});
render(<OrderListManager partyId="0x123" />);
render(generateJsx());
expect(await screen.findByText('OrderList')).toBeInTheDocument();
});

View File

@ -1,21 +1,32 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useRef } from 'react';
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
import { t } from '@vegaprotocol/react-helpers';
import { useRef, useState } from 'react';
import type {
BodyScrollEvent,
BodyScrollEndEvent,
FilterChangedEvent,
SortChangedEvent,
} from 'ag-grid-community';
import type { AgGridReact } from 'ag-grid-react';
import { OrderList } from '../order-list/order-list';
import { useOrderListData } from './use-order-list-data';
import type { Filter, Sort } from './use-order-list-data';
interface OrderListManagerProps {
export interface OrderListManagerProps {
partyId: string;
}
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const scrolledToTop = useRef(true);
const [sort, setSort] = useState<Sort[] | undefined>();
const [filter, setFilter] = useState<Filter | undefined>();
const { data, error, loading, addNewRows, getRows } = useOrderListData({
partyId,
sort,
filter,
gridRef,
scrolledToTop,
});
@ -30,16 +41,49 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
scrolledToTop.current = event.top <= 0;
};
const onFilterChanged = (event: FilterChangedEvent) => {
const updatedFilter = event.api.getFilterModel();
if (Object.keys(updatedFilter).length) {
setFilter(updatedFilter);
} else if (filter) {
setFilter(undefined);
}
};
const onSortChange = (event: SortChangedEvent) => {
const sort = event.columnApi
.getColumnState()
.sort((a, b) => (a.sortIndex || 0) - (b.sortIndex || 0))
.reduce((acc, col) => {
if (col.sort) {
const { colId, sort } = col;
acc.push({ colId, sort });
}
return acc;
}, [] as { colId: string; sort: string }[]);
setSort(sort.length > 0 ? sort : undefined);
};
return (
<AsyncRenderer loading={loading} error={error} data={data}>
<>
<OrderList
ref={gridRef}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
rowModelType="infinite"
datasource={{ getRows }}
onBodyScrollEnd={onBodyScrollEnd}
onBodyScroll={onBodyScroll}
onFilterChanged={onFilterChanged}
onSortChanged={onSortChange}
/>
</AsyncRenderer>
<div className="pointer-events-none absolute inset-0 top-5">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !(data && data.length)}
/>
</div>
</>
);
};

View File

@ -95,8 +95,6 @@ describe('useOrderListData Hook', () => {
getRows: expect.any(Function),
});
updateMock({ data: mockData, delta: [] });
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
updateMock({ data: mockData, delta: [] });
expect(mockRefreshAgGridApi).toHaveBeenCalled();
});

View File

@ -5,17 +5,43 @@ import {
makeInfiniteScrollGetRows,
useDataProvider,
} from '@vegaprotocol/react-helpers';
import { ordersWithMarketProvider } from '../';
import type { OrderEdge, Order } from '../';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import type {
OrderEdge,
Order,
} from '../order-data-provider/order-data-provider';
import type { OrdersQueryVariables } from '../order-data-provider/__generated__/Orders';
import type { Schema as Types } from '@vegaprotocol/types';
export interface Sort {
colId: string;
sort: string;
}
export interface Filter {
updatedAt?: {
value: Types.DateRange;
};
type?: {
value: Types.OrderType[];
};
status?: {
value: Types.OrderStatus[];
};
timeInForce?: {
value: Types.OrderTimeInForce[];
};
}
interface Props {
partyId: string;
filter?: Filter;
sort?: Sort[];
gridRef: RefObject<AgGridReact>;
scrolledToTop: RefObject<boolean>;
}
export const useOrderListData = ({
partyId,
sort,
filter,
gridRef,
scrolledToTop,
}: Props) => {
@ -23,7 +49,10 @@ export const useOrderListData = ({
const totalCountRef = useRef<number | undefined>(undefined);
const newRows = useRef(0);
const variables = useMemo(() => ({ partyId }), [partyId]);
const variables = useMemo<OrdersQueryVariables>(
() => ({ partyId, dateRange: filter?.updatedAt?.value }),
[partyId, filter]
);
const addNewRows = useCallback(() => {
if (newRows.current === 0) {
@ -37,22 +66,28 @@ export const useOrderListData = ({
}, [gridRef]);
const update = useCallback(
({ data, delta }: { data: (OrderEdge | null)[]; delta?: Order[] }) => {
if (dataRef.current?.length) {
if (!scrolledToTop.current) {
const createdAt = dataRef.current?.[0]?.node.createdAt;
if (createdAt) {
newRows.current += (delta || []).filter(
(trade) => trade.createdAt > createdAt
).length;
}
({
data,
delta,
}: {
data: (OrderEdge | null)[] | null;
delta?: Order[];
}) => {
if (dataRef.current?.length && delta?.length && !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 avoidRerender = !!(
(dataRef.current?.length && data?.length) ||
(!dataRef.current?.length && !data?.length)
);
dataRef.current = data;
return false;
gridRef.current?.api?.refreshInfiniteCache();
return avoidRerender;
},
[gridRef, scrolledToTop]
);
@ -62,7 +97,7 @@ export const useOrderListData = ({
data,
totalCount,
}: {
data: (OrderEdge | null)[];
data: (OrderEdge | null)[] | null;
totalCount?: number;
}) => {
dataRef.current = data;

View File

@ -7,6 +7,8 @@ import {
positiveClassNames,
t,
truncateByChars,
SetFilter,
DateRangeFilter,
} from '@vegaprotocol/react-helpers';
import {
OrderRejectionReasonMapping,
@ -155,7 +157,11 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<AgGrid
ref={ref}
overlayNoRowsTemplate="No orders"
defaultColDef={{ flex: 1, resizable: true }}
defaultColDef={{
flex: 1,
resizable: true,
filterParams: { buttons: ['reset'] },
}}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data.id}
rowHeight={34}
@ -165,6 +171,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.code"
filter
cellRenderer={({
value,
data,
@ -216,6 +223,10 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/>
<AgGridColumn
field="type"
filter={SetFilter}
filterParams={{
set: OrderTypeMapping,
}}
valueFormatter={({
data: order,
value,
@ -232,6 +243,10 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/>
<AgGridColumn
field="status"
filter={SetFilter}
filterParams={{
set: OrderStatusMapping,
}}
valueFormatter={({
value,
data,
@ -295,6 +310,10 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/>
<AgGridColumn
field="timeInForce"
filter={SetFilter}
filterParams={{
set: OrderTimeInForceMapping,
}}
valueFormatter={({
value,
data,
@ -322,6 +341,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
/>
<AgGridColumn
field="updatedAt"
filter={DateRangeFilter}
valueFormatter={({
value,
node,

View File

@ -1,6 +1,11 @@
import { useRef } from 'react';
import { AsyncRenderer, Icon, Intent } from '@vegaprotocol/ui-toolkit';
import { useClosePosition, usePositionsData, PositionsTable } from '../';
import {
useClosePosition,
usePositionsData,
PositionsTable,
getSummaryRowData,
} from '../';
import type { AgGridReact } from 'ag-grid-react';
import { Requested } from './close-position-dialog/requested';
import { Complete } from './close-position-dialog/complete';
@ -29,6 +34,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
<PositionsTable
ref={gridRef}
rowData={data}
pinnedBottomRowData={data ? [getSummaryRowData(data)] : []}
onClose={(position) => submit(position)}
/>
</AsyncRenderer>

View File

@ -22,6 +22,7 @@ import {
getDateTimeFormat,
signedNumberCssClass,
signedNumberCssClassRules,
DateRangeFilter,
} from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react';
@ -137,6 +138,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
tooltipComponent: TooltipCellComponent,
}}
components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
@ -421,7 +423,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
headerName={t('Updated')}
field="updatedAt"
type="rightAligned"
filter="agDateColumnFilter"
filter={DateRangeFilter}
valueFormatter={({
value,
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {

View File

@ -9,7 +9,7 @@ import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
export const getRowId = ({ data }: { data: Position }) => data.marketId;
const getSummaryRowData = (positions: Position[]) => {
export const getSummaryRowData = (positions: Position[]) => {
const summaryRow = {
notional: new BigNumber(0),
realisedPNL: BigInt(0),

View File

@ -1,5 +1,6 @@
import { marketDataProvider, marketProvider } from '@vegaprotocol/market-list';
import { isOrderActive, ordersWithMarketProvider } from '@vegaprotocol/orders';
import type { OrdersQueryVariables } from '@vegaprotocol/orders';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
@ -8,7 +9,10 @@ export const useRequestClosePositionData = (
partyId?: string
) => {
const marketVariables = useMemo(() => ({ marketId }), [marketId]);
const orderVariables = useMemo(() => ({ partyId }), [partyId]);
const orderVariables = useMemo<OrdersQueryVariables>(
() => ({ partyId: partyId || '' }),
[partyId]
);
const { data: market, loading: marketLoading } = useDataProvider({
dataProvider: marketProvider,
variables: marketVariables,
@ -21,6 +25,7 @@ export const useRequestClosePositionData = (
const { data: orderData, loading: orderDataLoading } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables: orderVariables,
skip: !partyId,
});
const orders = useMemo(() => {

View File

@ -21,14 +21,22 @@ interface useDataProviderParams<
Variables extends OperationVariables = OperationVariables
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({ delta, data }: { delta?: Delta; data: Data }) => boolean;
update?: ({
delta,
data,
variables,
}: {
delta?: Delta;
data: Data | null;
variables?: Variables;
}) => boolean;
insert?: ({
insertionData,
data,
totalCount,
}: {
insertionData: Data;
data: Data;
data: Data | null;
totalCount?: number;
}) => boolean;
variables?: Variables;
@ -95,41 +103,45 @@ export const useDataProvider = <
} = arg;
setError(error);
setLoading(loading);
if (!error && !loading) {
// if update or insert function returns true it means that component handles updates
// component can use flush() which will call callback without delta and cause data state update
if (initialized.current && data) {
if (
isUpdate &&
!noUpdate &&
update &&
hasDelta<Delta>(arg) &&
update({ delta: arg.delta, data })
) {
return;
}
if (
isInsert &&
insert &&
(!insertionData || insert({ insertionData, data, totalCount }))
) {
return;
}
// if update or insert function returns true it means that component handles updates
// component can use flush() which will call callback without delta and cause data state update
if (initialized.current) {
if (
isUpdate &&
!noUpdate &&
update &&
hasDelta<Delta>(arg) &&
update({ delta: arg.delta, data, variables })
) {
return;
}
setTotalCount(totalCount);
setData(data);
if (updateOnInit && !initialized.current && update && data) {
update({ data });
if (
isInsert &&
insert &&
(!insertionData || insert({ insertionData, data, totalCount }))
) {
return;
}
initialized.current = true;
}
setTotalCount(totalCount);
setData(data);
if (updateOnInit && !initialized.current && update) {
update({ data });
}
initialized.current = true;
},
[update, insert, noUpdate, updateOnInit]
[update, insert, noUpdate, updateOnInit, variables]
);
useEffect(() => {
setData(null);
setError(undefined);
setTotalCount(undefined);
if (skip) {
setLoading(false);
return;
}
setLoading(true);
initialized.current = false;
const { unsubscribe, flush, reload, load } = dataProvider(
callback,
client,
@ -138,7 +150,6 @@ export const useDataProvider = <
flushRef.current = flush;
reloadRef.current = reload;
loadRef.current = load;
initialized.current = false;
return unsubscribe;
}, [client, initialized, dataProvider, callback, variables, skip]);
return { data, loading, error, flush, reload, load, totalCount };

View File

@ -2,8 +2,8 @@ import type {
ApolloClient,
DocumentNode,
FetchPolicy,
TypedDocumentNode,
OperationVariables,
TypedDocumentNode,
} from '@apollo/client';
import type { Subscription } from 'zen-observable-ts';
import isEqual from 'lodash/isEqual';
@ -35,6 +35,10 @@ export interface Load<Data> {
(start?: number, end?: number): Promise<Data | null>;
}
export interface Reload {
(forceReset?: boolean): void;
}
type Pagination = Schema.Pagination & {
skip?: number;
};
@ -56,7 +60,7 @@ export interface Subscribe<
variables?: Variables
): {
unsubscribe: () => void;
reload: (forceReset?: boolean) => void;
reload: Reload;
flush: () => void;
load?: Load<Data>;
};
@ -65,8 +69,12 @@ export interface Subscribe<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
export interface Update<Data, Delta> {
(data: Data, delta: Delta, reload: (forceReset?: boolean) => void): Data;
export interface Update<
Data,
Delta,
Variables extends OperationVariables = OperationVariables
> {
(data: Data, delta: Delta, reload: Reload, variables?: Variables): Data;
}
export interface Append<Data> {
@ -82,8 +90,8 @@ export interface Append<Data> {
};
}
interface GetData<QueryData, Data> {
(queryData: QueryData, variables?: OperationVariables): Data | null;
interface GetData<QueryData, Data, Variables> {
(queryData: QueryData, variables?: Variables): Data | null;
}
interface GetPageInfo<QueryData> {
@ -94,8 +102,8 @@ interface GetTotalCount<QueryData> {
(queryData: QueryData): number | undefined;
}
interface GetDelta<SubscriptionData, Delta> {
(subscriptionData: SubscriptionData, variables?: OperationVariables): Delta;
interface GetDelta<SubscriptionData, Delta, Variables> {
(subscriptionData: SubscriptionData, variables?: Variables): Delta;
}
export type Node = { id: string };
@ -146,12 +154,18 @@ export function defaultAppend<Data>(
return { data, totalCount };
}
interface DataProviderParams<QueryData, Data, SubscriptionData, Delta> {
interface DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables extends OperationVariables = OperationVariables
> {
query: Query<QueryData>;
subscriptionQuery?: Query<SubscriptionData>;
update?: Update<Data, Delta>;
getData: GetData<QueryData, Data>;
getDelta?: GetDelta<SubscriptionData, Delta>;
update?: Update<Data, Delta, Variables>;
getData: GetData<QueryData, Data, Variables>;
getDelta?: GetDelta<SubscriptionData, Delta, Variables>;
pagination?: {
getPageInfo: GetPageInfo<QueryData>;
getTotalCount?: GetTotalCount<QueryData>;
@ -185,18 +199,20 @@ function makeDataProviderInternal<
pagination,
fetchPolicy,
resetDelay,
}: DataProviderParams<QueryData, Data, SubscriptionData, Delta>): Subscribe<
}: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables
> {
>): Subscribe<Data, Delta, Variables> {
// list of callbacks passed through subscribe call
const callbacks: UpdateCallback<Data, Delta>[] = [];
// subscription is started before initial query, all deltas that will arrive before initial query response are put on queue
const updateQueue: Delta[] = [];
let resetTimer: ReturnType<typeof setTimeout>;
let variables: OperationVariables | undefined;
let variables: Variables | undefined;
let data: Data | null = null;
let error: Error | undefined;
let loading = true;
@ -228,14 +244,14 @@ function makeDataProviderInternal<
};
const load = async (start?: number, end?: number) => {
if (!pagination || !pageInfo || !(data instanceof Array)) {
if (!pagination) {
return Promise.reject();
}
const paginationVariables: Pagination = {
first: pagination.first,
after: pageInfo.endCursor,
after: pageInfo?.endCursor,
};
if (start !== undefined) {
if (start !== undefined && data instanceof Array) {
if (!start) {
paginationVariables.after = undefined;
} else if (data && data[start - 1]) {
@ -252,7 +268,7 @@ function makeDataProviderInternal<
paginationVariables.after = (data[start - 1 - skip] as Cursor).cursor;
}
}
} else if (!pageInfo.hasNextPage) {
} else if (!pageInfo?.hasNextPage) {
return null;
}
const res = await client.query<QueryData>({
@ -291,7 +307,6 @@ function makeDataProviderInternal<
? { ...variables, pagination: { first: pagination.first } }
: variables,
fetchPolicy: fetchPolicy || 'no-cache',
errorPolicy: 'ignore',
});
data = getData(res.data, variables);
if (data && pagination) {
@ -317,7 +332,7 @@ function makeDataProviderInternal<
while (updateQueue.length) {
const delta = updateQueue.shift();
if (delta) {
data = update(data, delta, reload);
data = update(data, delta, reload, variables);
}
}
}
@ -380,7 +395,7 @@ function makeDataProviderInternal<
if (loading || !data) {
updateQueue.push(delta);
} else {
const updatedData = update(data, delta, reload);
const updatedData = update(data, delta, reload, variables);
if (updatedData === data) {
return;
}
@ -484,7 +499,7 @@ const memoize = <
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>({
* query: gql`query MarketMidPrice($marketId: ID!) { market(id: $marketId) { data { midPrice } } }`,
* subscriptionQuery: gql`subscription MarketMidPriceSubscription($marketId: ID!) { marketDepthUpdate(marketId: $marketId) { market { data { midPrice } } } }`,
* update: (draft: Draft<Data>, delta: Delta, reload: (forceReset?: boolean) => void) => { draft.midPrice = delta.midPrice }
* update: (draft: Draft<Data>, delta: Delta, reload: Reload) => { draft.midPrice = delta.midPrice }
* getData: (data:QueryData) => data.market.data.midPrice
* getDelta: (delta:SubscriptionData) => delta.marketData.market
* })
@ -503,9 +518,15 @@ export function makeDataProvider<
Delta,
Variables extends OperationVariables = OperationVariables
>(
params: DataProviderParams<QueryData, Data, SubscriptionData, Delta>
params: DataProviderParams<
QueryData,
Data,
SubscriptionData,
Delta,
Variables
>
): Subscribe<Data, Delta, Variables> {
const getInstance = memoize<Data, Delta>(() =>
const getInstance = memoize<Data, Delta, Variables>(() =>
makeDataProviderInternal(params)
);
return (callback, client, variables) =>
@ -516,13 +537,22 @@ export function makeDataProvider<
* Dependency subscribe needs to use any as Data and Delta because it's unknown what dependencies will be used.
* This effects in parts in combine function has any[] type
*/
type DependencySubscribe = Subscribe<any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
type DependencyUpdateCallback = Parameters<DependencySubscribe>['0'];
export type DerivedPart = Parameters<DependencyUpdateCallback>['0'];
type DependencySubscribe<
Variables extends OperationVariables = OperationVariables
> = Subscribe<any, any, Variables>; // eslint-disable-line @typescript-eslint/no-explicit-any
type DependencyUpdateCallback<
Variables extends OperationVariables = OperationVariables
> = Parameters<DependencySubscribe<Variables>>['0'];
export type DerivedPart<
Variables extends OperationVariables = OperationVariables
> = Parameters<DependencyUpdateCallback<Variables>>['0'];
export type CombineDerivedData<
Data,
Variables extends OperationVariables = OperationVariables
> = (data: DerivedPart['data'][], variables?: Variables) => Data | null;
> = (
data: DerivedPart<Variables>['data'][],
variables?: Variables
) => Data | null;
export type CombineDerivedDelta<
Data,
@ -530,7 +560,7 @@ export type CombineDerivedDelta<
Variables extends OperationVariables = OperationVariables
> = (
data: Data,
parts: DerivedPart[],
parts: DerivedPart<Variables>[],
previousData: Data | null,
variables?: Variables
) => Delta | undefined;
@ -540,7 +570,7 @@ export type CombineInsertionData<
Variables extends OperationVariables = OperationVariables
> = (
data: Data,
parts: DerivedPart[],
parts: DerivedPart<Variables>[],
variables?: Variables
) => Data | undefined;
@ -549,7 +579,7 @@ function makeDerivedDataProviderInternal<
Delta,
Variables extends OperationVariables = OperationVariables
>(
dependencies: DependencySubscribe[],
dependencies: DependencySubscribe<Variables>[],
combineData: CombineDerivedData<Data, Variables>,
combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
combineInsertionData?: CombineInsertionData<Data, Variables>
@ -558,7 +588,7 @@ function makeDerivedDataProviderInternal<
let client: ApolloClient<object>;
const callbacks: UpdateCallback<Data, Delta>[] = [];
let variables: Variables | undefined;
const parts: DerivedPart[] = [];
const parts: DerivedPart<Variables>[] = [];
let data: Data | null = null;
let error: Error | undefined;
let loading = true;
@ -700,7 +730,7 @@ export function makeDerivedDataProvider<
Delta,
Variables extends OperationVariables = OperationVariables
>(
dependencies: DependencySubscribe[],
dependencies: DependencySubscribe<Variables>[],
combineData: CombineDerivedData<Data, Variables>,
combineDelta?: CombineDerivedDelta<Data, Delta, Variables>,
combineInsertionData?: CombineInsertionData<Data, Variables>

View File

@ -0,0 +1,125 @@
import type { ChangeEvent } from 'react';
import type { Schema as Types } from '@vegaprotocol/types';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
import { isValidDate } from '../format/date';
const defaultFilterValue: Types.DateRange = {};
const toInputValue = (value: string) => {
const date = new Date(value);
if (!isValidDate(date)) {
return;
}
return `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}T${(
date.getHours() + 1
)
.toString()
.padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
};
export const DateRangeFilter = forwardRef((props: IFilterParams, ref) => {
const [value, setValue] = useState<Types.DateRange>(defaultFilterValue);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
doesFilterPass(params: IDoesFilterPassParams) {
const { api, colDef, column, columnApi, context } = props;
const { node } = params;
const rowValue = props.valueGetter({
api,
colDef,
column,
columnApi,
context,
data: node.data,
getValue: (field) => node.data[field],
node,
});
if (
value.start &&
rowValue &&
new Date(rowValue) <= new Date(value.start)
) {
return false;
}
if (
value.end &&
rowValue &&
new Date(rowValue) >= new Date(value.end)
) {
return false;
}
return true;
},
isFilterActive() {
return value.start || value.end;
},
getModel() {
if (!this.isFilterActive()) {
return null;
}
return { value };
},
setModel(model?: { value: Types.DateRange } | null) {
setValue(model?.value || defaultFilterValue);
},
};
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue({
...value,
[event.target.name]:
event.target.value &&
new Date(event.target.value).toISOString().replace('Z', '000000Z'),
});
};
useEffect(() => {
props.filterChangedCallback();
}, [value]); //eslint-disable-line react-hooks/exhaustive-deps
const start = (value.start && toInputValue(value.start)) || '';
const end = (value.end && toInputValue(value.end)) || '';
return (
<div className="ag-filter-body-wrapper">
<fieldset className="ag-simple-filter-body-wrapper">
<label className="block" key="start">
<span className="block">start</span>
<input
type="datetime-local"
name="start"
value={start}
onChange={onChange}
/>
</label>
<label className="block" key="end">
<span className="block">end</span>
<input
type="datetime-local"
name="end"
value={end}
onChange={onChange}
/>
</label>
</fieldset>
<div className="ag-filter-apply-panel">
<button
type="button"
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue(defaultFilterValue)}
>
Reset
</button>
</div>
</div>
);
});

View File

@ -6,3 +6,5 @@ export * from './price-flash-cell';
export * from './size';
export * from './summary-rows';
export * from './vol-cell';
export * from './set-filter';
export * from './date-range-filter';

View File

@ -0,0 +1,91 @@
import type { ChangeEvent } from 'react';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react';
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
export const SetFilter = forwardRef((props: IFilterParams, ref) => {
const [value, setValue] = useState<string[]>([]);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
doesFilterPass(params: IDoesFilterPassParams) {
const { api, colDef, column, columnApi, context } = props;
const { node } = params;
return (
props.valueGetter({
api,
colDef,
column,
columnApi,
context,
data: node.data,
getValue: (field) => node.data[field],
node,
}) === value
);
},
isFilterActive() {
return value.length !== 0;
},
getModel() {
if (!this.isFilterActive()) {
return null;
}
return { value };
},
setModel(model?: { value: string[] } | null) {
setValue(!model ? [] : model.value);
},
};
});
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(
event.target.checked
? [...value, event.target.value]
: value.filter((v) => v !== event.target.value)
);
};
useEffect(() => {
props.filterChangedCallback();
}, [value]); //eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="ag-filter-body-wrapper">
<fieldset className="ag-simple-filter-body-wrapper">
{Object.keys(props.colDef.filterParams.set).map((key) => (
<label className="flex">
<input
type="checkbox"
key={key}
value={key}
className="mr-1"
checked={value.includes(key)}
onChange={onChange}
/>
<span>{props.colDef.filterParams.set[key]}</span>
</label>
))}
</fieldset>
<div className="ag-filter-apply-panel">
<button
type="button"
className="ag-standard-button ag-filter-apply-panel-button"
onClick={() => setValue([])}
>
Reset
</button>
</div>
</div>
);
});

View File

@ -11,6 +11,7 @@ interface AsyncRendererProps<T> {
noDataMessage?: string;
children?: ReactNode | null;
render?: (data: T) => ReactNode;
noDataCondition?(data: T): boolean;
}
export function AsyncRenderer<T = object>({
@ -20,6 +21,7 @@ export function AsyncRenderer<T = object>({
errorMessage,
data,
noDataMessage,
noDataCondition,
children,
render,
}: AsyncRendererProps<T>) {
@ -37,7 +39,7 @@ export function AsyncRenderer<T = object>({
return <Splash>{loadingMessage ? loadingMessage : t('Loading...')}</Splash>;
}
if (!data) {
if (!data || (noDataCondition && noDataCondition(data))) {
return <Splash>{noDataMessage ? noDataMessage : t('No data')}</Splash>;
}
// eslint-disable-next-line react/jsx-no-useless-fragment