Merge remote-tracking branch 'origin/master' into feat/127-generalised-stats-page
# Conflicts: # apps/stats/jest.config.js # libs/network-stats/src/config/types.ts # tsconfig.base.json # workspace.json
This commit is contained in:
commit
af5832fa9e
@ -1,4 +1,23 @@
|
|||||||
Feature: Home page
|
Feature: Home page
|
||||||
|
|
||||||
Scenario: Visit Home page
|
Scenario Outline: Succesfful search for specific id by <IdType>
|
||||||
Given I am on the homepage
|
Given I am on the homepage
|
||||||
|
When I search for '<Id>'
|
||||||
|
Then I am redirected to page containing id '<Id>'
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| IdType | Id |
|
||||||
|
| Block Id | 973624 |
|
||||||
|
| Tx Hash | 9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8 |
|
||||||
|
| Tx Id | 0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898 |
|
||||||
|
|
||||||
|
Scenario Outline: Error message displayed when invalid search by <invalidType>
|
||||||
|
Given I am on the homepage
|
||||||
|
When I search for '<Id>'
|
||||||
|
Then search error message "<errorMessage>" is displayed
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| invalidType | Id | errorMessage |
|
||||||
|
| wrong string length | 9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8 | Something doesn't look right |
|
||||||
|
| invalid hash | 9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8 | Transaction is not hexadecimal |
|
||||||
|
| empty search | | Search required |
|
||||||
|
@ -9,7 +9,9 @@ export default class BasePage {
|
|||||||
networkParametersUrl = '/network-parameters';
|
networkParametersUrl = '/network-parameters';
|
||||||
validatorsUrl = '/validators';
|
validatorsUrl = '/validators';
|
||||||
blockExplorerHeader = 'explorer-header';
|
blockExplorerHeader = 'explorer-header';
|
||||||
searchField = 'search-input';
|
searchField = 'search';
|
||||||
|
searchButton = 'search-button';
|
||||||
|
searchError = 'search-error';
|
||||||
|
|
||||||
navigateToTxs() {
|
navigateToTxs() {
|
||||||
cy.get(`a[href='${this.transactionsUrl}']`).click();
|
cy.get(`a[href='${this.transactionsUrl}']`).click();
|
||||||
@ -48,7 +50,17 @@ export default class BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
search(searchText) {
|
search(searchText) {
|
||||||
cy.getByTestId(this.searchField).type(searchText);
|
if (searchText) {
|
||||||
|
cy.getByTestId(this.searchField).type(searchText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clickSearch() {
|
||||||
|
cy.getByTestId(this.searchButton).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
validateUrl(expectedUrl) {
|
||||||
|
cy.url().should('include', expectedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
validateSearchDisplayed() {
|
validateSearchDisplayed() {
|
||||||
@ -59,6 +71,10 @@ export default class BasePage {
|
|||||||
cy.getByTestId(this.searchField).should('be.visible');
|
cy.getByTestId(this.searchField).should('be.visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSearchErrorDisplayed(errorMessage) {
|
||||||
|
cy.getByTestId(this.searchError).should('have.text', errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
validateBlockDataDisplayed(headerTestId) {
|
validateBlockDataDisplayed(headerTestId) {
|
||||||
cy.getByTestId(headerTestId).then(($assetHeaders) => {
|
cy.getByTestId(headerTestId).then(($assetHeaders) => {
|
||||||
const headersAmount = parseInt($assetHeaders.length);
|
const headersAmount = parseInt($assetHeaders.length);
|
||||||
@ -68,7 +84,7 @@ export default class BasePage {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cy.get('.language-json')
|
cy.get('.language-json')
|
||||||
.each(($asset, index, $list) => {
|
.each(($asset) => {
|
||||||
expect($asset).to.not.be.empty;
|
expect($asset).to.not.be.empty;
|
||||||
})
|
})
|
||||||
.then(($list) => {
|
.then(($list) => {
|
||||||
|
@ -1,5 +1,20 @@
|
|||||||
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
|
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
|
||||||
|
import BasePage from '../pages/base-page';
|
||||||
|
const basePage = new BasePage();
|
||||||
|
|
||||||
Given('I am on the homepage', () => {
|
Given('I am on the homepage', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
When('I search for {string}', (searchText) => {
|
||||||
|
basePage.search(searchText);
|
||||||
|
basePage.clickSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('I am redirected to page containing id {string}', (expectedUrl) => {
|
||||||
|
basePage.validateUrl(expectedUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
Then('search error message {string} is displayed', (expectedErrorMsg) => {
|
||||||
|
basePage.validateSearchErrorDisplayed(expectedErrorMsg);
|
||||||
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps';
|
import { Then, When } from 'cypress-cucumber-preprocessor/steps';
|
||||||
import NetworkPage from '../pages/network-page';
|
import NetworkPage from '../pages/network-page';
|
||||||
const networkPage = new NetworkPage();
|
const networkPage = new NetworkPage();
|
||||||
|
|
||||||
|
@ -7,4 +7,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
coverageDirectory: '../../coverage/apps/stats',
|
coverageDirectory: '../../coverage/apps/stats',
|
||||||
|
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||||
};
|
};
|
||||||
|
27
apps/trading/components/async-renderer/async-renderer.tsx
Normal file
27
apps/trading/components/async-renderer/async-renderer.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface AsyncRendererProps<T> {
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | undefined;
|
||||||
|
data: T;
|
||||||
|
children: (data: T) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export function AsyncRenderer<T = any>({
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
children,
|
||||||
|
}: AsyncRendererProps<T>) {
|
||||||
|
if (error) {
|
||||||
|
return <Splash>Something went wrong: {error.message}</Splash>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Splash>Loading...</Splash>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children(data)}</>;
|
||||||
|
}
|
1
apps/trading/components/async-renderer/index.ts
Normal file
1
apps/trading/components/async-renderer/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './async-renderer';
|
@ -4,10 +4,7 @@ import {
|
|||||||
TransactionState,
|
TransactionState,
|
||||||
VegaTxStatus,
|
VegaTxStatus,
|
||||||
} from '../../hooks/use-vega-transaction';
|
} from '../../hooks/use-vega-transaction';
|
||||||
import {
|
import { OrderEvent_busEvents_event_Order } from '@vegaprotocol/graphql';
|
||||||
OrderEvent_busEvents_event_Order,
|
|
||||||
OrderStatus,
|
|
||||||
} from '@vegaprotocol/graphql';
|
|
||||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
import { formatNumber } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
interface OrderDialogProps {
|
interface OrderDialogProps {
|
||||||
|
1
apps/trading/components/order-list-container/index.ts
Normal file
1
apps/trading/components/order-list-container/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './order-list-container';
|
@ -0,0 +1,41 @@
|
|||||||
|
import { OrderListContainer } from './order-list-container';
|
||||||
|
import * as useOrdersHook from '../../hooks/use-orders';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { Orders_party_orders } from '@vegaprotocol/graphql';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/order-list', () => ({
|
||||||
|
OrderList: () => <div>OrderList</div>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
test('Renders a loading state while awaiting orders', () => {
|
||||||
|
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
||||||
|
orders: [],
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
render(<OrderListContainer />);
|
||||||
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders an error state', () => {
|
||||||
|
const errorMsg = 'Oops! An Error';
|
||||||
|
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
||||||
|
orders: [],
|
||||||
|
loading: false,
|
||||||
|
error: new Error(errorMsg),
|
||||||
|
});
|
||||||
|
render(<OrderListContainer />);
|
||||||
|
expect(
|
||||||
|
screen.getByText(`Something went wrong: ${errorMsg}`)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders the order list if orders provided', () => {
|
||||||
|
jest.spyOn(useOrdersHook, 'useOrders').mockReturnValue({
|
||||||
|
orders: [{ id: '1' } as Orders_party_orders],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
render(<OrderListContainer />);
|
||||||
|
expect(screen.getByText('OrderList')).toBeInTheDocument();
|
||||||
|
});
|
@ -0,0 +1,14 @@
|
|||||||
|
import { useOrders } from '../../hooks/use-orders';
|
||||||
|
import { OrderList } from '@vegaprotocol/order-list';
|
||||||
|
import { AsyncRenderer } from '../async-renderer';
|
||||||
|
import { OrderFields } from '@vegaprotocol/graphql';
|
||||||
|
|
||||||
|
export const OrderListContainer = () => {
|
||||||
|
const { orders, loading, error } = useOrders();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer<OrderFields[]> loading={loading} error={error} data={orders}>
|
||||||
|
{(data) => <OrderList orders={data} />}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { OperationVariables, QueryHookOptions, useQuery } from '@apollo/client';
|
import { OperationVariables, QueryHookOptions, useQuery } from '@apollo/client';
|
||||||
import { DocumentNode } from 'graphql';
|
import { DocumentNode } from 'graphql';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '../async-renderer';
|
||||||
|
|
||||||
interface PageQueryContainerProps<TData, TVariables> {
|
interface PageQueryContainerProps<TData, TVariables> {
|
||||||
query: DocumentNode;
|
query: DocumentNode;
|
||||||
@ -16,13 +16,13 @@ export const PageQueryContainer = <TData, TVariables = OperationVariables>({
|
|||||||
}: PageQueryContainerProps<TData, TVariables>) => {
|
}: PageQueryContainerProps<TData, TVariables>) => {
|
||||||
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
|
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
|
||||||
|
|
||||||
if (loading || !data) {
|
return (
|
||||||
return <Splash>Loading...</Splash>;
|
<AsyncRenderer<TData>
|
||||||
}
|
loading={loading || Boolean(!data)}
|
||||||
|
error={error}
|
||||||
if (error) {
|
data={data}
|
||||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
>
|
||||||
}
|
{(data) => children(data)}
|
||||||
|
</AsyncRenderer>
|
||||||
return <>{children(data)}</>;
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { gql, useApolloClient } from '@apollo/client';
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
import { singletonHook } from 'react-singleton-hook';
|
|
||||||
import {
|
import {
|
||||||
Markets,
|
Markets,
|
||||||
Markets_markets,
|
Markets_markets,
|
||||||
@ -56,7 +55,13 @@ const MARKET_DATA_SUB = gql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const useMarketsImpl = () => {
|
interface UseMarkets {
|
||||||
|
markets: Markets_markets[];
|
||||||
|
error: Error | null;
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMarkets = (): UseMarkets => {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const [markets, setMarkets] = useState<Markets_markets[]>([]);
|
const [markets, setMarkets] = useState<Markets_markets[]>([]);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
@ -121,11 +126,3 @@ export const useMarketsImpl = () => {
|
|||||||
|
|
||||||
return { markets, error, loading };
|
return { markets, error, loading };
|
||||||
};
|
};
|
||||||
|
|
||||||
const initial = {
|
|
||||||
markets: [],
|
|
||||||
error: null,
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useMarkets = singletonHook(initial, useMarketsImpl);
|
|
||||||
|
165
apps/trading/hooks/use-orders.spec.tsx
Normal file
165
apps/trading/hooks/use-orders.spec.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import {
|
||||||
|
OrderFields,
|
||||||
|
Orders,
|
||||||
|
OrderStatus,
|
||||||
|
OrderSub,
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderType,
|
||||||
|
Side,
|
||||||
|
} from '@vegaprotocol/graphql';
|
||||||
|
import {
|
||||||
|
VegaKeyExtended,
|
||||||
|
VegaWalletContext,
|
||||||
|
VegaWalletContextShape,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { ORDERS_QUERY, ORDERS_SUB, useOrders } from './use-orders';
|
||||||
|
|
||||||
|
const keypair = { pub: '0x123' } as VegaKeyExtended;
|
||||||
|
const defaultWalletContext = {
|
||||||
|
keypair,
|
||||||
|
keypairs: [keypair],
|
||||||
|
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
|
||||||
|
connect: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
selectPublicKey: jest.fn(),
|
||||||
|
connector: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
context?: Partial<VegaWalletContextShape>,
|
||||||
|
mocks: MockedResponse[] = []
|
||||||
|
) {
|
||||||
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<VegaWalletContext.Provider
|
||||||
|
value={{ ...defaultWalletContext, ...context }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
return renderHook(() => useOrders(), { wrapper });
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Fetches and subscribes to orders and merges appropriately', async () => {
|
||||||
|
const order = generateOrder();
|
||||||
|
const mockOrderQuery: MockedResponse<Orders> = {
|
||||||
|
request: {
|
||||||
|
query: ORDERS_QUERY,
|
||||||
|
variables: { partyId: keypair.pub },
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: keypair.pub,
|
||||||
|
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: keypair.pub },
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
orders: [updatedOrder, newOrder],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
delay: 100,
|
||||||
|
};
|
||||||
|
const { result, waitForNextUpdate } = setup(defaultWalletContext, [
|
||||||
|
mockOrderQuery,
|
||||||
|
mockOrderSub,
|
||||||
|
]);
|
||||||
|
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: keypair.pub },
|
||||||
|
},
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
const { result, waitForNextUpdate } = setup(defaultWalletContext, [
|
||||||
|
mockFailedOrderQuery,
|
||||||
|
]);
|
||||||
|
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: keypair.pub },
|
||||||
|
},
|
||||||
|
newData: jest.fn(),
|
||||||
|
};
|
||||||
|
const { result } = setup(
|
||||||
|
{ ...defaultWalletContext, keypair: null, keypairs: [] },
|
||||||
|
[mockQuery]
|
||||||
|
);
|
||||||
|
expect(mockQuery.newData).not.toBeCalled();
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
expect(result.current.error).toBe(null);
|
||||||
|
});
|
140
apps/trading/hooks/use-orders.ts
Normal file
140
apps/trading/hooks/use-orders.ts
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { gql, useApolloClient } from '@apollo/client';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
OrderSub,
|
||||||
|
OrderSubVariables,
|
||||||
|
Orders,
|
||||||
|
OrdersVariables,
|
||||||
|
OrderFields,
|
||||||
|
} from '@vegaprotocol/graphql';
|
||||||
|
import uniqBy from 'lodash/uniqBy';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
|
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 = (): UseOrders => {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { keypair } = useVegaWallet();
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
const uniq = uniqBy(sorted, 'id');
|
||||||
|
return uniq;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Make initial fetch
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchOrders = async () => {
|
||||||
|
if (!keypair?.pub) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await client.query<Orders, OrdersVariables>({
|
||||||
|
query: ORDERS_QUERY,
|
||||||
|
variables: { partyId: keypair.pub },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.data.party?.orders.length) return;
|
||||||
|
|
||||||
|
mergeOrders(res.data.party.orders);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchOrders();
|
||||||
|
}, [mergeOrders, keypair, client]);
|
||||||
|
|
||||||
|
// Start subscription
|
||||||
|
useEffect(() => {
|
||||||
|
if (!keypair?.pub) return;
|
||||||
|
|
||||||
|
const sub = client
|
||||||
|
.subscribe<OrderSub, OrderSubVariables>({
|
||||||
|
query: ORDERS_SUB,
|
||||||
|
variables: { partyId: keypair.pub },
|
||||||
|
})
|
||||||
|
.subscribe(({ data }) => {
|
||||||
|
mergeOrders(data.orders);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (sub) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [client, keypair, mergeOrders]);
|
||||||
|
|
||||||
|
return { orders, error, loading };
|
||||||
|
};
|
@ -7,4 +7,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
coverageDirectory: '../../coverage/apps/trading',
|
coverageDirectory: '../../coverage/apps/trading',
|
||||||
|
setupFilesAfterEnv: ['./setup-tests.ts'],
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,6 @@ import { ThemeContext } from '@vegaprotocol/react-helpers';
|
|||||||
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
|
import { VegaConnectDialog, VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||||
import { Connectors } from '../lib/connectors';
|
import { Connectors } from '../lib/connectors';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { SingletonHooksContainer } from 'react-singleton-hook';
|
|
||||||
import { createClient } from '../lib/apollo-client';
|
import { createClient } from '../lib/apollo-client';
|
||||||
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
|
import { ThemeSwitcher } from '@vegaprotocol/ui-toolkit';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
@ -35,7 +34,6 @@ function VegaTradingApp({ Component, pageProps }: AppProps) {
|
|||||||
<ThemeContext.Provider value={theme}>
|
<ThemeContext.Provider value={theme}>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<VegaWalletProvider>
|
<VegaWalletProvider>
|
||||||
<SingletonHooksContainer />
|
|
||||||
<AppLoader>
|
<AppLoader>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Welcome to trading!</title>
|
<title>Welcome to trading!</title>
|
||||||
|
@ -3,7 +3,7 @@ import { Market, MarketVariables } from '@vegaprotocol/graphql';
|
|||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { PageQueryContainer } from '../../components/page-query-container';
|
import { PageQueryContainer } from '../../components/page-query-container';
|
||||||
import { TradeGrid, TradePanels } from './trade-grid';
|
import { TradeGrid, TradePanels } from './trade-grid';
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export const GridTabs = ({ children, group }: GridTabsProps) => {
|
|||||||
{Children.map(children, (child) => {
|
{Children.map(children, (child) => {
|
||||||
if (!isValidElement(child)) return null;
|
if (!isValidElement(child)) return null;
|
||||||
return (
|
return (
|
||||||
<Tabs.Content value={child.props.name}>
|
<Tabs.Content value={child.props.name} className="h-full">
|
||||||
{child.props.children}
|
{child.props.children}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
);
|
);
|
||||||
|
@ -2,27 +2,23 @@ import { Markets } from '@vegaprotocol/graphql';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { MarketListTable } from '@vegaprotocol/market-list';
|
import { MarketListTable } from '@vegaprotocol/market-list';
|
||||||
import { useMarkets } from '../../hooks/use-markets';
|
import { useMarkets } from '../../hooks/use-markets';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '../../components/async-renderer';
|
||||||
|
|
||||||
const Markets = () => {
|
const Markets = () => {
|
||||||
const { pathname, push } = useRouter();
|
const { pathname, push } = useRouter();
|
||||||
const { markets, error, loading } = useMarkets();
|
const { markets, error, loading } = useMarkets();
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Splash>Loading...</Splash>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarketListTable
|
<AsyncRenderer loading={loading} error={error} data={markets}>
|
||||||
markets={markets}
|
{(data) => (
|
||||||
onRowClicked={(id) =>
|
<MarketListTable
|
||||||
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
markets={data}
|
||||||
}
|
onRowClicked={(id) =>
|
||||||
/>
|
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,10 +4,10 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { useState, ReactNode } from 'react';
|
import { useState, ReactNode } from 'react';
|
||||||
import { GridTab, GridTabs } from './grid-tabs';
|
import { GridTab, GridTabs } from './grid-tabs';
|
||||||
import { DealTicketContainer } from '../../components/deal-ticket-container';
|
import { DealTicketContainer } from '../../components/deal-ticket-container';
|
||||||
|
import { OrderListContainer } from '../..//components/order-list-container';
|
||||||
|
|
||||||
const Chart = () => <div>TODO: Chart</div>;
|
const Chart = () => <div>TODO: Chart</div>;
|
||||||
const Orderbook = () => <div>TODO: Orderbook</div>;
|
const Orderbook = () => <div>TODO: Orderbook</div>;
|
||||||
const Orders = () => <div>TODO: Orders</div>;
|
|
||||||
const Positions = () => <div>TODO: Positions</div>;
|
const Positions = () => <div>TODO: Positions</div>;
|
||||||
const Collateral = () => <div>TODO: Collateral</div>;
|
const Collateral = () => <div>TODO: Collateral</div>;
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ const TradingViews = {
|
|||||||
chart: Chart,
|
chart: Chart,
|
||||||
ticket: DealTicketContainer,
|
ticket: DealTicketContainer,
|
||||||
orderbook: Orderbook,
|
orderbook: Orderbook,
|
||||||
orders: Orders,
|
orders: OrderListContainer,
|
||||||
positions: Positions,
|
positions: Positions,
|
||||||
collateral: Collateral,
|
collateral: Collateral,
|
||||||
};
|
};
|
||||||
|
1
apps/trading/setup-tests.ts
Normal file
1
apps/trading/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
@ -1,26 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
import Index from '../pages/index.page';
|
import Index from '../pages/index.page';
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
|
||||||
|
jest.mock('@vegaprotocol/ui-toolkit', () => {
|
||||||
|
const original = jest.requireActual('@vegaprotocol/ui-toolkit');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
AgGridDynamic: () => <div>AgGrid</div>,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
describe('Index', () => {
|
describe('Index', () => {
|
||||||
it('should render successfully', () => {
|
it('should render successfully', () => {
|
||||||
const { baseElement } = render(
|
render(<Index />);
|
||||||
<VegaWalletContext.Provider
|
|
||||||
value={{
|
|
||||||
keypair: null,
|
|
||||||
keypairs: null,
|
|
||||||
connect: jest.fn(),
|
|
||||||
disconnect: jest.fn(),
|
|
||||||
selectPublicKey: jest.fn(),
|
|
||||||
connector: null,
|
|
||||||
sendTx: jest.fn(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Index />
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
);
|
|
||||||
expect(baseElement).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,9 +12,6 @@ const DEFAULT_ORDER: Order = {
|
|||||||
timeInForce: OrderTimeInForce.IOC,
|
timeInForce: OrderTimeInForce.IOC,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Consider using a generated type when we have a better solution for
|
|
||||||
// sharing the types from GQL
|
|
||||||
|
|
||||||
export type TransactionStatus = 'default' | 'pending';
|
export type TransactionStatus = 'default' | 'pending';
|
||||||
|
|
||||||
export interface DealTicketProps {
|
export interface DealTicketProps {
|
||||||
|
115
libs/graphql/src/__generated__/OrderFields.ts
generated
Normal file
115
libs/graphql/src/__generated__/OrderFields.ts
generated
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL fragment: OrderFields
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface OrderFields_market_tradableInstrument_instrument {
|
||||||
|
__typename: "Instrument";
|
||||||
|
/**
|
||||||
|
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderFields_market_tradableInstrument {
|
||||||
|
__typename: "TradableInstrument";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a fully specified instrument.
|
||||||
|
*/
|
||||||
|
instrument: OrderFields_market_tradableInstrument_instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderFields_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Market full name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||||
|
* number denominated in the currency of the Market. (uint64)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* Currency Balance decimalPlaces Real Balance
|
||||||
|
* GBP 100 0 GBP 100
|
||||||
|
* GBP 100 2 GBP 1.00
|
||||||
|
* GBP 100 4 GBP 0.01
|
||||||
|
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||||
|
*
|
||||||
|
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||||
|
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||||
|
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||||
|
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||||
|
*/
|
||||||
|
decimalPlaces: number;
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a tradable instrument.
|
||||||
|
*/
|
||||||
|
tradableInstrument: OrderFields_market_tradableInstrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderFields {
|
||||||
|
__typename: "Order";
|
||||||
|
/**
|
||||||
|
* Hash of the order data
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The market the order is trading on (probably stored internally as a hash of the market details)
|
||||||
|
*/
|
||||||
|
market: OrderFields_market | null;
|
||||||
|
/**
|
||||||
|
* Type the order type (defaults to PARTY)
|
||||||
|
*/
|
||||||
|
type: OrderType | null;
|
||||||
|
/**
|
||||||
|
* Whether the order is to buy or sell
|
||||||
|
*/
|
||||||
|
side: Side;
|
||||||
|
/**
|
||||||
|
* Total number of contracts that may be bought or sold (immutable) (uint64)
|
||||||
|
*/
|
||||||
|
size: string;
|
||||||
|
/**
|
||||||
|
* The status of an order, for example 'Active'
|
||||||
|
*/
|
||||||
|
status: OrderStatus;
|
||||||
|
/**
|
||||||
|
* Reason for the order to be rejected
|
||||||
|
*/
|
||||||
|
rejectionReason: OrderRejectionReason | null;
|
||||||
|
/**
|
||||||
|
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
|
||||||
|
*/
|
||||||
|
price: string;
|
||||||
|
/**
|
||||||
|
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
|
||||||
|
*/
|
||||||
|
timeInForce: OrderTimeInForce;
|
||||||
|
/**
|
||||||
|
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
|
||||||
|
*/
|
||||||
|
remaining: string;
|
||||||
|
/**
|
||||||
|
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
|
||||||
|
*/
|
||||||
|
expiresAt: string | null;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano formatted date and time for when the order was created (timestamp)
|
||||||
|
*/
|
||||||
|
createdAt: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time the order was altered
|
||||||
|
*/
|
||||||
|
updatedAt: string | null;
|
||||||
|
}
|
126
libs/graphql/src/__generated__/OrderSub.ts
generated
Normal file
126
libs/graphql/src/__generated__/OrderSub.ts
generated
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL subscription operation: OrderSub
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface OrderSub_orders_market_tradableInstrument_instrument {
|
||||||
|
__typename: "Instrument";
|
||||||
|
/**
|
||||||
|
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderSub_orders_market_tradableInstrument {
|
||||||
|
__typename: "TradableInstrument";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a fully specified instrument.
|
||||||
|
*/
|
||||||
|
instrument: OrderSub_orders_market_tradableInstrument_instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderSub_orders_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Market full name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||||
|
* number denominated in the currency of the Market. (uint64)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* Currency Balance decimalPlaces Real Balance
|
||||||
|
* GBP 100 0 GBP 100
|
||||||
|
* GBP 100 2 GBP 1.00
|
||||||
|
* GBP 100 4 GBP 0.01
|
||||||
|
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||||
|
*
|
||||||
|
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||||
|
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||||
|
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||||
|
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||||
|
*/
|
||||||
|
decimalPlaces: number;
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a tradable instrument.
|
||||||
|
*/
|
||||||
|
tradableInstrument: OrderSub_orders_market_tradableInstrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderSub_orders {
|
||||||
|
__typename: "Order";
|
||||||
|
/**
|
||||||
|
* Hash of the order data
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The market the order is trading on (probably stored internally as a hash of the market details)
|
||||||
|
*/
|
||||||
|
market: OrderSub_orders_market | null;
|
||||||
|
/**
|
||||||
|
* Type the order type (defaults to PARTY)
|
||||||
|
*/
|
||||||
|
type: OrderType | null;
|
||||||
|
/**
|
||||||
|
* Whether the order is to buy or sell
|
||||||
|
*/
|
||||||
|
side: Side;
|
||||||
|
/**
|
||||||
|
* Total number of contracts that may be bought or sold (immutable) (uint64)
|
||||||
|
*/
|
||||||
|
size: string;
|
||||||
|
/**
|
||||||
|
* The status of an order, for example 'Active'
|
||||||
|
*/
|
||||||
|
status: OrderStatus;
|
||||||
|
/**
|
||||||
|
* Reason for the order to be rejected
|
||||||
|
*/
|
||||||
|
rejectionReason: OrderRejectionReason | null;
|
||||||
|
/**
|
||||||
|
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
|
||||||
|
*/
|
||||||
|
price: string;
|
||||||
|
/**
|
||||||
|
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
|
||||||
|
*/
|
||||||
|
timeInForce: OrderTimeInForce;
|
||||||
|
/**
|
||||||
|
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
|
||||||
|
*/
|
||||||
|
remaining: string;
|
||||||
|
/**
|
||||||
|
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
|
||||||
|
*/
|
||||||
|
expiresAt: string | null;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano formatted date and time for when the order was created (timestamp)
|
||||||
|
*/
|
||||||
|
createdAt: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time the order was altered
|
||||||
|
*/
|
||||||
|
updatedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderSub {
|
||||||
|
/**
|
||||||
|
* Subscribe to orders updates
|
||||||
|
*/
|
||||||
|
orders: OrderSub_orders[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderSubVariables {
|
||||||
|
partyId: string;
|
||||||
|
}
|
138
libs/graphql/src/__generated__/Orders.ts
generated
Normal file
138
libs/graphql/src/__generated__/Orders.ts
generated
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { OrderType, Side, OrderStatus, OrderRejectionReason, OrderTimeInForce } from "./globalTypes";
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: Orders
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface Orders_party_orders_market_tradableInstrument_instrument {
|
||||||
|
__typename: "Instrument";
|
||||||
|
/**
|
||||||
|
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Orders_party_orders_market_tradableInstrument {
|
||||||
|
__typename: "TradableInstrument";
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a fully specified instrument.
|
||||||
|
*/
|
||||||
|
instrument: Orders_party_orders_market_tradableInstrument_instrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Orders_party_orders_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Market full name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||||
|
* number denominated in the currency of the Market. (uint64)
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* Currency Balance decimalPlaces Real Balance
|
||||||
|
* GBP 100 0 GBP 100
|
||||||
|
* GBP 100 2 GBP 1.00
|
||||||
|
* GBP 100 4 GBP 0.01
|
||||||
|
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||||
|
*
|
||||||
|
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||||
|
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||||
|
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||||
|
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||||
|
*/
|
||||||
|
decimalPlaces: number;
|
||||||
|
/**
|
||||||
|
* An instance of or reference to a tradable instrument.
|
||||||
|
*/
|
||||||
|
tradableInstrument: Orders_party_orders_market_tradableInstrument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Orders_party_orders {
|
||||||
|
__typename: "Order";
|
||||||
|
/**
|
||||||
|
* Hash of the order data
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The market the order is trading on (probably stored internally as a hash of the market details)
|
||||||
|
*/
|
||||||
|
market: Orders_party_orders_market | null;
|
||||||
|
/**
|
||||||
|
* Type the order type (defaults to PARTY)
|
||||||
|
*/
|
||||||
|
type: OrderType | null;
|
||||||
|
/**
|
||||||
|
* Whether the order is to buy or sell
|
||||||
|
*/
|
||||||
|
side: Side;
|
||||||
|
/**
|
||||||
|
* Total number of contracts that may be bought or sold (immutable) (uint64)
|
||||||
|
*/
|
||||||
|
size: string;
|
||||||
|
/**
|
||||||
|
* The status of an order, for example 'Active'
|
||||||
|
*/
|
||||||
|
status: OrderStatus;
|
||||||
|
/**
|
||||||
|
* Reason for the order to be rejected
|
||||||
|
*/
|
||||||
|
rejectionReason: OrderRejectionReason | null;
|
||||||
|
/**
|
||||||
|
* The worst price the order will trade at (e.g. buy for price or less, sell for price or more) (uint64)
|
||||||
|
*/
|
||||||
|
price: string;
|
||||||
|
/**
|
||||||
|
* The timeInForce of order (determines how and if it executes, and whether it persists on the book)
|
||||||
|
*/
|
||||||
|
timeInForce: OrderTimeInForce;
|
||||||
|
/**
|
||||||
|
* Number of contracts remaining of the total that have not yet been bought or sold (uint64)
|
||||||
|
*/
|
||||||
|
remaining: string;
|
||||||
|
/**
|
||||||
|
* Expiration time of this order (ISO-8601 RFC3339+Nano formatted date)
|
||||||
|
*/
|
||||||
|
expiresAt: string | null;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano formatted date and time for when the order was created (timestamp)
|
||||||
|
*/
|
||||||
|
createdAt: string;
|
||||||
|
/**
|
||||||
|
* RFC3339Nano time the order was altered
|
||||||
|
*/
|
||||||
|
updatedAt: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Orders_party {
|
||||||
|
__typename: "Party";
|
||||||
|
/**
|
||||||
|
* Party identifier
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Orders relating to a party
|
||||||
|
*/
|
||||||
|
orders: Orders_party_orders[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Orders {
|
||||||
|
/**
|
||||||
|
* An entity that is trading on the VEGA network
|
||||||
|
*/
|
||||||
|
party: Orders_party | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrdersVariables {
|
||||||
|
partyId: string;
|
||||||
|
}
|
20
libs/graphql/src/__generated__/globalTypes.ts
generated
20
libs/graphql/src/__generated__/globalTypes.ts
generated
@ -162,6 +162,18 @@ export enum OrderStatus {
|
|||||||
Stopped = "Stopped",
|
Stopped = "Stopped",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid order types, these determine what happens when an order is added to the book
|
||||||
|
*/
|
||||||
|
export enum OrderTimeInForce {
|
||||||
|
FOK = "FOK",
|
||||||
|
GFA = "GFA",
|
||||||
|
GFN = "GFN",
|
||||||
|
GTC = "GTC",
|
||||||
|
GTT = "GTT",
|
||||||
|
IOC = "IOC",
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrderType {
|
export enum OrderType {
|
||||||
Limit = "Limit",
|
Limit = "Limit",
|
||||||
Market = "Market",
|
Market = "Market",
|
||||||
@ -224,6 +236,14 @@ export enum ProposalState {
|
|||||||
WaitingForNodeVote = "WaitingForNodeVote",
|
WaitingForNodeVote = "WaitingForNodeVote",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the placer of an order is aiming to buy or sell on the market
|
||||||
|
*/
|
||||||
|
export enum Side {
|
||||||
|
Buy = "Buy",
|
||||||
|
Sell = "Sell",
|
||||||
|
}
|
||||||
|
|
||||||
export enum VoteValue {
|
export enum VoteValue {
|
||||||
No = "No",
|
No = "No",
|
||||||
Yes = "Yes",
|
Yes = "Yes",
|
||||||
|
@ -9,5 +9,8 @@ export * from './__generated__/MarketDataFields';
|
|||||||
export * from './__generated__/NetworkParametersQuery';
|
export * from './__generated__/NetworkParametersQuery';
|
||||||
export * from './__generated__/NodesQuery';
|
export * from './__generated__/NodesQuery';
|
||||||
export * from './__generated__/OrderEvent';
|
export * from './__generated__/OrderEvent';
|
||||||
|
export * from './__generated__/OrderFields';
|
||||||
|
export * from './__generated__/Orders';
|
||||||
|
export * from './__generated__/OrderSub';
|
||||||
export * from './__generated__/PartyAssetsQuery';
|
export * from './__generated__/PartyAssetsQuery';
|
||||||
export * from './__generated__/ProposalsQuery';
|
export * from './__generated__/ProposalsQuery';
|
||||||
|
@ -20,7 +20,7 @@ export const MarketListTable = ({
|
|||||||
}: MarketListTableProps) => {
|
}: MarketListTableProps) => {
|
||||||
const [initialMarkets] = useState(markets);
|
const [initialMarkets] = useState(markets);
|
||||||
const gridApi = useRef<GridApi | null>(null);
|
const gridApi = useRef<GridApi | null>(null);
|
||||||
useApplyGridTransaction(markets, gridApi.current);
|
useApplyGridTransaction<Markets_markets>(markets, gridApi.current);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgGrid
|
<AgGrid
|
||||||
@ -28,6 +28,7 @@ export const MarketListTable = ({
|
|||||||
overlayNoRowsTemplate="No markets"
|
overlayNoRowsTemplate="No markets"
|
||||||
rowData={initialMarkets}
|
rowData={initialMarkets}
|
||||||
getRowNodeId={(data) => data.id}
|
getRowNodeId={(data) => data.id}
|
||||||
|
suppressCellFocus={true}
|
||||||
defaultColDef={{
|
defaultColDef={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
resizable: true,
|
resizable: true,
|
||||||
|
@ -18,13 +18,14 @@ export interface Stats {
|
|||||||
chainId: string;
|
chainId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore investigate why series of 'ors' didn't work instead of this 'any'
|
// eslint-disable-next-line
|
||||||
export type value = any;
|
export type value = any;
|
||||||
export type goodThreshold = (...args: value[]) => boolean;
|
export type goodThreshold = (...args: value[]) => boolean;
|
||||||
|
|
||||||
export interface StatFields {
|
export interface StatFields {
|
||||||
title: string;
|
title: string;
|
||||||
goodThreshold?: goodThreshold;
|
goodThreshold?: goodThreshold;
|
||||||
|
// eslint-disable-next-line
|
||||||
formatter?: (arg0: value) => any;
|
formatter?: (arg0: value) => any;
|
||||||
promoted?: boolean;
|
promoted?: boolean;
|
||||||
value?: value;
|
value?: value;
|
||||||
|
12
libs/order-list/.babelrc
Normal file
12
libs/order-list/.babelrc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@nrwl/react/babel",
|
||||||
|
{
|
||||||
|
"runtime": "automatic",
|
||||||
|
"useBuiltIns": "usage"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"plugins": []
|
||||||
|
}
|
18
libs/order-list/.eslintrc.json
Normal file
18
libs/order-list/.eslintrc.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
7
libs/order-list/README.md
Normal file
7
libs/order-list/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# order-list
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test order-list` to execute the unit tests via [Jest](https://jestjs.io).
|
10
libs/order-list/jest.config.js
Normal file
10
libs/order-list/jest.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: 'order-list',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.[tj]sx?$': 'babel-jest',
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/libs/order-list',
|
||||||
|
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||||
|
};
|
23
libs/order-list/project.json
Normal file
23
libs/order-list/project.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"root": "libs/order-list",
|
||||||
|
"sourceRoot": "libs/order-list/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nrwl/linter:eslint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/order-list/**/*.{ts,tsx,js,jsx}"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nrwl/jest:jest",
|
||||||
|
"outputs": ["coverage/libs/order-list"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/order-list/jest.config.js",
|
||||||
|
"passWithNoTests": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
libs/order-list/src/index.ts
Normal file
1
libs/order-list/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './order-list';
|
153
libs/order-list/src/order-list.spec.tsx
Normal file
153
libs/order-list/src/order-list.spec.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Orders_party_orders } from '@vegaprotocol/graphql';
|
||||||
|
import {
|
||||||
|
OrderStatus,
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderType,
|
||||||
|
Side,
|
||||||
|
OrderRejectionReason,
|
||||||
|
} from '@vegaprotocol/graphql';
|
||||||
|
import { OrderList } from './order-list';
|
||||||
|
|
||||||
|
test('No orders message shown', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<OrderList orders={[]} />);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('No orders')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketOrder: Orders_party_orders = {
|
||||||
|
__typename: 'Order',
|
||||||
|
id: 'order-id',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-id',
|
||||||
|
name: 'market-name',
|
||||||
|
decimalPlaces: 2,
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
code: 'instrument-code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: '10',
|
||||||
|
type: OrderType.Market,
|
||||||
|
status: OrderStatus.Active,
|
||||||
|
side: Side.Buy,
|
||||||
|
remaining: '5',
|
||||||
|
price: '',
|
||||||
|
timeInForce: OrderTimeInForce.IOC,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const limitOrder: Orders_party_orders = {
|
||||||
|
__typename: 'Order',
|
||||||
|
id: 'order-id',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-id',
|
||||||
|
name: 'market-name',
|
||||||
|
decimalPlaces: 2,
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
code: 'instrument-code',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
size: '10',
|
||||||
|
type: OrderType.Limit,
|
||||||
|
status: OrderStatus.Active,
|
||||||
|
side: Side.Sell,
|
||||||
|
remaining: '5',
|
||||||
|
price: '12345',
|
||||||
|
timeInForce: OrderTimeInForce.GTT,
|
||||||
|
createdAt: new Date('2022-3-3').toISOString(),
|
||||||
|
expiresAt: new Date('2022-3-5').toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Correct columns are rendered', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<OrderList orders={[marketOrder]} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const headers = screen.getAllByRole('columnheader');
|
||||||
|
expect(headers).toHaveLength(8);
|
||||||
|
expect(headers.map((h) => h.textContent?.trim())).toEqual([
|
||||||
|
'Market',
|
||||||
|
'Amount',
|
||||||
|
'Type',
|
||||||
|
'Status',
|
||||||
|
'Filled',
|
||||||
|
'Price',
|
||||||
|
'Time In Force',
|
||||||
|
'Created At',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Correct formatting applied for market order', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<OrderList orders={[marketOrder]} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cells = screen.getAllByRole('gridcell');
|
||||||
|
const expectedValues = [
|
||||||
|
marketOrder.market?.tradableInstrument.instrument.code,
|
||||||
|
'+10',
|
||||||
|
marketOrder.type,
|
||||||
|
marketOrder.status,
|
||||||
|
'5',
|
||||||
|
'-',
|
||||||
|
marketOrder.timeInForce,
|
||||||
|
getDateTimeFormat().format(new Date(marketOrder.createdAt)),
|
||||||
|
];
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Correct formatting applied for GTT limit order', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<OrderList orders={[limitOrder]} />);
|
||||||
|
});
|
||||||
|
const cells = screen.getAllByRole('gridcell');
|
||||||
|
const expectedValues = [
|
||||||
|
limitOrder.market?.tradableInstrument.instrument.code,
|
||||||
|
'-10',
|
||||||
|
limitOrder.type,
|
||||||
|
limitOrder.status,
|
||||||
|
'5',
|
||||||
|
formatNumber(limitOrder.price, limitOrder.market?.decimalPlaces ?? 0),
|
||||||
|
`${limitOrder.timeInForce}: ${getDateTimeFormat().format(
|
||||||
|
new Date(limitOrder.expiresAt ?? '')
|
||||||
|
)}`,
|
||||||
|
getDateTimeFormat().format(new Date(limitOrder.createdAt)),
|
||||||
|
];
|
||||||
|
cells.forEach((cell, i) => {
|
||||||
|
expect(cell).toHaveTextContent(expectedValues[i]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Correct formatting applied for a rejected order', async () => {
|
||||||
|
const rejectedOrder = {
|
||||||
|
...marketOrder,
|
||||||
|
status: OrderStatus.Rejected,
|
||||||
|
rejectionReason: OrderRejectionReason.InsufficientAssetBalance,
|
||||||
|
};
|
||||||
|
await act(async () => {
|
||||||
|
render(<OrderList orders={[rejectedOrder]} />);
|
||||||
|
});
|
||||||
|
const cells = screen.getAllByRole('gridcell');
|
||||||
|
expect(cells[3]).toHaveTextContent(
|
||||||
|
`${rejectedOrder.status}: ${rejectedOrder.rejectionReason}`
|
||||||
|
);
|
||||||
|
});
|
97
libs/order-list/src/order-list.tsx
Normal file
97
libs/order-list/src/order-list.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Orders_party_orders,
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderStatus,
|
||||||
|
Side,
|
||||||
|
} from '@vegaprotocol/graphql';
|
||||||
|
import {
|
||||||
|
formatNumber,
|
||||||
|
getDateTimeFormat,
|
||||||
|
useApplyGridTransaction,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { 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="Market"
|
||||||
|
field="market.tradableInstrument.instrument.code"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
headerName="Amount"
|
||||||
|
field="size"
|
||||||
|
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"
|
||||||
|
valueFormatter={({ data }: ValueFormatterParams) => {
|
||||||
|
return `${Number(data.size) - Number(data.remaining)}/${data.size}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
field="price"
|
||||||
|
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
libs/order-list/src/setup-tests.ts
Normal file
1
libs/order-list/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
25
libs/order-list/tsconfig.json
Normal file
25
libs/order-list/tsconfig.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
22
libs/order-list/tsconfig.lib.json
Normal file
22
libs/order-list/tsconfig.lib.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.test.jsx"
|
||||||
|
],
|
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
|
||||||
|
}
|
19
libs/order-list/tsconfig.spec.json
Normal file
19
libs/order-list/tsconfig.spec.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.test.tsx",
|
||||||
|
"**/*.spec.tsx",
|
||||||
|
"**/*.test.js",
|
||||||
|
"**/*.spec.js",
|
||||||
|
"**/*.test.jsx",
|
||||||
|
"**/*.spec.jsx",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
import { useApplyGridTransaction } from './use-apply-grid-transaction';
|
||||||
|
import { renderHook } from '@testing-library/react-hooks';
|
||||||
|
import { GridApi } from 'ag-grid-community';
|
||||||
|
|
||||||
|
type Items = Array<{ id: string; value: number }>;
|
||||||
|
|
||||||
|
const item = {
|
||||||
|
id: '1',
|
||||||
|
value: 1,
|
||||||
|
};
|
||||||
|
const item2 = {
|
||||||
|
id: '2',
|
||||||
|
value: 2,
|
||||||
|
};
|
||||||
|
const items = [item, item2];
|
||||||
|
|
||||||
|
function setup(items: Items, rowNodes: Items) {
|
||||||
|
const gridApiMock = {
|
||||||
|
applyTransaction: jest.fn(),
|
||||||
|
getRowNode: (id: string) => {
|
||||||
|
const node = rowNodes.find((i) => i.id === id);
|
||||||
|
if (node) {
|
||||||
|
return { data: node };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
renderHook(() => useApplyGridTransaction(items, gridApiMock as any));
|
||||||
|
return gridApiMock;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Adds items', () => {
|
||||||
|
const gridApiMock = setup(items, []);
|
||||||
|
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
|
||||||
|
update: [],
|
||||||
|
add: items,
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Doesnt update rows without changes', () => {
|
||||||
|
const rowNodes: Array<{ id: string; value: number }> = [...items];
|
||||||
|
const gridApiMock = setup(items, rowNodes);
|
||||||
|
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
|
||||||
|
update: [],
|
||||||
|
add: [],
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update rows with changes', () => {
|
||||||
|
const rowNodes = [...items];
|
||||||
|
const updatedItems = [
|
||||||
|
{ id: '1', value: 10 },
|
||||||
|
{ id: '2', value: 20 },
|
||||||
|
];
|
||||||
|
const gridApiMock = setup(updatedItems, rowNodes);
|
||||||
|
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
|
||||||
|
update: updatedItems,
|
||||||
|
add: [],
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Updates and adds at the same time', () => {
|
||||||
|
const newItem = { id: '3', value: 3 };
|
||||||
|
const updatedItem = { id: '2', value: 20 };
|
||||||
|
const gridApiMock = setup([newItem, updatedItem], [...items]);
|
||||||
|
expect(gridApiMock.applyTransaction).toHaveBeenCalledWith({
|
||||||
|
update: [updatedItem],
|
||||||
|
add: [newItem],
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
import { GridApi } from 'ag-grid-community';
|
import { GridApi } from 'ag-grid-community';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
|
|
||||||
export const useApplyGridTransaction = <T extends { id: string }>(
|
export const useApplyGridTransaction = <T extends { id: string }>(
|
||||||
data: T[],
|
data: T[],
|
||||||
@ -18,7 +19,9 @@ export const useApplyGridTransaction = <T extends { id: string }>(
|
|||||||
const rowNode = gridApi.getRowNode(d.id);
|
const rowNode = gridApi.getRowNode(d.id);
|
||||||
|
|
||||||
if (rowNode) {
|
if (rowNode) {
|
||||||
update.push(d);
|
if (!isEqual(rowNode.data, d)) {
|
||||||
|
update.push(d);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
add.push(d);
|
add.push(d);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import once from 'lodash.once';
|
import once from 'lodash/once';
|
||||||
import memoize from 'lodash.memoize';
|
import memoize from 'lodash/memoize';
|
||||||
import { addDecimal } from '../decimals';
|
import { addDecimal } from '../decimals';
|
||||||
|
|
||||||
const getUserLocale = () => 'default';
|
const getUserLocale = () => 'default';
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
"@radix-ui/react-tooltip": "^0.1.7",
|
"@radix-ui/react-tooltip": "^0.1.7",
|
||||||
"@sentry/react": "^6.18.1",
|
"@sentry/react": "^6.18.1",
|
||||||
"@sentry/tracing": "^6.18.1",
|
"@sentry/tracing": "^6.18.1",
|
||||||
|
"@types/lodash": "^4.14.180",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@vegaprotocol/vegawallet-service-api-client": "^0.4.6",
|
"@vegaprotocol/vegawallet-service-api-client": "^0.4.6",
|
||||||
"ag-grid-community": "^27.0.1",
|
"ag-grid-community": "^27.0.1",
|
||||||
@ -31,20 +32,18 @@
|
|||||||
"bignumber.js": "^9.0.2",
|
"bignumber.js": "^9.0.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"ethers": "^5.6.0",
|
"ethers": "^5.6.0",
|
||||||
"graphql": "^15.7.2",
|
"graphql": "^15.7.2",
|
||||||
"graphql-ws": "^5.6.3",
|
"graphql-ws": "^5.6.3",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash": "^4.17.21",
|
||||||
"lodash.memoize": "^4.1.2",
|
|
||||||
"lodash.once": "^4.1.1",
|
|
||||||
"next": "12.0.7",
|
"next": "12.0.7",
|
||||||
"nx": "^13.8.3",
|
"nx": "^13.8.3",
|
||||||
"postcss": "^8.4.6",
|
"postcss": "^8.4.6",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-hook-form": "^7.27.0",
|
"react-hook-form": "^7.27.0",
|
||||||
"react-singleton-hook": "^3.2.3",
|
|
||||||
"react-syntax-highlighter": "^15.4.5",
|
"react-syntax-highlighter": "^15.4.5",
|
||||||
"react-use-websocket": "^3.0.0",
|
"react-use-websocket": "^3.0.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.6",
|
"react-virtualized-auto-sizer": "^1.0.6",
|
||||||
@ -80,8 +79,6 @@
|
|||||||
"@testing-library/react-hooks": "7.0.2",
|
"@testing-library/react-hooks": "7.0.2",
|
||||||
"@types/classnames": "^2.3.1",
|
"@types/classnames": "^2.3.1",
|
||||||
"@types/jest": "27.0.2",
|
"@types/jest": "27.0.2",
|
||||||
"@types/lodash.memoize": "^4.1.6",
|
|
||||||
"@types/lodash.once": "^4.1.6",
|
|
||||||
"@types/node": "16.11.7",
|
"@types/node": "16.11.7",
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"@types/react": "17.0.30",
|
"@types/react": "17.0.30",
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"@vegaprotocol/graphql": ["libs/graphql/src/index.ts"],
|
"@vegaprotocol/graphql": ["libs/graphql/src/index.ts"],
|
||||||
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
||||||
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
|
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
|
||||||
|
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],
|
||||||
"@vegaprotocol/react-helpers": ["libs/react-helpers/src/index.ts"],
|
"@vegaprotocol/react-helpers": ["libs/react-helpers/src/index.ts"],
|
||||||
"@vegaprotocol/tailwindcss-config": [
|
"@vegaprotocol/tailwindcss-config": [
|
||||||
"libs/tailwindcss-config/src/index.js"
|
"libs/tailwindcss-config/src/index.js"
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"graphql": "libs/graphql",
|
"graphql": "libs/graphql",
|
||||||
"market-list": "libs/market-list",
|
"market-list": "libs/market-list",
|
||||||
"network-stats": "libs/network-stats",
|
"network-stats": "libs/network-stats",
|
||||||
|
"order-list": "libs/order-list",
|
||||||
"react-helpers": "libs/react-helpers",
|
"react-helpers": "libs/react-helpers",
|
||||||
"stats": "apps/stats",
|
"stats": "apps/stats",
|
||||||
"stats-e2e": "apps/stats-e2e",
|
"stats-e2e": "apps/stats-e2e",
|
||||||
|
26
yarn.lock
26
yarn.lock
@ -5201,21 +5201,7 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
|
||||||
|
|
||||||
"@types/lodash.memoize@^4.1.6":
|
"@types/lodash@^4.14.180":
|
||||||
version "4.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23"
|
|
||||||
integrity sha512-mYxjKiKzRadRJVClLKxS4wb3Iy9kzwJ1CkbyKiadVxejnswnRByyofmPMscFKscmYpl36BEEhCMPuWhA1R/1ZQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/lodash" "*"
|
|
||||||
|
|
||||||
"@types/lodash.once@^4.1.6":
|
|
||||||
version "4.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash.once/-/lodash.once-4.1.6.tgz#f6ea46e3426bc3494b72a45e65efed440103b967"
|
|
||||||
integrity sha512-kawTe2cBNZ5OI4CvTJT9cs8wacGZK4BoakKAGASl/jH3LxflMTuy82wN2U5klYsxYjrABkNWmzgO33volt7urQ==
|
|
||||||
dependencies:
|
|
||||||
"@types/lodash" "*"
|
|
||||||
|
|
||||||
"@types/lodash@*":
|
|
||||||
version "4.14.180"
|
version "4.14.180"
|
||||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
||||||
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
|
integrity sha512-XOKXa1KIxtNXgASAnwj7cnttJxS4fksBRywK/9LzRV5YxrF80BXZIGeQSuoESQ/VkUj30Ae0+YcuHc15wJCB2g==
|
||||||
@ -9230,6 +9216,11 @@ date-fns@^1.27.2:
|
|||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
|
||||||
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
|
||||||
|
|
||||||
|
date-fns@^2.28.0:
|
||||||
|
version "2.28.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||||
|
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4:
|
||||||
version "1.10.8"
|
version "1.10.8"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.8.tgz#267df4bc6276fcb33c04a6735287e3f429abec41"
|
||||||
@ -16577,11 +16568,6 @@ react-shallow-renderer@^16.13.1:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.12.0 || ^17.0.0"
|
react-is "^16.12.0 || ^17.0.0"
|
||||||
|
|
||||||
react-singleton-hook@^3.2.3:
|
|
||||||
version "3.2.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-singleton-hook/-/react-singleton-hook-3.2.3.tgz#1765e67b1910fd163cdcd186a840af82b60ca17e"
|
|
||||||
integrity sha512-DYJ70V8IVUZznygmRYYF+hxwLkGPLrCCTAD1Je7sZYXquE9HpAJqBVeEgb2XIZwxWUw97PN2ho5J/+63YwdKaA==
|
|
||||||
|
|
||||||
react-sizeme@^3.0.1:
|
react-sizeme@^3.0.1:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4"
|
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4"
|
||||||
|
Loading…
Reference in New Issue
Block a user