Compare commits

...
Author SHA1 Message Date
Matthew Russell 8990c1185c chore: add applyTransaction 2023-05-26 13:07:57 -07:00
Matthew Russell 375f9b07ea chore: simple order list example 2023-05-26 12:15:16 -07:00
@@ -1,42 +1,23 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit';
import compact from 'lodash/compact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import type { GridReadyEvent, FilterChangedEvent } from 'ag-grid-community';
import { OrderListTable } from '../order-list/order-list';
import { useHasAmendableOrder } from '../../order-hooks/use-has-amendable-order';
import { useBottomPlaceholder } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
import {
normalizeOrderAmendment,
useVegaTransactionStore,
} from '@vegaprotocol/wallet';
import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/wallet';
import { OrderEditDialog } from '../order-list/order-edit-dialog';
import type { Order } from '../order-data-provider';
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { useMarketsQuery } from '@vegaprotocol/markets';
import { useAssetsQuery } from '@vegaprotocol/assets';
import { AgGridLazy } from '@vegaprotocol/datagrid';
import type { OrdersUpdateSubscription } from '../order-data-provider';
import { useOrdersUpdateSubscription } from '../order-data-provider';
import type { ArrayElement } from 'type-fest/source/internal';
import { OrderStatus } from '@vegaprotocol/types';
import { orderBy, uniqBy } from 'lodash';
import type { ColDef, GridApi } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
export enum Filter {
'Open',
'Closed',
'Rejected',
}
const FilterStatusValue = {
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
[Filter.Closed]: [
OrderStatus.STATUS_CANCELLED,
OrderStatus.STATUS_EXPIRED,
OrderStatus.STATUS_FILLED,
OrderStatus.STATUS_PARTIALLY_FILLED,
OrderStatus.STATUS_STOPPED,
],
[Filter.Rejected]: [OrderStatus.STATUS_REJECTED],
};
export interface OrderListManagerProps {
partyId: string;
marketId?: string;
@@ -48,164 +29,209 @@ export interface OrderListManagerProps {
storeKey?: string;
}
const CancelAllOrdersButton = ({ onClick }: { onClick: () => void }) => (
<div className="dark:bg-black/75 bg-white/75 h-auto flex justify-end px-[11px] py-2 absolute bottom-0 right-3 rounded">
<Button
variant="primary"
size="sm"
onClick={onClick}
data-testid="cancelAll"
>
{t('Cancel all')}
</Button>
</div>
);
const useMarkets = () => {
const { data, loading, error } = useMarketsQuery();
export const OrderListManager = ({
partyId,
marketId,
onMarketClick,
onOrderTypeClick,
isReadOnly,
enforceBottomPlaceholder,
filter,
storeKey,
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [hasData, setHasData] = useState(false);
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const { data, error, loading, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables:
filter === Filter.Open
? { partyId, filter: { liveOnly: true } }
: { partyId },
const markets = data?.marketsConnection?.edges.map((e) => e.node) || [];
const marketLookup: Record<string, MarketFieldsFragment> = {};
markets.forEach((m) => {
marketLookup[m.id] = m;
});
const {
onFilterChanged: bottomPlaceholderOnFilterChanged,
...bottomPlaceholderProps
} = useBottomPlaceholder({
gridRef,
disabled: !enforceBottomPlaceholder && !isReadOnly && !hasAmendableOrder,
});
return {
markets: marketLookup,
data,
loading,
error,
};
};
const cancel = useCallback(
(order: Order) => {
if (!order.market) return;
create({
orderCancellation: {
orderId: order.id,
marketId: order.market.id,
},
const useAssets = () => {
const { data, loading, error } = useAssetsQuery();
return {
assets: compact(data?.assetsConnection?.edges || []).map((e) => e.node),
data,
loading,
error,
};
};
type Order = ArrayElement<OrdersUpdateSubscription['orders']>;
const useOrders = (partyId?: string) => {
const loadRef = useRef(true);
const [orders, setOrders] = useState<Order[]>([]);
const { loading, error } = useOrdersUpdateSubscription({
variables: {
partyId: partyId || '',
},
skip: !partyId,
fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
onData: ({ data }) => {
const update = data.data?.orders || [];
if (loadRef.current) {
setOrders(update);
loadRef.current = false;
return;
}
if (!update.length) return;
// update orders
setOrders((curr) => {
// this might not be required all timestamps seem to be the same now
const sortedIncoming = orderBy(
update,
(o) => {
if (o.updatedAt) {
return new Date(o.updatedAt).getTime();
}
return new Date(o.createdAt).getTime();
},
'desc'
);
const combined = uniqBy([...sortedIncoming, ...curr], 'id');
return combined.filter((o) => isOrderActive(o));
});
},
[create]
);
});
const onGridReady = useCallback(
({ api }: GridReadyEvent) => {
if (filter !== undefined) {
api.setFilterModel({
status: {
value: FilterStatusValue[filter],
},
});
}
},
[filter]
);
return {
orders,
loading,
error,
};
};
const onFilterChanged = useCallback(
(event: FilterChangedEvent) => {
const rowCount = gridRef.current?.api?.getModel().getRowCount();
setHasData((rowCount ?? 0) > 0);
bottomPlaceholderOnFilterChanged?.();
},
[bottomPlaceholderOnFilterChanged]
);
const isOrderActive = (o: { status: OrderStatus }) => {
return [
OrderStatus.STATUS_ACTIVE,
OrderStatus.STATUS_PARKED,
OrderStatus.STATUS_PARTIALLY_FILLED,
].includes(o.status);
};
useEffect(() => {
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
}, [data]);
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
const loadRef = useRef(true);
const [gridApi, setGridApi] = useState<GridApi | null>(null);
const { markets } = useMarkets();
const { assets } = useAssets();
// Example using react state
//
// Pass orders as rowData, because we use getRowId the grid should update 'naturally'
const { orders } = useOrders(partyId);
const cancelAll = useCallback(() => {
create({
orderCancellation: {},
});
}, [create]);
// Example with gridApi.applyTransaction
//
// in this example the entire order state is in the grid as we
// dont cache in apollo and we discard the data once applied to the grid
// useOrdersUpdateSubscription({
// variables: {
// partyId: partyId || '',
// },
// skip: !gridApi || !partyId,
// fetchPolicy: 'no-cache', // dont cache as we are caching here in the hook
// onData: ({ data }) => {
// console.log(gridApi);
// if (!gridApi) {
// throw new Error('Grid not ready');
// }
// const incoming = data.data?.orders || [];
// if (loadRef.current) {
// loadRef.current = false;
// gridApi.applyTransaction({
// add: incoming,
// });
// return;
// }
// if (!incoming.length) return;
// const add: Order[] = [];
// const remove: Order[] = [];
// const update: Order[] = [];
// incoming.forEach((o) => {
// const exists = gridApi.getRowNode(o.id);
// if (exists) {
// if (isOrderActive(o)) {
// update.push(o);
// } else {
// remove.push(o);
// }
// } else {
// if (isOrderActive(o)) {
// add.push(o);
// }
// }
// });
// gridApi.applyTransaction({
// add,
// remove,
// update,
// });
// },
// });
const coldefs = useMemo<ColDef[]>(() => {
return [
{
field: 'marketId',
valueGetter: ({ data }) => {
const market = markets[data.marketId];
return market.tradableInstrument.instrument.code;
},
sortable: true,
filter: true,
},
{
field: 'status',
},
{
field: 'price',
},
{
field: 'side',
},
{
headerName: 'Created',
colId: 'createdAt',
sort: 'desc', // default sort by latest
valueGetter: ({ data }) => new Date(data.createdAt),
valueFormatter: ({ value }) => getDateTimeFormat().format(value),
},
{
field: 'updatedAt',
},
];
}, [markets]);
if (!Object.keys(markets).length) {
return <div>No markets</div>;
}
if (!assets.length) {
return <div>No assets</div>;
}
return (
<>
<div className="h-full relative">
<OrderListTable
rowData={data as Order[]}
ref={gridRef}
filter={filter}
onGridReady={onGridReady}
onCancel={cancel}
onEdit={setEditOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={onFilterChanged}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
storeKey={storeKey}
suppressLoadingOverlay
suppressNoRowsOverlay
suppressAutoSize
{...bottomPlaceholderProps}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
loading={loading}
error={error}
data={data}
noDataMessage={t('No orders')}
noDataCondition={(data) => !hasData}
reload={reload}
/>
</div>
</div>
{!isReadOnly && hasAmendableOrder && (
<CancelAllOrdersButton onClick={cancelAll} />
)}
{editOrder && (
<OrderEditDialog
isOpen={Boolean(editOrder)}
onChange={(isOpen) => {
if (!isOpen) setEditOrder(null);
}}
order={editOrder}
onSubmit={(fields) => {
if (!editOrder.market) {
return;
}
const orderAmendment = normalizeOrderAmendment(
editOrder,
editOrder.market,
fields.limitPrice,
fields.size
);
const originalOrder: OrderTxUpdateFieldsFragment = {
type: editOrder.type,
id: editOrder.id,
status: editOrder.status,
createdAt: editOrder.createdAt,
size: editOrder.size,
price: editOrder.price,
timeInForce: editOrder.timeInForce,
expiresAt: editOrder.expiresAt,
side: editOrder.side,
marketId: editOrder.market.id,
};
create({ orderAmendment }, originalOrder);
setEditOrder(null);
}}
/>
)}
</>
<AgGridLazy
rowData={orders}
onGridReady={(event) => {
setGridApi(event.api);
}}
style={{ width: '100%', height: '100%' }}
columnDefs={coldefs}
getRowId={(params) => {
return params.data.id;
}}
/>
);
};