Task/Orders data provider for libs/order-list (#173)
* add small/large options for loader * switch order-list lib to use orders data provider * prepare incoming orders for table insertion * delete dupe file, fix test * ignore sentry-cli from netlify build * move sentry/nextjs to dev deps * exclude sentry-cli from netlify
This commit is contained in:
parent
018e5fe139
commit
3b56dcd2b9
@ -37,7 +37,7 @@ export const OrderDialog = ({
|
|||||||
return (
|
return (
|
||||||
<OrderDialogWrapper
|
<OrderDialogWrapper
|
||||||
title="Awaiting network confirmation"
|
title="Awaiting network confirmation"
|
||||||
icon={<Loader />}
|
icon={<Loader size="small" />}
|
||||||
>
|
>
|
||||||
{transaction.hash && (
|
{transaction.hash && (
|
||||||
<p data-testid="tx-hash" className="break-all">
|
<p data-testid="tx-hash" className="break-all">
|
||||||
|
4
libs/order-list/package.json
Normal file
4
libs/order-list/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@vegaprotocol/order-list",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
@ -4,6 +4,26 @@
|
|||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"targets": {
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nrwl/web:rollup",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/order-list",
|
||||||
|
"tsConfig": "libs/order-list/tsconfig.lib.json",
|
||||||
|
"project": "libs/order-list/package.json",
|
||||||
|
"entryFile": "libs/order-list/src/index.ts",
|
||||||
|
"external": ["react/jsx-runtime"],
|
||||||
|
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||||
|
"compiler": "babel",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"glob": "libs/order-list/README.md",
|
||||||
|
"input": ".",
|
||||||
|
"output": "."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"lint": {
|
"lint": {
|
||||||
"executor": "@nrwl/linter:eslint",
|
"executor": "@nrwl/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
export * from './order-list';
|
export * from './lib/order-list';
|
||||||
export * from './order-list-container';
|
export * from './lib/order-list-container';
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { OrderListManager } from './order-list-manager';
|
import { OrderListManager } from './order-list-manager';
|
||||||
import * as useOrdersHook from './use-orders';
|
import * as useDataProviderHook from '@vegaprotocol/react-helpers';
|
||||||
import type { Orders_party_orders } from './__generated__/Orders';
|
import type { Orders_party_orders } from './__generated__/Orders';
|
||||||
|
import * as orderListMock from './order-list';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
jest.mock('./order-list', () => ({
|
jest.mock('./order-list');
|
||||||
OrderList: () => <div>OrderList</div>,
|
|
||||||
}));
|
|
||||||
|
|
||||||
test('Renders a loading state while awaiting orders', () => {
|
test('Renders a loading state while awaiting orders', () => {
|
||||||
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||||
orders: [],
|
data: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: undefined,
|
||||||
});
|
});
|
||||||
render(<OrderListManager partyId="0x123" />);
|
render(<OrderListManager partyId="0x123" />);
|
||||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
@ -19,8 +19,8 @@ test('Renders a loading state while awaiting orders', () => {
|
|||||||
|
|
||||||
test('Renders an error state', () => {
|
test('Renders an error state', () => {
|
||||||
const errorMsg = 'Oops! An Error';
|
const errorMsg = 'Oops! An Error';
|
||||||
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||||
orders: [],
|
data: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: new Error(errorMsg),
|
error: new Error(errorMsg),
|
||||||
});
|
});
|
||||||
@ -31,10 +31,13 @@ test('Renders an error state', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Renders the order list if orders provided', async () => {
|
test('Renders the order list if orders provided', async () => {
|
||||||
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
// @ts-ignore Orderlist is read only but we need to override with the forwardref to
|
||||||
orders: [{ id: '1' } as Orders_party_orders],
|
// avoid warnings about padding refs
|
||||||
|
orderListMock.OrderList = forwardRef(() => <div>OrderList</div>);
|
||||||
|
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||||
|
data: [{ id: '1' } as Orders_party_orders],
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: undefined,
|
||||||
});
|
});
|
||||||
render(<OrderListManager partyId="0x123" />);
|
render(<OrderListManager partyId="0x123" />);
|
||||||
expect(await screen.findByText('OrderList')).toBeInTheDocument();
|
expect(await screen.findByText('OrderList')).toBeInTheDocument();
|
79
libs/order-list/src/lib/order-list-manager.tsx
Normal file
79
libs/order-list/src/lib/order-list-manager.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { OrderList } from './order-list';
|
||||||
|
import type { OrderFields } from './__generated__/OrderFields';
|
||||||
|
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import {
|
||||||
|
ordersDataProvider,
|
||||||
|
prepareIncomingOrders,
|
||||||
|
sortOrders,
|
||||||
|
} from './orders-data-provider';
|
||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { OrderSub_orders } from './__generated__/OrderSub';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
|
interface OrderListManagerProps {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
|
||||||
|
// Apply updates to the table
|
||||||
|
const update = useCallback((delta: OrderSub_orders[]) => {
|
||||||
|
if (!gridRef.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const incoming = prepareIncomingOrders(delta);
|
||||||
|
|
||||||
|
const update: OrderFields[] = [];
|
||||||
|
const add: OrderFields[] = [];
|
||||||
|
|
||||||
|
incoming.forEach((d) => {
|
||||||
|
if (!gridRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowNode = gridRef.current.api.getRowNode(d.id);
|
||||||
|
|
||||||
|
if (rowNode) {
|
||||||
|
if (!isEqual) {
|
||||||
|
update.push(d);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
add.push(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (update.length || add.length) {
|
||||||
|
gridRef.current.api.applyTransactionAsync({
|
||||||
|
update,
|
||||||
|
add,
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data, error, loading } = useDataProvider(
|
||||||
|
ordersDataProvider,
|
||||||
|
update,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
const orders = useMemo(() => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return sortOrders(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={orders}>
|
||||||
|
{(data) => <OrderList ref={gridRef} data={data} />}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
@ -12,7 +12,7 @@ import { OrderList } from './order-list';
|
|||||||
|
|
||||||
test('No orders message shown', async () => {
|
test('No orders message shown', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<OrderList orders={[]} />);
|
render(<OrderList data={[]} />);
|
||||||
});
|
});
|
||||||
expect(screen.getByText('No orders')).toBeInTheDocument();
|
expect(screen.getByText('No orders')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@ -77,7 +77,7 @@ const limitOrder: Orders_party_orders = {
|
|||||||
|
|
||||||
test('Correct columns are rendered', async () => {
|
test('Correct columns are rendered', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<OrderList orders={[marketOrder]} />);
|
render(<OrderList data={[marketOrder]} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const headers = screen.getAllByRole('columnheader');
|
const headers = screen.getAllByRole('columnheader');
|
||||||
@ -87,7 +87,7 @@ test('Correct columns are rendered', async () => {
|
|||||||
'Amount',
|
'Amount',
|
||||||
'Type',
|
'Type',
|
||||||
'Status',
|
'Status',
|
||||||
'Remaining',
|
'Filled',
|
||||||
'Price',
|
'Price',
|
||||||
'Time In Force',
|
'Time In Force',
|
||||||
'Created At',
|
'Created At',
|
||||||
@ -96,7 +96,7 @@ test('Correct columns are rendered', async () => {
|
|||||||
|
|
||||||
test('Correct formatting applied for market order', async () => {
|
test('Correct formatting applied for market order', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<OrderList orders={[marketOrder]} />);
|
render(<OrderList data={[marketOrder]} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
@ -117,7 +117,7 @@ test('Correct formatting applied for market order', async () => {
|
|||||||
|
|
||||||
test('Correct formatting applied for GTT limit order', async () => {
|
test('Correct formatting applied for GTT limit order', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<OrderList orders={[limitOrder]} />);
|
render(<OrderList data={[limitOrder]} />);
|
||||||
});
|
});
|
||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
const expectedValues = [
|
const expectedValues = [
|
||||||
@ -144,7 +144,7 @@ test('Correct formatting applied for a rejected order', async () => {
|
|||||||
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
|
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
|
||||||
};
|
};
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
render(<OrderList orders={[rejectedOrder]} />);
|
render(<OrderList data={[rejectedOrder]} />);
|
||||||
});
|
});
|
||||||
const cells = screen.getAllByRole('gridcell');
|
const cells = screen.getAllByRole('gridcell');
|
||||||
expect(cells[3]).toHaveTextContent(
|
expect(cells[3]).toHaveTextContent(
|
89
libs/order-list/src/lib/order-list.tsx
Normal file
89
libs/order-list/src/lib/order-list.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
|
||||||
|
import type { Orders_party_orders } from './__generated__/Orders';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||||
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { AgGridColumn } from 'ag-grid-react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
interface OrderListProps {
|
||||||
|
data: Orders_party_orders[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderList = forwardRef<AgGridReact, OrderListProps>(
|
||||||
|
({ data }, ref) => {
|
||||||
|
return (
|
||||||
|
<AgGrid
|
||||||
|
ref={ref}
|
||||||
|
rowData={data}
|
||||||
|
overlayNoRowsTemplate="No orders"
|
||||||
|
defaultColDef={{ flex: 1, resizable: true }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
getRowNodeId={(data) => data.id}
|
||||||
|
>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Market"
|
||||||
|
field="market.tradableInstrument.instrument.code"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Amount"
|
||||||
|
field="size"
|
||||||
|
cellClass="font-mono"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||||
|
const prefix = data.side === Side.Buy ? '+' : '-';
|
||||||
|
return prefix + value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn field="type" />
|
||||||
|
<AgGridColumn
|
||||||
|
field="status"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value === OrderStatus.Rejected) {
|
||||||
|
return `${value}: ${data.rejectionReason}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Filled"
|
||||||
|
field="remaining"
|
||||||
|
cellClass="font-mono"
|
||||||
|
valueFormatter={({ data }: ValueFormatterParams) => {
|
||||||
|
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
field="price"
|
||||||
|
cellClass="font-mono"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (data.type === 'Market') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return formatNumber(value, data.market.decimalPlaces);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
field="timeInForce"
|
||||||
|
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value === OrderTimeInForce.GTT && data.expiresAt) {
|
||||||
|
const expiry = getDateTimeFormat().format(
|
||||||
|
new Date(data.expiresAt)
|
||||||
|
);
|
||||||
|
return `${value}: ${expiry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
field="createdAt"
|
||||||
|
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||||
|
return getDateTimeFormat().format(new Date(value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AgGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
104
libs/order-list/src/lib/orders-data-provider.ts
Normal file
104
libs/order-list/src/lib/orders-data-provider.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { OrderFields } from './__generated__/OrderFields';
|
||||||
|
import type { Orders, Orders_party_orders } from './__generated__/Orders';
|
||||||
|
import type { OrderSub } from './__generated__/OrderSub';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import uniqBy from 'lodash/uniqBy';
|
||||||
|
|
||||||
|
const ORDER_FRAGMENT = gql`
|
||||||
|
fragment OrderFields on Order {
|
||||||
|
id
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
decimalPlaces
|
||||||
|
tradableInstrument {
|
||||||
|
instrument {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type
|
||||||
|
side
|
||||||
|
size
|
||||||
|
status
|
||||||
|
rejectionReason
|
||||||
|
price
|
||||||
|
timeInForce
|
||||||
|
remaining
|
||||||
|
expiresAt
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ORDERS_QUERY = gql`
|
||||||
|
${ORDER_FRAGMENT}
|
||||||
|
query Orders($partyId: ID!) {
|
||||||
|
party(id: $partyId) {
|
||||||
|
id
|
||||||
|
orders {
|
||||||
|
...OrderFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ORDERS_SUB = gql`
|
||||||
|
${ORDER_FRAGMENT}
|
||||||
|
subscription OrderSub($partyId: ID!) {
|
||||||
|
orders(partyId: $partyId) {
|
||||||
|
...OrderFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// A single update can contain the same order with multiple updates, so we need to find
|
||||||
|
// the latest version of the order and only update using that
|
||||||
|
export const sortOrders = (orders: OrderFields[]) => {
|
||||||
|
return orderBy(
|
||||||
|
orders,
|
||||||
|
(o) => {
|
||||||
|
if (!o.updatedAt) return new Date(o.createdAt).getTime();
|
||||||
|
return new Date(o.updatedAt).getTime();
|
||||||
|
},
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uniqOrders = (orders: OrderFields[]) => {
|
||||||
|
return uniqBy(orders, 'id');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareIncomingOrders = (delta: OrderFields[]) => {
|
||||||
|
const sortedOrders = sortOrders(delta);
|
||||||
|
const incoming = uniqOrders(sortedOrders);
|
||||||
|
return incoming;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = (draft: OrderFields[], delta: OrderFields[]) => {
|
||||||
|
const incoming = prepareIncomingOrders(delta);
|
||||||
|
|
||||||
|
// Add or update incoming orders
|
||||||
|
incoming.forEach((order) => {
|
||||||
|
const index = draft.findIndex((o) => o.id === order.id);
|
||||||
|
if (index === -1) {
|
||||||
|
draft.unshift(order);
|
||||||
|
} else {
|
||||||
|
draft[index] = order;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = (responseData: Orders): Orders_party_orders[] | null =>
|
||||||
|
responseData?.party?.orders || null;
|
||||||
|
const getDelta = (subscriptionData: OrderSub) => subscriptionData.orders || [];
|
||||||
|
|
||||||
|
export const ordersDataProvider = makeDataProvider(
|
||||||
|
ORDERS_QUERY,
|
||||||
|
ORDERS_SUB,
|
||||||
|
update,
|
||||||
|
getData,
|
||||||
|
getDelta
|
||||||
|
);
|
@ -1,18 +0,0 @@
|
|||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useOrders } from './use-orders';
|
|
||||||
import { OrderList } from './order-list';
|
|
||||||
import type { OrderFields } from './__generated__/OrderFields';
|
|
||||||
|
|
||||||
interface OrderListManagerProps {
|
|
||||||
partyId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
|
||||||
const { orders, loading, error } = useOrders(partyId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AsyncRenderer<OrderFields[]> loading={loading} error={error} data={orders}>
|
|
||||||
{(data) => <OrderList orders={data} />}
|
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,96 +0,0 @@
|
|||||||
import { OrderTimeInForce, OrderStatus, Side } from '@vegaprotocol/types';
|
|
||||||
import type { Orders_party_orders } from './__generated__/Orders';
|
|
||||||
import {
|
|
||||||
formatNumber,
|
|
||||||
getDateTimeFormat,
|
|
||||||
t,
|
|
||||||
useApplyGridTransaction,
|
|
||||||
} from '@vegaprotocol/react-helpers';
|
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { GridApi, ValueFormatterParams } from 'ag-grid-community';
|
|
||||||
import { AgGridColumn } from 'ag-grid-react';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface OrderListProps {
|
|
||||||
orders: Orders_party_orders[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const OrderList = ({ orders }: OrderListProps) => {
|
|
||||||
// Store initial orders for initial table data set, further updates
|
|
||||||
// are handled by the effect below
|
|
||||||
const [initialOrders] = useState(orders);
|
|
||||||
const gridApi = useRef<GridApi | null>(null);
|
|
||||||
useApplyGridTransaction<Orders_party_orders>(orders, gridApi.current);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgGrid
|
|
||||||
rowData={initialOrders}
|
|
||||||
overlayNoRowsTemplate="No orders"
|
|
||||||
defaultColDef={{ flex: 1, resizable: true }}
|
|
||||||
style={{ width: '100%', height: '100%' }}
|
|
||||||
onGridReady={(params) => {
|
|
||||||
gridApi.current = params.api;
|
|
||||||
}}
|
|
||||||
getRowNodeId={(data) => data.id}
|
|
||||||
>
|
|
||||||
<AgGridColumn
|
|
||||||
headerName={t('Market')}
|
|
||||||
field="market.tradableInstrument.instrument.code"
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
headerName={t('Amount')}
|
|
||||||
field="size"
|
|
||||||
cellClass="font-mono"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
|
||||||
const prefix = data.side === Side.Buy ? '+' : '-';
|
|
||||||
return prefix + value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn field="type" />
|
|
||||||
<AgGridColumn
|
|
||||||
field="status"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
|
||||||
if (value === OrderStatus.Rejected) {
|
|
||||||
return `${value}: ${data.rejectionReason}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
field="remaining"
|
|
||||||
cellClass="font-mono"
|
|
||||||
valueFormatter={({ data }: ValueFormatterParams) => {
|
|
||||||
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
field="price"
|
|
||||||
cellClass="font-mono"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
|
||||||
if (data.type === 'Market') {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
return formatNumber(value, data.market.decimalPlaces);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
field="timeInForce"
|
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) => {
|
|
||||||
if (value === OrderTimeInForce.GTT && data.expiresAt) {
|
|
||||||
const expiry = getDateTimeFormat().format(new Date(data.expiresAt));
|
|
||||||
return `${value}: ${expiry}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AgGridColumn
|
|
||||||
field="createdAt"
|
|
||||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
|
||||||
return getDateTimeFormat().format(new Date(value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AgGrid>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,138 +0,0 @@
|
|||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { renderHook } from '@testing-library/react-hooks';
|
|
||||||
import type { OrderFields } from './__generated__/OrderFields';
|
|
||||||
import type { Orders } from './__generated__/Orders';
|
|
||||||
import type { OrderSub } from './__generated__/OrderSub';
|
|
||||||
import {
|
|
||||||
OrderStatus,
|
|
||||||
Side,
|
|
||||||
OrderType,
|
|
||||||
OrderTimeInForce,
|
|
||||||
} from '@vegaprotocol/types';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import { ORDERS_QUERY, ORDERS_SUB, useOrders } from './use-orders';
|
|
||||||
|
|
||||||
const partyId = '0x123';
|
|
||||||
|
|
||||||
function generateOrder(order?: Partial<OrderFields>): OrderFields {
|
|
||||||
return {
|
|
||||||
__typename: 'Order',
|
|
||||||
id: '1',
|
|
||||||
market: {
|
|
||||||
__typename: 'Market',
|
|
||||||
id: 'market-id',
|
|
||||||
name: 'market-name',
|
|
||||||
decimalPlaces: 0,
|
|
||||||
tradableInstrument: {
|
|
||||||
__typename: 'TradableInstrument',
|
|
||||||
instrument: {
|
|
||||||
__typename: 'Instrument',
|
|
||||||
code: 'instrument-code',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
type: OrderType.Market,
|
|
||||||
side: Side.Buy,
|
|
||||||
size: '10',
|
|
||||||
status: OrderStatus.Active,
|
|
||||||
rejectionReason: null,
|
|
||||||
price: '',
|
|
||||||
timeInForce: OrderTimeInForce.GTC,
|
|
||||||
remaining: '10',
|
|
||||||
createdAt: '2022-01-01T00:00:00',
|
|
||||||
updatedAt: null,
|
|
||||||
expiresAt: null,
|
|
||||||
...order,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup(mocks: MockedResponse[] = [], id: string | null) {
|
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
||||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
|
||||||
);
|
|
||||||
return renderHook(() => useOrders(id as string), { wrapper });
|
|
||||||
}
|
|
||||||
|
|
||||||
test('Fetches and subscribes to orders and merges appropriately', async () => {
|
|
||||||
const order = generateOrder();
|
|
||||||
const mockOrderQuery: MockedResponse<Orders> = {
|
|
||||||
request: {
|
|
||||||
query: ORDERS_QUERY,
|
|
||||||
variables: { partyId },
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
party: {
|
|
||||||
__typename: 'Party',
|
|
||||||
id: partyId,
|
|
||||||
orders: [order],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedOrder = generateOrder({
|
|
||||||
id: '1',
|
|
||||||
remaining: '5',
|
|
||||||
updatedAt: '2022-01-01T00:01:00',
|
|
||||||
});
|
|
||||||
const newOrder = generateOrder({
|
|
||||||
id: '2',
|
|
||||||
createdAt: '2022-01-01T01:00:00',
|
|
||||||
});
|
|
||||||
const mockOrderSub: MockedResponse<OrderSub> = {
|
|
||||||
request: {
|
|
||||||
query: ORDERS_SUB,
|
|
||||||
variables: { partyId },
|
|
||||||
},
|
|
||||||
result: {
|
|
||||||
data: {
|
|
||||||
orders: [updatedOrder, newOrder],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
delay: 100,
|
|
||||||
};
|
|
||||||
const { result, waitForNextUpdate } = setup(
|
|
||||||
[mockOrderQuery, mockOrderSub],
|
|
||||||
partyId
|
|
||||||
);
|
|
||||||
expect(result.current.loading).toBe(true);
|
|
||||||
expect(result.current.error).toBe(null);
|
|
||||||
await waitForNextUpdate();
|
|
||||||
expect(result.current.orders).toEqual([order]);
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
await waitForNextUpdate();
|
|
||||||
expect(result.current.orders).toEqual([newOrder, updatedOrder]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Returns an error if fetch fails', async () => {
|
|
||||||
const error = new Error('Something failed');
|
|
||||||
const mockFailedOrderQuery: MockedResponse<Orders> = {
|
|
||||||
request: {
|
|
||||||
query: ORDERS_QUERY,
|
|
||||||
variables: { partyId },
|
|
||||||
},
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
const { result, waitForNextUpdate } = setup([mockFailedOrderQuery], partyId);
|
|
||||||
expect(result.current.loading).toBe(true);
|
|
||||||
expect(result.current.error).toBe(null);
|
|
||||||
await waitForNextUpdate();
|
|
||||||
expect(result.current.error).toEqual(error);
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('No queries are made if no pubkey provided', () => {
|
|
||||||
const mockQuery: MockedResponse<Orders> = {
|
|
||||||
request: {
|
|
||||||
query: ORDERS_QUERY,
|
|
||||||
variables: { partyId },
|
|
||||||
},
|
|
||||||
newData: jest.fn(),
|
|
||||||
};
|
|
||||||
const { result } = setup([mockQuery], null);
|
|
||||||
expect(mockQuery.newData).not.toBeCalled();
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
expect(result.current.error).toBe(null);
|
|
||||||
});
|
|
@ -1,138 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { gql, useApolloClient } from '@apollo/client';
|
|
||||||
import uniqBy from 'lodash/uniqBy';
|
|
||||||
import orderBy from 'lodash/orderBy';
|
|
||||||
import type { Orders, OrdersVariables } from './__generated__/Orders';
|
|
||||||
import type { OrderSub, OrderSubVariables } from './__generated__/OrderSub';
|
|
||||||
import type { OrderFields } from './__generated__/OrderFields';
|
|
||||||
|
|
||||||
const ORDER_FRAGMENT = gql`
|
|
||||||
fragment OrderFields on Order {
|
|
||||||
id
|
|
||||||
market {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
decimalPlaces
|
|
||||||
tradableInstrument {
|
|
||||||
instrument {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type
|
|
||||||
side
|
|
||||||
size
|
|
||||||
status
|
|
||||||
rejectionReason
|
|
||||||
price
|
|
||||||
timeInForce
|
|
||||||
remaining
|
|
||||||
expiresAt
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ORDERS_QUERY = gql`
|
|
||||||
${ORDER_FRAGMENT}
|
|
||||||
query Orders($partyId: ID!) {
|
|
||||||
party(id: $partyId) {
|
|
||||||
id
|
|
||||||
orders {
|
|
||||||
...OrderFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ORDERS_SUB = gql`
|
|
||||||
${ORDER_FRAGMENT}
|
|
||||||
subscription OrderSub($partyId: ID!) {
|
|
||||||
orders(partyId: $partyId) {
|
|
||||||
...OrderFields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface UseOrders {
|
|
||||||
orders: OrderFields[];
|
|
||||||
error: Error | null;
|
|
||||||
loading: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useOrders = (partyId: string): UseOrders => {
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [orders, setOrders] = useState<OrderFields[]>([]);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const mergeOrders = useCallback((update: OrderFields[]) => {
|
|
||||||
// A subscription payload can contain multiple updates for a single order so we need to first
|
|
||||||
// sort them by updatedAt (or createdAt if the order hasn't been updated) with the newest first,
|
|
||||||
// then use uniqBy, which selects the first occuring order for an id to ensure we only get the latest order
|
|
||||||
setOrders((curr) => {
|
|
||||||
const sorted = orderBy(
|
|
||||||
[...curr, ...update],
|
|
||||||
(o) => {
|
|
||||||
if (!o.updatedAt) return new Date(o.createdAt).getTime();
|
|
||||||
return new Date(o.updatedAt).getTime();
|
|
||||||
},
|
|
||||||
'desc'
|
|
||||||
);
|
|
||||||
return uniqBy(sorted, 'id');
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Make initial fetch
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchOrders = async () => {
|
|
||||||
if (!partyId) return;
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await client.query<Orders, OrdersVariables>({
|
|
||||||
query: ORDERS_QUERY,
|
|
||||||
variables: { partyId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.data.party?.orders?.length) return;
|
|
||||||
|
|
||||||
mergeOrders(res.data.party.orders);
|
|
||||||
} catch (err) {
|
|
||||||
setError(
|
|
||||||
err instanceof Error ? err : new Error('Something went wrong')
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchOrders();
|
|
||||||
}, [mergeOrders, partyId, client]);
|
|
||||||
|
|
||||||
// Start subscription
|
|
||||||
useEffect(() => {
|
|
||||||
if (!partyId) return;
|
|
||||||
|
|
||||||
const sub = client
|
|
||||||
.subscribe<OrderSub, OrderSubVariables>({
|
|
||||||
query: ORDERS_SUB,
|
|
||||||
variables: { partyId },
|
|
||||||
})
|
|
||||||
.subscribe(({ data }) => {
|
|
||||||
if (!data?.orders) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mergeOrders(data.orders);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (sub) {
|
|
||||||
sub.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [client, partyId, mergeOrders]);
|
|
||||||
|
|
||||||
return { orders, error, loading };
|
|
||||||
};
|
|
@ -8,8 +8,7 @@ interface AsyncRendererProps<T> {
|
|||||||
children: (data: T) => ReactNode;
|
children: (data: T) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
export function AsyncRenderer<T = object>({
|
||||||
export function AsyncRenderer<T = any>({
|
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
data,
|
data,
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export const Loader = () => {
|
interface LoaderProps {
|
||||||
|
size?: 'small' | 'large';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loader = ({ size = 'large' }: LoaderProps) => {
|
||||||
const [, forceRender] = useState(false);
|
const [, forceRender] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -11,13 +15,17 @@ export const Loader = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const wrapperClasses = size === 'small' ? 'w-[15px] h-[15px]' : 'w-64 h-64';
|
||||||
|
const gridItemClasses = size === 'small' ? 'w-[5px] h-[5px]' : 'w-16 h-16';
|
||||||
|
const items = size === 'small' ? 9 : 16;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center" data-testid="splash-loader">
|
<div className="flex flex-col items-center" data-testid="splash-loader">
|
||||||
<div className="w-64 h-64 flex flex-wrap">
|
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||||
{new Array(16).fill(null).map((_, i) => {
|
{new Array(items).fill(null).map((_, i) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-16 h-16 dark:bg-white bg-black"
|
className={`${gridItemClasses} dark:bg-white bg-black`}
|
||||||
key={i}
|
key={i}
|
||||||
style={{
|
style={{
|
||||||
opacity: Math.random() > 0.75 ? 1 : 0,
|
opacity: Math.random() > 0.75 ? 1 : 0,
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
[build]
|
[build]
|
||||||
ignore = "node ignore-netlify-build.js"
|
ignore = "node ignore-netlify-build.js"
|
||||||
|
|
||||||
|
[functions]
|
||||||
|
included_files = ["!node_modules/@sentry/cli/sentry-cli"]
|
Loading…
Reference in New Issue
Block a user