feat: [console-lite] - abstract components for portfolio page (#1306)
* feat: [console-lite] - abstract components for portfolio page * feat: [console-lite] - abstract components for portfolio page - improvements * feat: [console-lite] - use abstract list with accounts manager * feat: [console-lite] - use abstract list with positions * feat: [console-lite] - use abstract list with orders * feat: [console-lite] - use abstract list with fills * feat: [console-lite] - fix failings linters * feat: [console-lite] - fix failings e2e test * feat: [console-lite] - fix failings e2e test * feat: [console-lite] - improve some css * feat: [console-lite] - a bunch of fixes for positions * feat: [console-lite] - tweaks of columns confs, bunch of e2e tests * feat: [console-lite] - abstract components for portfolio page - after review feedback fixes * feat: [console-lite] - abstract components for portfolio page - after review feedback fixes * feat: [console-lite] - abstract components for portfolio page - add missing asset datails dialog * feat: [console-lite] - portfolio abstractions - add fills hook unit tests * feat: [console-lite] - portfolio abstractions - add orders hook unit tests * feat: [console-lite] - portfolio abstractions - fix lint error * feat: [console-lite] - abstract components for portfolio page -fix failings tests * feat: [console-lite] - abstract components for portfolio page -fix failings tests * feat: [console-lite] - abstract components for portfolio page - fix failings lint check * feat: [console-lite] - abstract components for portfolio page - next fix * feat: [console-lite] - abstract components for portfolio page - improve some int test Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
d95bfb60ea
commit
5c4af868a7
@ -2,6 +2,12 @@ import {
|
|||||||
connectVegaWallet,
|
connectVegaWallet,
|
||||||
disconnectVegaWallet,
|
disconnectVegaWallet,
|
||||||
} from '../support/connect-wallet';
|
} from '../support/connect-wallet';
|
||||||
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
|
import { generatePositions } from '../support/mocks/generate-positions';
|
||||||
|
import { generateAccounts } from '../support/mocks/generate-accounts';
|
||||||
|
import { generateOrders } from '../support/mocks/generate-orders';
|
||||||
|
import { generateFills } from '../support/mocks/generate-fills';
|
||||||
|
import { generateFillsMarkets } from '../support/mocks/generate-markets';
|
||||||
|
|
||||||
describe('Portfolio page', () => {
|
describe('Portfolio page', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -19,19 +25,91 @@ describe('Portfolio page', () => {
|
|||||||
it('certain tabs should exist', () => {
|
it('certain tabs should exist', () => {
|
||||||
cy.visit('/portfolio');
|
cy.visit('/portfolio');
|
||||||
connectVegaWallet();
|
connectVegaWallet();
|
||||||
cy.getByTestId('Assets').should('exist');
|
|
||||||
cy.getByTestId('tab-assets').should('exist');
|
|
||||||
|
|
||||||
cy.getByTestId('Positions').click();
|
cy.getByTestId('assets').click();
|
||||||
cy.getByTestId('tab-positions').should('exist');
|
cy.location('pathname').should('eq', '/portfolio/assets');
|
||||||
|
|
||||||
cy.getByTestId('Orders').click();
|
cy.getByTestId('positions').click();
|
||||||
cy.getByTestId('tab-orders').should('exist');
|
cy.location('pathname').should('eq', '/portfolio/positions');
|
||||||
|
|
||||||
cy.getByTestId('Fills').click();
|
cy.getByTestId('orders').click();
|
||||||
cy.getByTestId('tab-fills').should('exist');
|
cy.location('pathname').should('eq', '/portfolio/orders');
|
||||||
|
|
||||||
cy.getByTestId('Deposits').click();
|
cy.getByTestId('fills').click();
|
||||||
cy.getByTestId('tab-deposits').should('exist');
|
cy.location('pathname').should('eq', '/portfolio/fills');
|
||||||
|
|
||||||
|
cy.getByTestId('deposits').click();
|
||||||
|
cy.location('pathname').should('eq', '/portfolio/deposits');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Assets view', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(req, 'Positions', generatePositions());
|
||||||
|
aliasQuery(req, 'Accounts', generateAccounts());
|
||||||
|
});
|
||||||
|
cy.visit('/portfolio/assets');
|
||||||
|
connectVegaWallet();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('data should be properly rendered', () => {
|
||||||
|
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||||
|
cy.get(
|
||||||
|
'.ag-center-cols-container [row-id="ACCOUNT_TYPE_GENERAL-asset-id-null"]'
|
||||||
|
)
|
||||||
|
.find('button')
|
||||||
|
.click();
|
||||||
|
cy.getByTestId('dialog-title').should(
|
||||||
|
'have.text',
|
||||||
|
'Asset details - tEURO'
|
||||||
|
);
|
||||||
|
cy.getByTestId('dialog-close').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Positions view', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(req, 'Positions', generatePositions());
|
||||||
|
aliasQuery(req, 'Accounts', generateAccounts());
|
||||||
|
});
|
||||||
|
cy.visit('/portfolio/positions');
|
||||||
|
connectVegaWallet();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('data should be properly rendered', () => {
|
||||||
|
cy.getByTestId('positions-asset-tDAI').should('exist');
|
||||||
|
cy.getByTestId('positions-asset-tEURO').should('exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Orders view', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(req, 'Orders', generateOrders());
|
||||||
|
aliasQuery(req, 'Markets', generateFillsMarkets());
|
||||||
|
});
|
||||||
|
cy.visit('/portfolio/orders');
|
||||||
|
connectVegaWallet();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('data should be properly rendered', () => {
|
||||||
|
cy.get('.ag-center-cols-container .ag-row').should('have.length', 5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Fills view', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(req, 'Fills', generateFills());
|
||||||
|
aliasQuery(req, 'Markets', generateFillsMarkets());
|
||||||
|
});
|
||||||
|
cy.visit('/portfolio/fills');
|
||||||
|
connectVegaWallet();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('data should be properly rendered', () => {
|
||||||
|
cy.get('.ag-center-cols-container .ag-row').should('have.length', 4);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
108
apps/console-lite-e2e/src/support/mocks/generate-accounts.ts
Normal file
108
apps/console-lite-e2e/src/support/mocks/generate-accounts.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { AccountsQuery } from '@vegaprotocol/accounts';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
|
export const generateAccounts = (
|
||||||
|
override?: PartialDeep<AccountsQuery>
|
||||||
|
): AccountsQuery => {
|
||||||
|
const defaultAccounts: AccountsQuery = {
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
__typename: 'Account',
|
||||||
|
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '100000000',
|
||||||
|
market: null,
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id',
|
||||||
|
symbol: 'tEURO',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Account',
|
||||||
|
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '100000000',
|
||||||
|
market: {
|
||||||
|
id: '0604e8c918655474525e1a95367902266ade70d318c2c908f0cca6e3d11dcb13',
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__typename: 'Market',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id-2',
|
||||||
|
symbol: 'tDAI',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Account',
|
||||||
|
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||||
|
balance: '1000',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id',
|
||||||
|
symbol: 'tEURO',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Account',
|
||||||
|
type: AccountType.ACCOUNT_TYPE_MARGIN,
|
||||||
|
balance: '1000',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id-2',
|
||||||
|
symbol: 'tDAI',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Account',
|
||||||
|
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '100000000',
|
||||||
|
market: null,
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-0',
|
||||||
|
symbol: 'AST0',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return merge(defaultAccounts, override);
|
||||||
|
};
|
114
apps/console-lite-e2e/src/support/mocks/generate-fills.ts
Normal file
114
apps/console-lite-e2e/src/support/mocks/generate-fills.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import type {
|
||||||
|
Fills,
|
||||||
|
Fills_party_tradesConnection_edges_node,
|
||||||
|
} from '@vegaprotocol/fills';
|
||||||
|
import { Side } from '@vegaprotocol/types';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
|
export const generateFills = (override?: PartialDeep<Fills>): Fills => {
|
||||||
|
const fills: Fills_party_tradesConnection_edges_node[] = [
|
||||||
|
generateFill({
|
||||||
|
buyer: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
generateFill({
|
||||||
|
id: '1',
|
||||||
|
seller: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
},
|
||||||
|
aggressor: Side.SIDE_SELL,
|
||||||
|
buyerFee: {
|
||||||
|
infrastructureFee: '5000',
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
id: 'market-1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
generateFill({
|
||||||
|
id: '2',
|
||||||
|
seller: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
},
|
||||||
|
aggressor: Side.SIDE_BUY,
|
||||||
|
}),
|
||||||
|
generateFill({
|
||||||
|
id: '3',
|
||||||
|
aggressor: Side.SIDE_SELL,
|
||||||
|
market: {
|
||||||
|
id: 'market-2',
|
||||||
|
},
|
||||||
|
buyer: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultResult: Fills = {
|
||||||
|
party: {
|
||||||
|
id: 'buyer-id',
|
||||||
|
tradesConnection: {
|
||||||
|
__typename: 'TradeConnection',
|
||||||
|
edges: fills.map((f) => {
|
||||||
|
return {
|
||||||
|
__typename: 'TradeEdge',
|
||||||
|
node: f,
|
||||||
|
cursor: '3',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
pageInfo: {
|
||||||
|
__typename: 'PageInfo',
|
||||||
|
startCursor: '1',
|
||||||
|
endCursor: '2',
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__typename: 'Party',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return merge(defaultResult, override);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFill = (
|
||||||
|
override?: PartialDeep<Fills_party_tradesConnection_edges_node>
|
||||||
|
) => {
|
||||||
|
const defaultFill: Fills_party_tradesConnection_edges_node = {
|
||||||
|
__typename: 'Trade',
|
||||||
|
id: '0',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
price: '10000000',
|
||||||
|
size: '50000',
|
||||||
|
buyOrder: 'buy-order',
|
||||||
|
sellOrder: 'sell-order',
|
||||||
|
aggressor: Side.SIDE_BUY,
|
||||||
|
buyer: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'buyer-id',
|
||||||
|
},
|
||||||
|
seller: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: 'seller-id',
|
||||||
|
},
|
||||||
|
buyerFee: {
|
||||||
|
__typename: 'TradeFee',
|
||||||
|
makerFee: '100',
|
||||||
|
infrastructureFee: '100',
|
||||||
|
liquidityFee: '100',
|
||||||
|
},
|
||||||
|
sellerFee: {
|
||||||
|
__typename: 'TradeFee',
|
||||||
|
makerFee: '200',
|
||||||
|
infrastructureFee: '200',
|
||||||
|
liquidityFee: '200',
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return merge(defaultFill, override);
|
||||||
|
};
|
@ -1103,3 +1103,16 @@ export const generateMarkets = (override?): Markets => {
|
|||||||
|
|
||||||
return merge(defaultResult, override);
|
return merge(defaultResult, override);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateFillsMarkets = () => {
|
||||||
|
const marketIds = ['market-0', 'market-1', 'market-2', 'market-4'];
|
||||||
|
return {
|
||||||
|
marketsConnection: {
|
||||||
|
__typename: 'MarketConnection',
|
||||||
|
edges: marketIds.map((id) => ({
|
||||||
|
__typename: 'MarketEdge',
|
||||||
|
node: { ...protoMarket, id },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
139
apps/console-lite-e2e/src/support/mocks/generate-orders.ts
Normal file
139
apps/console-lite-e2e/src/support/mocks/generate-orders.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type {
|
||||||
|
Orders,
|
||||||
|
Orders_party_ordersConnection_edges_node,
|
||||||
|
} from '@vegaprotocol/orders';
|
||||||
|
import {
|
||||||
|
OrderStatus,
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderType,
|
||||||
|
Side,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export const generateOrders = (override?: PartialDeep<Orders>): Orders => {
|
||||||
|
const orders: Orders_party_ordersConnection_edges_node[] = [
|
||||||
|
{
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-0',
|
||||||
|
},
|
||||||
|
size: '10',
|
||||||
|
type: OrderType.TYPE_LIMIT,
|
||||||
|
status: OrderStatus.STATUS_FILLED,
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
remaining: '0',
|
||||||
|
price: '20000000',
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
createdAt: new Date(2020, 1, 30).toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '48DB6767E4E4E0F649C5A13ABFADE39F8451C27DA828DAF14B7A1E8E5EBDAD99',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-1',
|
||||||
|
},
|
||||||
|
size: '1',
|
||||||
|
type: OrderType.TYPE_LIMIT,
|
||||||
|
status: OrderStatus.STATUS_FILLED,
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
remaining: '0',
|
||||||
|
price: '100',
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
createdAt: new Date(2020, 1, 29).toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '4e93702990712c41f6995fcbbd94f60bb372ad12d64dfa7d96d205c49f790336',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-2',
|
||||||
|
},
|
||||||
|
size: '1',
|
||||||
|
type: OrderType.TYPE_LIMIT,
|
||||||
|
status: OrderStatus.STATUS_FILLED,
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
remaining: '0',
|
||||||
|
price: '20000',
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
createdAt: new Date(2020, 1, 28).toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '94737d2bafafa4bc3b80a56ef084ae52a983b91aa067c31e243c61a0f962a836',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-3',
|
||||||
|
},
|
||||||
|
size: '1',
|
||||||
|
type: OrderType.TYPE_LIMIT,
|
||||||
|
status: OrderStatus.STATUS_ACTIVE,
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
remaining: '0',
|
||||||
|
price: '100000',
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
createdAt: new Date(2020, 1, 27).toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Order',
|
||||||
|
id: '94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-3',
|
||||||
|
},
|
||||||
|
size: '10',
|
||||||
|
type: OrderType.TYPE_LIMIT,
|
||||||
|
status: OrderStatus.STATUS_PARTIALLY_FILLED,
|
||||||
|
side: Side.SIDE_SELL,
|
||||||
|
remaining: '3',
|
||||||
|
price: '100000',
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||||
|
createdAt: new Date(2020, 1, 27).toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
rejectionReason: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultResult: Orders = {
|
||||||
|
party: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
ordersConnection: {
|
||||||
|
__typename: 'OrderConnection',
|
||||||
|
edges: orders.map((f) => {
|
||||||
|
return {
|
||||||
|
__typename: 'OrderEdge',
|
||||||
|
node: f,
|
||||||
|
cursor: f.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
pageInfo: {
|
||||||
|
__typename: 'PageInfo',
|
||||||
|
startCursor:
|
||||||
|
'066468C06549101DAF7BC51099E1412A0067DC08C246B7D8013C9D0CBF1E8EE7',
|
||||||
|
endCursor:
|
||||||
|
'94737d2bafafa4bc3b80a56ef084ae52a983b91aa067c31e243c61a0f962a836',
|
||||||
|
hasNextPage: false,
|
||||||
|
hasPreviousPage: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__typename: 'Party',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return merge(defaultResult, override);
|
||||||
|
};
|
191
apps/console-lite-e2e/src/support/mocks/generate-positions.ts
Normal file
191
apps/console-lite-e2e/src/support/mocks/generate-positions.ts
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type {
|
||||||
|
Positions,
|
||||||
|
Positions_party_positionsConnection_edges_node,
|
||||||
|
} from '@vegaprotocol/positions';
|
||||||
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export const generatePositions = (
|
||||||
|
override?: PartialDeep<Positions>
|
||||||
|
): Positions => {
|
||||||
|
const nodes: Positions_party_positionsConnection_edges_node[] = [
|
||||||
|
{
|
||||||
|
__typename: 'Position',
|
||||||
|
realisedPNL: '0',
|
||||||
|
openVolume: '6',
|
||||||
|
unrealisedPNL: '895000',
|
||||||
|
averageEntryPrice: '1129935',
|
||||||
|
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||||
|
marginsConnection: {
|
||||||
|
__typename: 'MarginConnection',
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
__typename: 'MarginEdge',
|
||||||
|
node: {
|
||||||
|
__typename: 'MarginLevels',
|
||||||
|
maintenanceLevel: '0',
|
||||||
|
searchLevel: '0',
|
||||||
|
initialLevel: '0',
|
||||||
|
collateralReleaseLevel: '0',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
symbol: 'tDAI',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||||
|
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
|
data: {
|
||||||
|
markPrice: '17588787',
|
||||||
|
__typename: 'MarketData',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decimalPlaces: 5,
|
||||||
|
positionDecimalPlaces: 0,
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'UNIDAI Monthly (30 Jun 2022)',
|
||||||
|
__typename: 'Instrument',
|
||||||
|
},
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
},
|
||||||
|
__typename: 'Market',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'Position',
|
||||||
|
realisedPNL: '100',
|
||||||
|
openVolume: '20',
|
||||||
|
unrealisedPNL: '895000',
|
||||||
|
averageEntryPrice: '8509338',
|
||||||
|
updatedAt: '2022-07-28T15:09:34.441143Z',
|
||||||
|
marginsConnection: {
|
||||||
|
__typename: 'MarginConnection',
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
__typename: 'MarginEdge',
|
||||||
|
node: {
|
||||||
|
__typename: 'MarginLevels',
|
||||||
|
maintenanceLevel: '0',
|
||||||
|
searchLevel: '0',
|
||||||
|
initialLevel: '0',
|
||||||
|
collateralReleaseLevel: '0',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: '0604e8c918655474525e1a95367902266ade70d318c2c908f0cca6e3d11dcb13',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
symbol: 'tDAI',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
id: '0604e8c918655474525e1a95367902266ade70d318c2c908f0cca6e3d11dcb13',
|
||||||
|
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
|
data: {
|
||||||
|
markPrice: '8649338',
|
||||||
|
__typename: 'MarketData',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: '0604e8c918655474525e1a95367902266ade70d318c2c908f0cca6e3d11dcb13',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decimalPlaces: 5,
|
||||||
|
positionDecimalPlaces: 0,
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'AAVEDAI Monthly (30 Jun 2022)',
|
||||||
|
__typename: 'Instrument',
|
||||||
|
},
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
},
|
||||||
|
__typename: 'Market',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
realisedPNL: '0',
|
||||||
|
openVolume: '1',
|
||||||
|
unrealisedPNL: '-22519',
|
||||||
|
averageEntryPrice: '84400088',
|
||||||
|
updatedAt: '2022-07-28T14:53:54.725477Z',
|
||||||
|
marginsConnection: {
|
||||||
|
__typename: 'MarginConnection',
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
__typename: 'MarginEdge',
|
||||||
|
node: {
|
||||||
|
__typename: 'MarginLevels',
|
||||||
|
maintenanceLevel: '0',
|
||||||
|
searchLevel: '0',
|
||||||
|
initialLevel: '0',
|
||||||
|
collateralReleaseLevel: '0',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||||
|
},
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
symbol: 'tEURO',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
market: {
|
||||||
|
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||||
|
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
|
data: {
|
||||||
|
markPrice: '84377569',
|
||||||
|
__typename: 'MarketData',
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: '5a4b0b9e9c0629f0315ec56fcb7bd444b0c6e4da5ec7677719d502626658a376',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decimalPlaces: 5,
|
||||||
|
positionDecimalPlaces: 0,
|
||||||
|
tradableInstrument: {
|
||||||
|
instrument: {
|
||||||
|
name: 'Tesla Quarterly (30 Jun 2022)',
|
||||||
|
__typename: 'Instrument',
|
||||||
|
},
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
},
|
||||||
|
__typename: 'Market',
|
||||||
|
},
|
||||||
|
__typename: 'Position',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultResult: Positions = {
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
positionsConnection: {
|
||||||
|
__typename: 'PositionConnection',
|
||||||
|
edges: nodes.map((node) => {
|
||||||
|
return {
|
||||||
|
__typename: 'PositionEdge',
|
||||||
|
node,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return merge(defaultResult, override);
|
||||||
|
};
|
@ -0,0 +1,107 @@
|
|||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
t,
|
||||||
|
ThemeContext,
|
||||||
|
useScreenDimensions,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
GridOptions,
|
||||||
|
GetRowIdParams,
|
||||||
|
TabToNextCellParams,
|
||||||
|
CellKeyDownEvent,
|
||||||
|
FullWidthCellKeyDownEvent,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import * as constants from '../simple-market-list/constants';
|
||||||
|
|
||||||
|
interface Props<T> extends GridOptions {
|
||||||
|
data?: T[];
|
||||||
|
handleRowClicked?: (event: { data: T }) => void;
|
||||||
|
components?: Record<string, unknown>;
|
||||||
|
classNamesParam?: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConsoleLiteGrid = <T extends { id?: string }>(
|
||||||
|
{ data, handleRowClicked, getRowId, classNamesParam, ...props }: Props<T>,
|
||||||
|
ref?: React.Ref<AgGridReact>
|
||||||
|
) => {
|
||||||
|
const { isMobile, screenSize } = useScreenDimensions();
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
const handleOnGridReady = useCallback(() => {
|
||||||
|
(
|
||||||
|
(ref as React.RefObject<AgGridReact>) || gridRef
|
||||||
|
).current?.api?.sizeColumnsToFit();
|
||||||
|
}, [gridRef, ref]);
|
||||||
|
const onTabToNextCell = useCallback((params: TabToNextCellParams) => {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
previousCellPosition: { rowIndex },
|
||||||
|
} = params;
|
||||||
|
const rowCount = api.getDisplayedRowCount();
|
||||||
|
if (rowCount <= rowIndex + 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { ...params.previousCellPosition, rowIndex: rowIndex + 1 };
|
||||||
|
}, []);
|
||||||
|
const getRowIdLocal = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
||||||
|
const onCellKeyDown = useCallback(
|
||||||
|
(
|
||||||
|
params: (CellKeyDownEvent | FullWidthCellKeyDownEvent) & {
|
||||||
|
event: KeyboardEvent;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { event: { key = '' } = {}, data } = params;
|
||||||
|
if (key === 'Enter') {
|
||||||
|
handleRowClicked?.({ data });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleRowClicked]
|
||||||
|
);
|
||||||
|
const shouldSuppressHorizontalScroll = useMemo(() => {
|
||||||
|
return !isMobile && constants.LARGE_SCREENS.includes(screenSize);
|
||||||
|
}, [isMobile, screenSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AgGrid
|
||||||
|
className={classNames(classNamesParam)}
|
||||||
|
rowData={data}
|
||||||
|
rowHeight={60}
|
||||||
|
customThemeParams={
|
||||||
|
theme === 'dark'
|
||||||
|
? constants.agGridDarkVariables
|
||||||
|
: constants.agGridLightVariables
|
||||||
|
}
|
||||||
|
onGridReady={handleOnGridReady}
|
||||||
|
onRowClicked={handleRowClicked}
|
||||||
|
rowClass={isMobile ? 'mobile' : ''}
|
||||||
|
rowClassRules={constants.ROW_CLASS_RULES}
|
||||||
|
ref={ref || gridRef}
|
||||||
|
overlayNoRowsTemplate={t('No data to display')}
|
||||||
|
suppressContextMenu
|
||||||
|
getRowId={getRowId || getRowIdLocal}
|
||||||
|
suppressMovableColumns
|
||||||
|
suppressRowTransform
|
||||||
|
onCellKeyDown={onCellKeyDown}
|
||||||
|
tabToNextCell={onTabToNextCell}
|
||||||
|
suppressHorizontalScroll={shouldSuppressHorizontalScroll}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConsoleLiteGridForwarder = forwardRef(ConsoleLiteGrid) as <
|
||||||
|
T extends { id?: string }
|
||||||
|
>(
|
||||||
|
p: Props<T> & { ref?: React.Ref<AgGridReact> }
|
||||||
|
) => React.ReactElement;
|
||||||
|
|
||||||
|
export default ConsoleLiteGridForwarder;
|
@ -0,0 +1 @@
|
|||||||
|
export { default as ConsoleLiteGrid } from './console-lite-grid';
|
@ -0,0 +1,103 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
url?: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
color?: string;
|
||||||
|
cssClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
active?: string;
|
||||||
|
cssClass?: string[];
|
||||||
|
items: Item[];
|
||||||
|
'data-testid'?: string;
|
||||||
|
'aria-label'?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuItem = ({ id, name, url, isActive, cssClass }: Item): JSX.Element => {
|
||||||
|
if (!url) {
|
||||||
|
return <span data-testid={id}>{name}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={url}
|
||||||
|
aria-label={name}
|
||||||
|
className={classNames('pl-0 hover:opacity-75', cssClass, {
|
||||||
|
active: isActive,
|
||||||
|
})}
|
||||||
|
data-testid={id}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findActive = (active?: string, items?: Item[]) => {
|
||||||
|
if (!active && items?.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return items?.length
|
||||||
|
? items.findIndex((item) => item.id === active) || 0
|
||||||
|
: -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HorizontalMenu = ({
|
||||||
|
items,
|
||||||
|
active,
|
||||||
|
'data-testid': dataTestId,
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeNumber, setActiveNumber] = useState<number>(
|
||||||
|
findActive(active, items)
|
||||||
|
);
|
||||||
|
const slideContRef = useRef<HTMLUListElement | null>(null);
|
||||||
|
const [sliderStyles, setSliderStyles] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveNumber(findActive(active, items));
|
||||||
|
}, [active, items]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const contStyles = (
|
||||||
|
slideContRef.current as HTMLUListElement
|
||||||
|
).getBoundingClientRect();
|
||||||
|
const selectedStyles = (slideContRef.current as HTMLUListElement).children[
|
||||||
|
activeNumber
|
||||||
|
]?.getBoundingClientRect();
|
||||||
|
const styles: Record<string, string> = selectedStyles
|
||||||
|
? {
|
||||||
|
backgroundColor: items[activeNumber].color || '',
|
||||||
|
width: `${selectedStyles.width}px`,
|
||||||
|
left: `${selectedStyles.left - contStyles.left}px`,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
setSliderStyles(styles);
|
||||||
|
}, [activeNumber, slideContRef, items]);
|
||||||
|
|
||||||
|
return items.length ? (
|
||||||
|
<ul
|
||||||
|
ref={slideContRef}
|
||||||
|
className="grid grid-flow-col auto-cols-min gap-4 relative pb-2 mb-2"
|
||||||
|
data-testid={dataTestId}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<li key={item.id} className="md:mr-2 whitespace-nowrap">
|
||||||
|
<MenuItem {...item} isActive={i === activeNumber} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className="absolute bottom-0 h-[2px] transition-left duration-300 dark:bg-white bg-black"
|
||||||
|
key="slider"
|
||||||
|
style={sliderStyles}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HorizontalMenu;
|
@ -0,0 +1,2 @@
|
|||||||
|
export { default as HorizontalMenu } from './horizontal-menu';
|
||||||
|
export type { Item as HorizontalMenuItem } from './horizontal-menu';
|
@ -0,0 +1,63 @@
|
|||||||
|
import { useMemo, useRef } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { PriceCell, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
AccountFieldsFragment,
|
||||||
|
AccountEventsSubscription,
|
||||||
|
} from '@vegaprotocol/accounts';
|
||||||
|
import {
|
||||||
|
accountsDataProvider,
|
||||||
|
accountsManagerUpdate,
|
||||||
|
getId,
|
||||||
|
} from '@vegaprotocol/accounts';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
AssetDetailsDialog,
|
||||||
|
useAssetDetailsDialogStore,
|
||||||
|
} from '@vegaprotocol/assets';
|
||||||
|
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||||
|
import { useAccountColumnDefinitions } from '.';
|
||||||
|
|
||||||
|
interface AccountObj extends AccountFieldsFragment {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountsManager = ({ partyId }: Props) => {
|
||||||
|
const {
|
||||||
|
isAssetDetailsDialogOpen,
|
||||||
|
assetDetailsDialogSymbol,
|
||||||
|
setAssetDetailsDialogOpen,
|
||||||
|
} = useAssetDetailsDialogStore();
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
const update = accountsManagerUpdate(gridRef);
|
||||||
|
const { data, error, loading } = useDataProvider<
|
||||||
|
AccountFieldsFragment[],
|
||||||
|
AccountEventsSubscription['accounts']
|
||||||
|
>({ dataProvider: accountsDataProvider, update, variables });
|
||||||
|
const { columnDefs, defaultColDef } = useAccountColumnDefinitions();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<ConsoleLiteGrid<AccountObj>
|
||||||
|
data={data as AccountObj[]}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
components={{ PriceCell }}
|
||||||
|
getRowId={({ data }) => getId(data)}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
<AssetDetailsDialog
|
||||||
|
assetSymbol={assetDetailsDialogSymbol}
|
||||||
|
open={isAssetDetailsDialogOpen}
|
||||||
|
onChange={(open) => setAssetDetailsDialogOpen(open)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccountsManager;
|
@ -0,0 +1,2 @@
|
|||||||
|
export { default as AccountManager } from './accounts';
|
||||||
|
export { default as useAccountColumnDefinitions } from './use-column-definitions';
|
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { SummaryRow } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
||||||
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
|
import type {
|
||||||
|
ColDef,
|
||||||
|
GroupCellRendererParams,
|
||||||
|
ValueFormatterParams,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import type { AccountType } from '@vegaprotocol/types';
|
||||||
|
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
interface AccountsTableValueFormatterParams extends ValueFormatterParams {
|
||||||
|
data: AccountFieldsFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparator = (
|
||||||
|
valueA: string,
|
||||||
|
valueB: string,
|
||||||
|
nodeA: { data: AccountFieldsFragment & SummaryRow },
|
||||||
|
nodeB: { data: AccountFieldsFragment & SummaryRow },
|
||||||
|
isInverted: boolean
|
||||||
|
) => {
|
||||||
|
if (valueA < valueB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueA > valueB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeA.data.__summaryRow) {
|
||||||
|
return isInverted ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeB.data.__summaryRow) {
|
||||||
|
return isInverted ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAccountColumnDefinitions = () => {
|
||||||
|
const { setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol } =
|
||||||
|
useAssetDetailsDialogStore();
|
||||||
|
const columnDefs: ColDef[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
colId: 'account-asset',
|
||||||
|
headerName: t('Asset'),
|
||||||
|
field: 'asset.symbol',
|
||||||
|
comparator,
|
||||||
|
headerClass: 'uppercase justify-start',
|
||||||
|
cellClass: 'uppercase flex h-full items-center md:pl-4',
|
||||||
|
cellRenderer: ({ value }: GroupCellRendererParams) =>
|
||||||
|
value && value.length > 0 ? (
|
||||||
|
<div className="md:pl-4 grid h-full items-center" title={value}>
|
||||||
|
<div className="truncate min-w-0">
|
||||||
|
<button
|
||||||
|
className="hover:underline"
|
||||||
|
onClick={() => {
|
||||||
|
setAssetDetailsDialogOpen(true);
|
||||||
|
setAssetDetailsDialogSymbol(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'type',
|
||||||
|
headerName: t('Type'),
|
||||||
|
field: 'type',
|
||||||
|
cellClass: 'uppercase !flex h-full items-center',
|
||||||
|
valueFormatter: ({ value }: ValueFormatterParams) =>
|
||||||
|
value ? AccountTypeMapping[value as AccountType] : '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'market',
|
||||||
|
headerName: t('Market'),
|
||||||
|
cellClass: 'uppercase !flex h-full items-center',
|
||||||
|
field: 'market.tradableInstrument.instrument.name',
|
||||||
|
valueFormatter: "value || '—'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'balance',
|
||||||
|
headerName: t('Balance'),
|
||||||
|
field: 'balance',
|
||||||
|
cellClass: 'uppercase !flex h-full items-center',
|
||||||
|
cellRenderer: 'PriceCell',
|
||||||
|
valueFormatter: ({ value, data }: AccountsTableValueFormatterParams) =>
|
||||||
|
addDecimalsFormatNumber(value, data.asset.decimals),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [setAssetDetailsDialogOpen, setAssetDetailsDialogSymbol]);
|
||||||
|
|
||||||
|
const defaultColDef = useMemo(() => {
|
||||||
|
return {
|
||||||
|
sortable: true,
|
||||||
|
unSortIcon: true,
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
editable: false,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return { columnDefs, defaultColDef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAccountColumnDefinitions;
|
35
apps/console-lite/src/app/components/portfolio/constants.ts
Normal file
35
apps/console-lite/src/app/components/portfolio/constants.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
export const PORTFOLIO_ASSETS = 'assets';
|
||||||
|
export const PORTFOLIO_POSITIONS = 'positions';
|
||||||
|
export const PORTFOLIO_ORDERS = 'orders';
|
||||||
|
export const PORTFOLIO_FILLS = 'fills';
|
||||||
|
export const PORTFOLIO_DEPOSITS = 'deposits';
|
||||||
|
|
||||||
|
export const PORTFOLIO_ITEMS = [
|
||||||
|
{
|
||||||
|
name: t('Assets'),
|
||||||
|
id: PORTFOLIO_ASSETS,
|
||||||
|
url: `/portfolio/${PORTFOLIO_ASSETS}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('Positions'),
|
||||||
|
id: PORTFOLIO_POSITIONS,
|
||||||
|
url: `/portfolio/${PORTFOLIO_POSITIONS}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('Orders'),
|
||||||
|
id: PORTFOLIO_ORDERS,
|
||||||
|
url: `/portfolio/${PORTFOLIO_ORDERS}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('Fills'),
|
||||||
|
id: PORTFOLIO_FILLS,
|
||||||
|
url: `/portfolio/${PORTFOLIO_FILLS}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: t('Deposits'),
|
||||||
|
id: PORTFOLIO_DEPOSITS,
|
||||||
|
url: `/portfolio/${PORTFOLIO_DEPOSITS}`,
|
||||||
|
},
|
||||||
|
];
|
@ -0,0 +1,49 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { TradeWithMarket } from '@vegaprotocol/fills';
|
||||||
|
import { useFillsList } from '@vegaprotocol/fills';
|
||||||
|
import type { BodyScrollEndEvent, BodyScrollEvent } from 'ag-grid-community';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||||
|
import useColumnDefinitions from './use-column-definitions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FillsManager = ({ partyId }: Props) => {
|
||||||
|
const { columnDefs, defaultColDef } = useColumnDefinitions({ partyId });
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
const { data, error, loading, addNewRows, getRows } = useFillsList({
|
||||||
|
partyId,
|
||||||
|
gridRef,
|
||||||
|
scrolledToTop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<ConsoleLiteGrid<TradeWithMarket>
|
||||||
|
ref={gridRef}
|
||||||
|
rowModelType="infinite"
|
||||||
|
datasource={{ getRows }}
|
||||||
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FillsManager;
|
@ -0,0 +1 @@
|
|||||||
|
export { default as FillsManager } from './fills';
|
@ -0,0 +1,190 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { ColDef, ValueFormatterParams } from 'ag-grid-community';
|
||||||
|
import type { TradeWithMarket } from '@vegaprotocol/fills';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import {
|
||||||
|
addDecimal,
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
getDateTimeFormat,
|
||||||
|
negativeClassNames,
|
||||||
|
positiveClassNames,
|
||||||
|
t,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { Side } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useColumnDefinitions = ({ partyId }: Props) => {
|
||||||
|
const columnDefs: ColDef[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
colId: 'market',
|
||||||
|
headerName: t('Market'),
|
||||||
|
field: 'market.tradableInstrument.instrument.name',
|
||||||
|
cellClass: '!flex h-full items-center !md:pl-4',
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'size',
|
||||||
|
headerName: t('Size'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
field: 'size',
|
||||||
|
width: 100,
|
||||||
|
cellClass: ({ data }: { data: TradeWithMarket }) => {
|
||||||
|
return classNames('!flex h-full items-center justify-center', {
|
||||||
|
[positiveClassNames]: data?.buyer.id === partyId,
|
||||||
|
[negativeClassNames]: data?.seller.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data?.market) {
|
||||||
|
let prefix;
|
||||||
|
if (data.buyer.id === partyId) {
|
||||||
|
prefix = '+';
|
||||||
|
} else if (data.seller.id) {
|
||||||
|
prefix = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = addDecimalsFormatNumber(
|
||||||
|
value,
|
||||||
|
data.market.positionDecimalPlaces
|
||||||
|
);
|
||||||
|
return `${prefix}${size}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'value',
|
||||||
|
headerName: t('Value'),
|
||||||
|
headerClass: 'uppercase text-center',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'price',
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data?.market) {
|
||||||
|
const asset =
|
||||||
|
data.market.tradableInstrument.instrument.product.settlementAsset
|
||||||
|
.symbol;
|
||||||
|
const valueFormatted = addDecimalsFormatNumber(
|
||||||
|
value,
|
||||||
|
data.market.decimalPlaces
|
||||||
|
);
|
||||||
|
return `${valueFormatted} ${asset}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
type: 'rightAligned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'filledvalue',
|
||||||
|
headerName: t('Filled value'),
|
||||||
|
headerClass: 'uppercase text-center',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'price',
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data?.market) {
|
||||||
|
const asset =
|
||||||
|
data.market.tradableInstrument.instrument.product.settlementAsset
|
||||||
|
.symbol;
|
||||||
|
const size = new BigNumber(
|
||||||
|
addDecimal(data.size, data.market.positionDecimalPlaces)
|
||||||
|
);
|
||||||
|
const price = new BigNumber(
|
||||||
|
addDecimal(value, data.market.decimalPlaces)
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = size.times(price).toString();
|
||||||
|
const valueFormatted = formatNumber(
|
||||||
|
total,
|
||||||
|
data?.market.decimalPlaces
|
||||||
|
);
|
||||||
|
return `${valueFormatted} ${asset}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
type: 'rightAligned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'role',
|
||||||
|
headerName: t('Role'),
|
||||||
|
field: 'aggressor',
|
||||||
|
width: 100,
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data) {
|
||||||
|
const taker = t('Taker');
|
||||||
|
const maker = t('Maker');
|
||||||
|
if (data?.buyer.id === partyId) {
|
||||||
|
if (value === Side.SIDE_BUY) {
|
||||||
|
return taker;
|
||||||
|
} else {
|
||||||
|
return maker;
|
||||||
|
}
|
||||||
|
} else if (data?.seller.id === partyId) {
|
||||||
|
if (value === Side.SIDE_SELL) {
|
||||||
|
return taker;
|
||||||
|
} else {
|
||||||
|
return maker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'fee',
|
||||||
|
headerName: t('Fee'),
|
||||||
|
headerClass: 'uppercase text-center',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'market.tradableInstrument.instrument.product',
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data) {
|
||||||
|
const asset = value.settlementAsset;
|
||||||
|
let feesObj;
|
||||||
|
if (data.buyer.id === partyId) {
|
||||||
|
feesObj = data.buyerFee;
|
||||||
|
} else if (data?.seller.id === partyId) {
|
||||||
|
feesObj = data?.sellerFee;
|
||||||
|
} else {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fee = new BigNumber(feesObj.makerFee)
|
||||||
|
.plus(feesObj.infrastructureFee)
|
||||||
|
.plus(feesObj.liquidityFee);
|
||||||
|
const totalFees = addDecimalsFormatNumber(
|
||||||
|
fee.toString(),
|
||||||
|
asset.decimals
|
||||||
|
);
|
||||||
|
return `${totalFees} ${asset.symbol}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
type: 'rightAligned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'date',
|
||||||
|
headerName: t('Date'),
|
||||||
|
field: 'createdAt',
|
||||||
|
valueFormatter: ({ value }: ValueFormatterParams) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [partyId]);
|
||||||
|
|
||||||
|
const defaultColDef = useMemo(() => {
|
||||||
|
return {
|
||||||
|
sortable: true,
|
||||||
|
unSortIcon: true,
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return { columnDefs, defaultColDef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useColumnDefinitions;
|
@ -0,0 +1 @@
|
|||||||
|
export { default as OrdersManager } from './orders';
|
@ -0,0 +1,96 @@
|
|||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { BodyScrollEndEvent, BodyScrollEvent } from 'ag-grid-community';
|
||||||
|
import type { OrderWithMarket } from '@vegaprotocol/orders';
|
||||||
|
import {
|
||||||
|
useOrderCancel,
|
||||||
|
useOrderListData,
|
||||||
|
useOrderEdit,
|
||||||
|
OrderFeedback,
|
||||||
|
getCancelDialogTitle,
|
||||||
|
getCancelDialogIntent,
|
||||||
|
getEditDialogTitle,
|
||||||
|
OrderEditDialog,
|
||||||
|
} from '@vegaprotocol/orders';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||||
|
import useColumnDefinitions from './use-column-definitions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrdersManager = ({ partyId }: Props) => {
|
||||||
|
const [editOrder, setEditOrder] = useState<OrderWithMarket | null>(null);
|
||||||
|
const orderCancel = useOrderCancel();
|
||||||
|
const orderEdit = useOrderEdit(editOrder);
|
||||||
|
const { columnDefs, defaultColDef } = useColumnDefinitions({
|
||||||
|
setEditOrder,
|
||||||
|
orderCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const scrolledToTop = useRef(true);
|
||||||
|
|
||||||
|
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||||
|
partyId,
|
||||||
|
gridRef,
|
||||||
|
scrolledToTop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
|
if (event.top === 0) {
|
||||||
|
addNewRows();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBodyScroll = (event: BodyScrollEvent) => {
|
||||||
|
scrolledToTop.current = event.top <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<ConsoleLiteGrid<OrderWithMarket>
|
||||||
|
ref={gridRef}
|
||||||
|
rowModelType="infinite"
|
||||||
|
datasource={{ getRows }}
|
||||||
|
onBodyScrollEnd={onBodyScrollEnd}
|
||||||
|
onBodyScroll={onBodyScroll}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
/>
|
||||||
|
<orderCancel.Dialog
|
||||||
|
title={getCancelDialogTitle(orderCancel.cancelledOrder?.status)}
|
||||||
|
intent={getCancelDialogIntent(orderCancel.cancelledOrder?.status)}
|
||||||
|
>
|
||||||
|
<OrderFeedback
|
||||||
|
transaction={orderCancel.transaction}
|
||||||
|
order={orderCancel.cancelledOrder}
|
||||||
|
/>
|
||||||
|
</orderCancel.Dialog>
|
||||||
|
<orderEdit.Dialog
|
||||||
|
title={getEditDialogTitle(orderEdit.updatedOrder?.status)}
|
||||||
|
>
|
||||||
|
<OrderFeedback
|
||||||
|
transaction={orderEdit.transaction}
|
||||||
|
order={orderEdit.updatedOrder}
|
||||||
|
/>
|
||||||
|
</orderEdit.Dialog>
|
||||||
|
{editOrder && (
|
||||||
|
<OrderEditDialog
|
||||||
|
isOpen={Boolean(editOrder)}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
if (!isOpen) setEditOrder(null);
|
||||||
|
}}
|
||||||
|
order={editOrder}
|
||||||
|
onSubmit={(fields) => {
|
||||||
|
setEditOrder(null);
|
||||||
|
orderEdit.edit({ price: fields.entryPrice });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrdersManager;
|
@ -0,0 +1,287 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type {
|
||||||
|
ColDef,
|
||||||
|
ValueFormatterParams,
|
||||||
|
ICellRendererParams,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import {
|
||||||
|
addDecimal,
|
||||||
|
getDateTimeFormat,
|
||||||
|
negativeClassNames,
|
||||||
|
positiveClassNames,
|
||||||
|
t,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
Orders_party_ordersConnection_edges_node,
|
||||||
|
OrderWithMarket,
|
||||||
|
CancelOrderArgs,
|
||||||
|
} from '@vegaprotocol/orders';
|
||||||
|
import { isOrderActive } from '@vegaprotocol/orders';
|
||||||
|
import {
|
||||||
|
OrderRejectionReasonMapping,
|
||||||
|
OrderStatus,
|
||||||
|
OrderType,
|
||||||
|
OrderStatusMapping,
|
||||||
|
OrderTypeMapping,
|
||||||
|
Side,
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderTimeInForceMapping,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
type StatusKey = keyof typeof OrderStatusMapping;
|
||||||
|
type RejectReasonKey = keyof typeof OrderRejectionReasonMapping;
|
||||||
|
type OrderTimeKey = keyof typeof OrderTimeInForceMapping;
|
||||||
|
interface Props {
|
||||||
|
setEditOrder: (order: OrderWithMarket) => void;
|
||||||
|
orderCancel: {
|
||||||
|
cancel: (args: CancelOrderArgs) => void;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const useColumnDefinitions = ({ setEditOrder, orderCancel }: Props) => {
|
||||||
|
const columnDefs: ColDef[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
colId: 'market',
|
||||||
|
headerName: t('Market'),
|
||||||
|
field: 'market.tradableInstrument.instrument.code',
|
||||||
|
headerClass: 'uppercase justify-start',
|
||||||
|
cellClass: '!flex h-full items-center !md:pl-4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'size',
|
||||||
|
headerName: t('Size'),
|
||||||
|
field: 'size',
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: 'font-mono !flex h-full items-center',
|
||||||
|
width: 80,
|
||||||
|
cellClassRules: {
|
||||||
|
[positiveClassNames]: ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Orders_party_ordersConnection_edges_node;
|
||||||
|
}) => data?.side === Side.SIDE_BUY,
|
||||||
|
[negativeClassNames]: ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Orders_party_ordersConnection_edges_node;
|
||||||
|
}) => data?.side === Side.SIDE_SELL,
|
||||||
|
},
|
||||||
|
valueFormatter: ({ value, data }: ValueFormatterParams) => {
|
||||||
|
if (value && data && data.market) {
|
||||||
|
const prefix = data
|
||||||
|
? data.side === Side.SIDE_BUY
|
||||||
|
? '+'
|
||||||
|
: '-'
|
||||||
|
: '';
|
||||||
|
return (
|
||||||
|
prefix + addDecimal(value, data.market.positionDecimalPlaces)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'type',
|
||||||
|
field: 'type',
|
||||||
|
width: 80,
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: Orders_party_ordersConnection_edges_node['type'];
|
||||||
|
}) => OrderTypeMapping[value as OrderType],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'status',
|
||||||
|
field: 'status',
|
||||||
|
cellClass: 'text-center font-mono !flex h-full items-center',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: StatusKey;
|
||||||
|
}) => {
|
||||||
|
if (value && data && data.market) {
|
||||||
|
if (value === OrderStatus.STATUS_REJECTED) {
|
||||||
|
return `${OrderStatusMapping[value as StatusKey]}: ${
|
||||||
|
data.rejectionReason &&
|
||||||
|
OrderRejectionReasonMapping[
|
||||||
|
data.rejectionReason as RejectReasonKey
|
||||||
|
]
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return OrderStatusMapping[value as StatusKey] as string;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'filled',
|
||||||
|
headerName: t('Filled'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
field: 'remaining',
|
||||||
|
cellClass: 'font-mono text-center !flex h-full items-center',
|
||||||
|
width: 80,
|
||||||
|
valueFormatter: ({
|
||||||
|
data,
|
||||||
|
value,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: Orders_party_ordersConnection_edges_node['remaining'];
|
||||||
|
}) => {
|
||||||
|
if (value && data && data.market) {
|
||||||
|
const dps = data.market.positionDecimalPlaces;
|
||||||
|
const size = new BigNumber(data.size);
|
||||||
|
const remaining = new BigNumber(value);
|
||||||
|
const fills = size.minus(remaining);
|
||||||
|
return `${addDecimal(fills.toString(), dps)}/${addDecimal(
|
||||||
|
size.toString(),
|
||||||
|
dps
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'price',
|
||||||
|
field: 'price',
|
||||||
|
type: 'rightAligned',
|
||||||
|
width: 125,
|
||||||
|
headerClass: 'uppercase text-right',
|
||||||
|
cellClass: 'font-mono text-right !flex h-full items-center',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: Orders_party_ordersConnection_edges_node['price'];
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
value === undefined ||
|
||||||
|
!data ||
|
||||||
|
!data.market ||
|
||||||
|
data.type === OrderType.TYPE_MARKET
|
||||||
|
) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return addDecimal(value, data.market.decimalPlaces);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'timeInForce',
|
||||||
|
field: 'timeInForce',
|
||||||
|
minWidth: 120,
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: OrderTimeKey;
|
||||||
|
}) => {
|
||||||
|
if (value && data?.market) {
|
||||||
|
if (
|
||||||
|
value === OrderTimeInForce.TIME_IN_FORCE_GTT &&
|
||||||
|
data.expiresAt
|
||||||
|
) {
|
||||||
|
const expiry = getDateTimeFormat().format(
|
||||||
|
new Date(data.expiresAt)
|
||||||
|
);
|
||||||
|
return `${
|
||||||
|
OrderTimeInForceMapping[value as OrderTimeKey]
|
||||||
|
}: ${expiry}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrderTimeInForceMapping[value as OrderTimeKey];
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'createdat',
|
||||||
|
field: 'createdAt',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: Orders_party_ordersConnection_edges_node['createdAt'];
|
||||||
|
}) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'updated',
|
||||||
|
field: 'updatedAt',
|
||||||
|
cellClass: '!flex h-full items-center justify-center',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: ValueFormatterParams & {
|
||||||
|
value?: Orders_party_ordersConnection_edges_node['updatedAt'];
|
||||||
|
}) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'edit',
|
||||||
|
field: 'edit',
|
||||||
|
width: 75,
|
||||||
|
cellRenderer: ({ data }: ICellRendererParams) => {
|
||||||
|
if (!data) return null;
|
||||||
|
if (isOrderActive(data.status)) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
data-testid="edit"
|
||||||
|
onClick={() => {
|
||||||
|
setEditOrder(data);
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{t('Edit')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'cancel',
|
||||||
|
field: 'cancel',
|
||||||
|
minWidth: 130,
|
||||||
|
cellRenderer: ({ data }: ICellRendererParams) => {
|
||||||
|
if (!data) return null;
|
||||||
|
if (isOrderActive(data.status)) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
data-testid="cancel"
|
||||||
|
onClick={() => {
|
||||||
|
if (data.market) {
|
||||||
|
orderCancel.cancel({
|
||||||
|
orderId: data.id,
|
||||||
|
marketId: data.market.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [orderCancel, setEditOrder]);
|
||||||
|
const defaultColDef = useMemo(() => {
|
||||||
|
return {
|
||||||
|
sortable: true,
|
||||||
|
unSortIcon: true,
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return { columnDefs, defaultColDef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useColumnDefinitions;
|
@ -1,40 +1,54 @@
|
|||||||
import * as React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
import { AccountManager } from './accounts';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { Tabs, Tab } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { OrderListContainer } from '@vegaprotocol/orders';
|
|
||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
|
||||||
import { FillsContainer } from '@vegaprotocol/fills';
|
|
||||||
import ConnectWallet from '../wallet-connector';
|
import ConnectWallet from '../wallet-connector';
|
||||||
import { DepositContainer } from '../deposits';
|
import { DepositContainer } from '../deposits';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { HorizontalMenu } from '../horizontal-menu';
|
||||||
|
import * as constants from './constants';
|
||||||
|
import { PositionsManager } from './positions';
|
||||||
|
import { OrdersManager } from './orders';
|
||||||
|
import { FillsManager } from './fills';
|
||||||
|
|
||||||
|
type RouterParams = {
|
||||||
|
module?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const Portfolio = () => {
|
export const Portfolio = () => {
|
||||||
const { keypair } = useVegaWallet();
|
const { keypair } = useVegaWallet();
|
||||||
if (!keypair) {
|
const params = useParams<RouterParams>();
|
||||||
return (
|
|
||||||
<section className="xl:w-1/2">
|
const module = useMemo(() => {
|
||||||
<ConnectWallet />
|
if (!keypair) {
|
||||||
</section>
|
return (
|
||||||
);
|
<section className="xl:w-1/2">
|
||||||
}
|
<ConnectWallet />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (params?.module) {
|
||||||
|
case constants.PORTFOLIO_ASSETS:
|
||||||
|
default:
|
||||||
|
return <AccountManager partyId={keypair.pub} />;
|
||||||
|
case constants.PORTFOLIO_POSITIONS:
|
||||||
|
return <PositionsManager partyId={keypair.pub} />;
|
||||||
|
case constants.PORTFOLIO_ORDERS:
|
||||||
|
return <OrdersManager partyId={keypair.pub} />;
|
||||||
|
case constants.PORTFOLIO_FILLS:
|
||||||
|
return <FillsManager partyId={keypair.pub} />;
|
||||||
|
case constants.PORTFOLIO_DEPOSITS:
|
||||||
|
return <DepositContainer />;
|
||||||
|
}
|
||||||
|
}, [params?.module, keypair]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs>
|
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content_1fr]">
|
||||||
<Tab id="assets" name={t('Assets')}>
|
<HorizontalMenu
|
||||||
<AccountsContainer />
|
active={params?.module}
|
||||||
</Tab>
|
items={constants.PORTFOLIO_ITEMS}
|
||||||
<Tab id="positions" name={t('Positions')}>
|
/>
|
||||||
<PositionsContainer />
|
{module}
|
||||||
</Tab>
|
</div>
|
||||||
<Tab id="orders" name={t('Orders')}>
|
|
||||||
<OrderListContainer />
|
|
||||||
</Tab>
|
|
||||||
<Tab id="fills" name={t('Fills')}>
|
|
||||||
<FillsContainer />
|
|
||||||
</Tab>
|
|
||||||
<Tab id="deposits" name={t('Deposits')}>
|
|
||||||
<DepositContainer />
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export { default as PositionsManager } from './positions';
|
@ -0,0 +1,58 @@
|
|||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { Position } from '@vegaprotocol/positions';
|
||||||
|
import { PriceFlashCell, t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { AssetBalance } from '@vegaprotocol/accounts';
|
||||||
|
import { usePositionsData } from '@vegaprotocol/positions';
|
||||||
|
import { ConsoleLiteGrid } from '../../console-lite-grid';
|
||||||
|
import useColumnDefinitions from './use-column-definitions';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
assetSymbol: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRowId = ({ data }: { data: Position }) => data.marketId;
|
||||||
|
|
||||||
|
const PositionsAsset = ({ partyId, assetSymbol }: Props) => {
|
||||||
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
const { data, error, loading, getRows } = usePositionsData({
|
||||||
|
partyId,
|
||||||
|
assetSymbol,
|
||||||
|
gridRef,
|
||||||
|
});
|
||||||
|
const { columnDefs, defaultColDef } = useColumnDefinitions();
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<div
|
||||||
|
data-testid={`positions-asset-${assetSymbol}`}
|
||||||
|
className="flex justify-between items-center px-4 pt-3 pb-1"
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
{assetSymbol} {t('markets')}
|
||||||
|
</h4>
|
||||||
|
<div className="text-sm text-neutral-500 dark:text-neutral-300">
|
||||||
|
{assetSymbol} {t('balance')}:
|
||||||
|
<span data-testid="balance" className="pl-1 font-mono">
|
||||||
|
<AssetBalance partyId={partyId} assetSymbol={assetSymbol} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ConsoleLiteGrid<Position & { id: undefined }>
|
||||||
|
ref={gridRef}
|
||||||
|
domLayout="autoHeight"
|
||||||
|
classNamesParam="h-auto"
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColDef}
|
||||||
|
getRowId={getRowId}
|
||||||
|
rowModelType={data?.length ? 'infinite' : 'clientSide'}
|
||||||
|
data={data?.length ? undefined : []}
|
||||||
|
datasource={{ getRows }}
|
||||||
|
components={{ PriceFlashCell }}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PositionsAsset;
|
@ -0,0 +1,32 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { usePositionsAssets } from '@vegaprotocol/positions';
|
||||||
|
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import PositionsAsset from './positions-asset';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Positions = ({ partyId }: Props) => {
|
||||||
|
const { data, error, loading, assetSymbols } = usePositionsAssets({
|
||||||
|
partyId,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
{assetSymbols && assetSymbols.length && (
|
||||||
|
<div className="w-full, h-max">
|
||||||
|
{assetSymbols?.map((assetSymbol) => (
|
||||||
|
<PositionsAsset
|
||||||
|
key={assetSymbol}
|
||||||
|
partyId={partyId}
|
||||||
|
assetSymbol={assetSymbol}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{assetSymbols?.length === 0 && <Splash>{t('No data to display')}</Splash>}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Positions;
|
@ -0,0 +1,291 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
getDateTimeFormat,
|
||||||
|
PriceFlashCell,
|
||||||
|
signedNumberCssClassRules,
|
||||||
|
t,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
PositionsTableValueFormatterParams,
|
||||||
|
Position,
|
||||||
|
} from '@vegaprotocol/positions';
|
||||||
|
import { AmountCell, ProgressBarCell } from '@vegaprotocol/positions';
|
||||||
|
import type {
|
||||||
|
CellRendererSelectorResult,
|
||||||
|
ICellRendererParams,
|
||||||
|
ValueGetterParams,
|
||||||
|
GroupCellRendererParams,
|
||||||
|
ColDef,
|
||||||
|
} from 'ag-grid-community';
|
||||||
|
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||||
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
const EmptyCell = () => '';
|
||||||
|
|
||||||
|
const useColumnDefinitions = () => {
|
||||||
|
const columnDefs: ColDef[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
colId: 'market',
|
||||||
|
headerName: t('Market'),
|
||||||
|
headerClass: 'uppercase justify-start',
|
||||||
|
cellClass: '!flex h-full items-center !md:pl-4',
|
||||||
|
field: 'marketName',
|
||||||
|
cellRenderer: ({ value }: GroupCellRendererParams) => {
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const valueFormatted: [string, string?] = (() => {
|
||||||
|
// split market name into two parts, 'Part1 (Part2)' or 'Part1 - Part2'
|
||||||
|
const matches = value.match(/^(.*)(\((.*)\)| - (.*))\s*$/);
|
||||||
|
if (matches && matches[1] && matches[3]) {
|
||||||
|
return [matches[1].trim(), matches[3].trim()];
|
||||||
|
}
|
||||||
|
return [value];
|
||||||
|
})();
|
||||||
|
if (valueFormatted && valueFormatted[1]) {
|
||||||
|
return (
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div>{valueFormatted[0]}</div>
|
||||||
|
<div>{valueFormatted[1]}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return valueFormatted ? valueFormatted[0] : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'amount',
|
||||||
|
headerName: t('Amount'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'openVolume',
|
||||||
|
valueGetter: ({ node, data }: ValueGetterParams) => {
|
||||||
|
return node?.rowPinned ? data?.notional : data?.openVolume;
|
||||||
|
},
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellRendererSelector: (
|
||||||
|
params: ICellRendererParams
|
||||||
|
): CellRendererSelectorResult => {
|
||||||
|
return {
|
||||||
|
component: params.node.rowPinned ? PriceFlashCell : AmountCell,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
node,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['openVolume'];
|
||||||
|
}) => {
|
||||||
|
let ret;
|
||||||
|
if (value && data) {
|
||||||
|
ret = node?.rowPinned
|
||||||
|
? addDecimalsFormatNumber(value, data.decimals)
|
||||||
|
: data;
|
||||||
|
}
|
||||||
|
// FIXME this column needs refactoring
|
||||||
|
return ret as unknown as string;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'markprice',
|
||||||
|
headerName: t('Mark price'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center justify-center',
|
||||||
|
field: 'markPrice',
|
||||||
|
cellRendererSelector: (
|
||||||
|
params: ICellRendererParams
|
||||||
|
): CellRendererSelectorResult => {
|
||||||
|
return {
|
||||||
|
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
node,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['markPrice'];
|
||||||
|
}) => {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
value &&
|
||||||
|
node?.rowPinned &&
|
||||||
|
data.marketTradingMode ===
|
||||||
|
MarketTradingMode.TRADING_MODE_OPENING_AUCTION
|
||||||
|
) {
|
||||||
|
return addDecimalsFormatNumber(
|
||||||
|
value.toString(),
|
||||||
|
data.marketDecimalPlaces
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'entryprice',
|
||||||
|
headerName: t('Entry price'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'averageEntryPrice',
|
||||||
|
headerComponentParams: {
|
||||||
|
template:
|
||||||
|
'<div class="ag-cell-label-container" role="presentation">' +
|
||||||
|
` <span>${t('Liquidation price (est)')}</span>` +
|
||||||
|
' <span ref="eText" class="ag-header-cell-text"></span>' +
|
||||||
|
'</div>',
|
||||||
|
},
|
||||||
|
cellRenderer: ({ node, data }: GroupCellRendererParams) => {
|
||||||
|
const valueFormatted =
|
||||||
|
data && !node?.rowPinned
|
||||||
|
? (() => {
|
||||||
|
const min = BigInt(data.averageEntryPrice);
|
||||||
|
const max = BigInt(data.liquidationPrice);
|
||||||
|
const mid = BigInt(data.markPrice);
|
||||||
|
const range = max - min;
|
||||||
|
return {
|
||||||
|
low: addDecimalsFormatNumber(
|
||||||
|
min.toString(),
|
||||||
|
data.marketDecimalPlaces
|
||||||
|
),
|
||||||
|
high: addDecimalsFormatNumber(
|
||||||
|
max.toString(),
|
||||||
|
data.marketDecimalPlaces
|
||||||
|
),
|
||||||
|
value: range
|
||||||
|
? Number(((mid - min) * BigInt(100)) / range)
|
||||||
|
: 0,
|
||||||
|
intent: data.lowMarginLevel ? Intent.Warning : undefined,
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: undefined;
|
||||||
|
return node.rowPinned ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<ProgressBarCell valueFormatted={valueFormatted} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'leverage',
|
||||||
|
headerName: t('Leverage'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center justify-center',
|
||||||
|
field: 'currentLeverage',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellRendererSelector: (
|
||||||
|
params: ICellRendererParams
|
||||||
|
): CellRendererSelectorResult => {
|
||||||
|
return {
|
||||||
|
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
node,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['currentLeverage'];
|
||||||
|
}) => (value === undefined ? '' : formatNumber(value.toString(), 1)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'marginallocated',
|
||||||
|
headerName: t('Margin allocated'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full flex-col justify-center',
|
||||||
|
field: 'capitalUtilisation',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellRenderer: ({ value, node, data }: GroupCellRendererParams) => {
|
||||||
|
const valueFormatted =
|
||||||
|
data && value
|
||||||
|
? (() => {
|
||||||
|
return {
|
||||||
|
low: `${formatNumber(value, 2)}%`,
|
||||||
|
high: addDecimalsFormatNumber(
|
||||||
|
data.totalBalance,
|
||||||
|
data.decimals
|
||||||
|
),
|
||||||
|
value: Number(value),
|
||||||
|
};
|
||||||
|
})()
|
||||||
|
: undefined;
|
||||||
|
return node.rowPinned ? (
|
||||||
|
''
|
||||||
|
) : (
|
||||||
|
<ProgressBarCell valueFormatted={valueFormatted} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'realisedpnl',
|
||||||
|
headerName: t('Realised PNL'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'realisedPNL',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellClassRules: signedNumberCssClassRules,
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['realisedPNL'];
|
||||||
|
}) =>
|
||||||
|
value === undefined
|
||||||
|
? ''
|
||||||
|
: addDecimalsFormatNumber(value.toString(), data.decimals),
|
||||||
|
cellRenderer: 'PriceFlashCell',
|
||||||
|
headerTooltip: t('P&L excludes any fees paid.'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'unrealisedpnl',
|
||||||
|
headerName: t('Unrealised PNL'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'unrealisedPNL',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellClassRules: signedNumberCssClassRules,
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['unrealisedPNL'];
|
||||||
|
}) =>
|
||||||
|
value === undefined
|
||||||
|
? ''
|
||||||
|
: addDecimalsFormatNumber(value.toString(), data.decimals),
|
||||||
|
cellRenderer: 'PriceFlashCell',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'updated',
|
||||||
|
headerName: t('Updated'),
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
field: 'updatedAt',
|
||||||
|
type: 'rightAligned',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: PositionsTableValueFormatterParams & {
|
||||||
|
value: Position['updatedAt'];
|
||||||
|
}) => {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return getDateTimeFormat().format(new Date(value));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
const defaultColDef = useMemo(() => {
|
||||||
|
return {
|
||||||
|
sortable: true,
|
||||||
|
unSortIcon: true,
|
||||||
|
headerClass: 'uppercase',
|
||||||
|
cellClass: '!flex h-full items-center',
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return { columnDefs, defaultColDef };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useColumnDefinitions;
|
@ -1,9 +1,9 @@
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { themelite as theme } from '@vegaprotocol/tailwindcss-config';
|
import { themelite as theme } from '@vegaprotocol/tailwindcss-config';
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
|
||||||
import colors from 'tailwindcss/colors';
|
import colors from 'tailwindcss/colors';
|
||||||
import type { Market } from '@vegaprotocol/market-list';
|
import type { Market } from '@vegaprotocol/market-list';
|
||||||
|
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||||
|
|
||||||
export const STATES_FILTER = [
|
export const STATES_FILTER = [
|
||||||
{ value: 'all', text: t('All') },
|
{ value: 'all', text: t('All') },
|
||||||
@ -198,4 +198,11 @@ export const ROW_CLASS_RULES = {
|
|||||||
|
|
||||||
export const LARGE_SCREENS = ['xl', 'xxl'];
|
export const LARGE_SCREENS = ['xl', 'xxl'];
|
||||||
|
|
||||||
|
export const ALL_PRODUCTS_ITEM = {
|
||||||
|
name: t('All Markets'),
|
||||||
|
id: 'allmarkets',
|
||||||
|
cssClass: 'text-pink',
|
||||||
|
color: theme.colors.pink,
|
||||||
|
};
|
||||||
|
|
||||||
export const AG_GRID_CONTAINER_STYLES = { width: '100%', height: '100%' };
|
export const AG_GRID_CONTAINER_STYLES = { width: '100%', height: '100%' };
|
||||||
|
@ -1,25 +1,14 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { ThemeContext } from '@vegaprotocol/react-helpers';
|
|
||||||
import type { MarketState } from '@vegaprotocol/types';
|
import type { MarketState } from '@vegaprotocol/types';
|
||||||
import useMarketsFilterData from './use-markets-filter-data';
|
import useMarketsFilterData from './use-markets-filter-data';
|
||||||
import useColumnDefinitions from './use-column-definitions';
|
import useColumnDefinitions from './use-column-definitions';
|
||||||
import * as constants from './constants';
|
|
||||||
import SimpleMarketToolbar from './simple-market-toolbar';
|
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||||
import type {
|
import { ConsoleLiteGrid } from '../console-lite-grid';
|
||||||
CellKeyDownEvent,
|
|
||||||
FullWidthCellKeyDownEvent,
|
|
||||||
} from 'ag-grid-community/dist/lib/events';
|
|
||||||
import type {
|
|
||||||
GetRowIdParams,
|
|
||||||
TabToNextCellParams,
|
|
||||||
} from 'ag-grid-community/dist/lib/entities/iCallbackParams';
|
|
||||||
import type { Market, MarketsListData } from '@vegaprotocol/market-list';
|
import type { Market, MarketsListData } from '@vegaprotocol/market-list';
|
||||||
import { useMarketList } from '@vegaprotocol/market-list';
|
import { useMarketList } from '@vegaprotocol/market-list';
|
||||||
|
|
||||||
@ -34,10 +23,9 @@ export type RouterParams = Partial<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
const SimpleMarketList = () => {
|
const SimpleMarketList = () => {
|
||||||
const { isMobile, screenSize } = useScreenDimensions();
|
const { isMobile } = useScreenDimensions();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams<RouterParams>();
|
const params = useParams<RouterParams>();
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
const statusesRef = useRef<Record<string, MarketState | ''>>({});
|
const statusesRef = useRef<Record<string, MarketState | ''>>({});
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
|
|
||||||
@ -63,8 +51,6 @@ const SimpleMarketList = () => {
|
|||||||
|
|
||||||
const { columnDefs, defaultColDef } = useColumnDefinitions({ isMobile });
|
const { columnDefs, defaultColDef } = useColumnDefinitions({ isMobile });
|
||||||
|
|
||||||
const getRowId = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
|
||||||
|
|
||||||
const handleRowClicked = useCallback(
|
const handleRowClicked = useCallback(
|
||||||
({ data }: { data: Market }) => {
|
({ data }: { data: Market }) => {
|
||||||
if (IS_MARKET_TRADABLE(data)) {
|
if (IS_MARKET_TRADABLE(data)) {
|
||||||
@ -74,65 +60,16 @@ const SimpleMarketList = () => {
|
|||||||
[navigate]
|
[navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTabToNextCell = useCallback((params: TabToNextCellParams) => {
|
|
||||||
const {
|
|
||||||
api,
|
|
||||||
previousCellPosition: { rowIndex },
|
|
||||||
} = params;
|
|
||||||
const rowCount = api.getDisplayedRowCount();
|
|
||||||
if (rowCount <= rowIndex + 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return { ...params.previousCellPosition, rowIndex: rowIndex + 1 };
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onCellKeyDown = useCallback(
|
|
||||||
(
|
|
||||||
params: (CellKeyDownEvent | FullWidthCellKeyDownEvent) & {
|
|
||||||
event: KeyboardEvent;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
const { event: { key = '' } = {}, data } = params;
|
|
||||||
if (key === 'Enter') {
|
|
||||||
handleRowClicked({ data });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleRowClicked]
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldSuppressHorizontalScroll = useMemo(() => {
|
|
||||||
return !isMobile && constants.LARGE_SCREENS.includes(screenSize);
|
|
||||||
}, [isMobile, screenSize]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content,1fr]">
|
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content,1fr]">
|
||||||
<SimpleMarketToolbar data={data?.markets || []} />
|
<SimpleMarketToolbar data={data?.markets || []} />
|
||||||
<AsyncRenderer loading={loading} error={error} data={localData}>
|
<AsyncRenderer loading={loading} error={error} data={localData}>
|
||||||
<AgGrid
|
<ConsoleLiteGrid<MarketWithPercentChange>
|
||||||
className="mb-32 min-h-[300px] w-full"
|
classNamesParam="mb-32 min-h-[300px]"
|
||||||
style={constants.AG_GRID_CONTAINER_STYLES}
|
|
||||||
defaultColDef={defaultColDef}
|
|
||||||
columnDefs={columnDefs}
|
columnDefs={columnDefs}
|
||||||
rowData={localData}
|
data={localData}
|
||||||
rowHeight={60}
|
defaultColDef={defaultColDef}
|
||||||
customThemeParams={
|
handleRowClicked={handleRowClicked}
|
||||||
theme === 'dark'
|
|
||||||
? constants.agGridDarkVariables
|
|
||||||
: constants.agGridLightVariables
|
|
||||||
}
|
|
||||||
onGridReady={handleOnGridReady}
|
|
||||||
onRowClicked={handleRowClicked}
|
|
||||||
rowClass={isMobile ? 'mobile' : ''}
|
|
||||||
rowClassRules={constants.ROW_CLASS_RULES}
|
|
||||||
ref={gridRef}
|
|
||||||
overlayNoRowsTemplate={t('No data to display')}
|
|
||||||
suppressContextMenu
|
|
||||||
getRowId={getRowId}
|
|
||||||
suppressMovableColumns
|
|
||||||
suppressRowTransform
|
|
||||||
onCellKeyDown={onCellKeyDown}
|
|
||||||
tabToNextCell={onTabToNextCell}
|
|
||||||
suppressHorizontalScroll={shouldSuppressHorizontalScroll}
|
|
||||||
/>
|
/>
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@ -7,7 +7,6 @@ import {
|
|||||||
} from '@radix-ui/react-dropdown-menu';
|
} from '@radix-ui/react-dropdown-menu';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { themelite as theme } from '@vegaprotocol/tailwindcss-config';
|
|
||||||
import {
|
import {
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -16,8 +15,10 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { MarketState } from '@vegaprotocol/types';
|
import { MarketState } from '@vegaprotocol/types';
|
||||||
import useMarketFiltersData from '../../hooks/use-markets-filter';
|
import useMarketFiltersData from '../../hooks/use-markets-filter';
|
||||||
import { STATES_FILTER } from './constants';
|
|
||||||
import type { Markets_marketsConnection_edges_node } from '@vegaprotocol/market-list';
|
import type { Markets_marketsConnection_edges_node } from '@vegaprotocol/market-list';
|
||||||
|
import { HorizontalMenu } from '../horizontal-menu';
|
||||||
|
import type { HorizontalMenuItem } from '../horizontal-menu';
|
||||||
|
import * as constants from './constants';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: Markets_marketsConnection_edges_node[];
|
data: Markets_marketsConnection_edges_node[];
|
||||||
@ -28,37 +29,6 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { products, assetsPerProduct } = useMarketFiltersData(data);
|
const { products, assetsPerProduct } = useMarketFiltersData(data);
|
||||||
const [isOpen, setOpen] = useState(false);
|
const [isOpen, setOpen] = useState(false);
|
||||||
const [activeNumber, setActiveNumber] = useState(
|
|
||||||
products?.length ? products.indexOf(params.product || '') + 1 : -1
|
|
||||||
);
|
|
||||||
|
|
||||||
const [sliderStyles, setSliderStyles] = useState<Record<string, string>>({});
|
|
||||||
const slideContRef = useRef<HTMLUListElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (products.length) {
|
|
||||||
setActiveNumber(products.indexOf(params.product || '') + 1);
|
|
||||||
} else {
|
|
||||||
setActiveNumber(-1);
|
|
||||||
}
|
|
||||||
}, [params, products, setActiveNumber]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const contStyles = (
|
|
||||||
slideContRef.current as HTMLUListElement
|
|
||||||
).getBoundingClientRect();
|
|
||||||
const selectedStyles = (slideContRef.current as HTMLUListElement).children[
|
|
||||||
activeNumber
|
|
||||||
]?.getBoundingClientRect();
|
|
||||||
const styles: Record<string, string> = selectedStyles
|
|
||||||
? {
|
|
||||||
backgroundColor: activeNumber ? '' : theme.colors.pink,
|
|
||||||
width: `${selectedStyles.width}px`,
|
|
||||||
left: `${selectedStyles.left - contStyles.left}px`,
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
setSliderStyles(styles);
|
|
||||||
}, [activeNumber, slideContRef]);
|
|
||||||
|
|
||||||
const onStateChange = useCallback(
|
const onStateChange = useCallback(
|
||||||
(activeState: string) => {
|
(activeState: string) => {
|
||||||
@ -74,53 +44,33 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
[params, navigate]
|
[params, navigate]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const productItems = useMemo(() => {
|
||||||
|
const currentState = params.state || MarketState.STATE_ACTIVE;
|
||||||
|
const noStateSkip = currentState !== MarketState.STATE_ACTIVE;
|
||||||
|
const items: HorizontalMenuItem[] = [
|
||||||
|
{
|
||||||
|
...constants.ALL_PRODUCTS_ITEM,
|
||||||
|
url: `/markets${noStateSkip ? '/' + currentState : ''}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
products.forEach((product) =>
|
||||||
|
items.push({
|
||||||
|
name: product,
|
||||||
|
id: product,
|
||||||
|
url: `/markets/${currentState}/${product}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
}, [params, products]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-full mb-2 md:mb-8 font-alpha">
|
<div className="w-full max-w-full mb-2 md:mb-8 font-alpha">
|
||||||
<ul
|
<HorizontalMenu
|
||||||
ref={slideContRef}
|
active={params.product || constants.ALL_PRODUCTS_ITEM.id}
|
||||||
className="grid grid-flow-col auto-cols-min gap-4 relative pb-2 mb-2"
|
items={productItems}
|
||||||
data-testid="market-products-menu"
|
data-testid="market-products-menu"
|
||||||
aria-label={t('Product type')}
|
aria-label={t('Product type')}
|
||||||
>
|
/>
|
||||||
<li key="all-markets" className="md:mr-2 whitespace-nowrap">
|
|
||||||
<Link
|
|
||||||
to={`/markets${
|
|
||||||
params.state && params.state !== MarketState.STATE_ACTIVE
|
|
||||||
? '/' + params.state
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
aria-label={t('All markets')}
|
|
||||||
className={classNames('pl-0 text-pink hover:opacity-75', {
|
|
||||||
active: !activeNumber,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{t('All Markets')}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{products.map((product, i) => (
|
|
||||||
<li key={`${product}-${i}`} className="mx-2 whitespace-nowrap">
|
|
||||||
<Link
|
|
||||||
to={`/markets/${
|
|
||||||
params.state || MarketState.STATE_ACTIVE
|
|
||||||
}/${product}`}
|
|
||||||
className={classNames(
|
|
||||||
'hover:opacity-75 text-black dark:text-white',
|
|
||||||
{
|
|
||||||
active: activeNumber - 1 === i,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
aria-label={product}
|
|
||||||
>
|
|
||||||
{product}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
<li
|
|
||||||
className="absolute bottom-0 h-[2px] transition-left duration-300 dark:bg-white bg-black"
|
|
||||||
key="slider"
|
|
||||||
style={sliderStyles}
|
|
||||||
/>
|
|
||||||
</ul>
|
|
||||||
<div className="grid gap-4 pb-2 mt-2 md:mt-6 md:grid-cols-[min-content,min-content,1fr]">
|
<div className="grid gap-4 pb-2 mt-2 md:mt-6 md:grid-cols-[min-content,min-content,1fr]">
|
||||||
<div className="pb-2">
|
<div className="pb-2">
|
||||||
<DropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
|
<DropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
|
||||||
@ -130,7 +80,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
onClick={() => setOpen(!isOpen)}
|
onClick={() => setOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
<div className="w-full justify-between uppercase inline-flex items-center justify-center box-border">
|
<div className="w-full justify-between uppercase inline-flex items-center justify-center box-border">
|
||||||
{STATES_FILTER.find(
|
{constants.STATES_FILTER.find(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.value === params.state ||
|
state.value === params.state ||
|
||||||
(!params.state && state.value === MarketState.STATE_ACTIVE)
|
(!params.state && state.value === MarketState.STATE_ACTIVE)
|
||||||
@ -147,7 +97,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{STATES_FILTER.map(({ value, text }) => (
|
{constants.STATES_FILTER.map(({ value, text }) => (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
className="uppercase text-ui"
|
className="uppercase text-ui"
|
||||||
key={value}
|
key={value}
|
||||||
@ -169,7 +119,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
<div className="hidden md:block text-deemphasise dark:text-midGrey">
|
<div className="hidden md:block text-deemphasise dark:text-midGrey">
|
||||||
|
|
|
|
||||||
</div>
|
</div>
|
||||||
{activeNumber > 0 && (
|
{params.product && (
|
||||||
<ul
|
<ul
|
||||||
className="md:gap-x-6 gap-x-4 gap-y-1 pb-2 md:ml-2 flex flex-wrap"
|
className="md:gap-x-6 gap-x-4 gap-y-1 pb-2 md:ml-2 flex flex-wrap"
|
||||||
data-testid="market-assets-menu"
|
data-testid="market-assets-menu"
|
||||||
@ -189,7 +139,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
|||||||
{t('All')}
|
{t('All')}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{assetsPerProduct[products[activeNumber - 1]]?.map((asset) => (
|
{assetsPerProduct[params?.product]?.map((asset) => (
|
||||||
<li key={asset}>
|
<li key={asset}>
|
||||||
<Link
|
<Link
|
||||||
to={`/markets/${params.state}/${params.product}/${asset}`}
|
to={`/markets/${params.state}/${params.product}/${asset}`}
|
||||||
|
@ -51,6 +51,12 @@ export const routerConfig = [
|
|||||||
name: 'Portfolio',
|
name: 'Portfolio',
|
||||||
text: t('Portfolio'),
|
text: t('Portfolio'),
|
||||||
element: <Portfolio />,
|
element: <Portfolio />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':module',
|
||||||
|
element: <Portfolio />,
|
||||||
|
},
|
||||||
|
],
|
||||||
icon: 'portfolio',
|
icon: 'portfolio',
|
||||||
isNavItem: true,
|
isNavItem: true,
|
||||||
},
|
},
|
||||||
|
@ -73,8 +73,11 @@ describe('orders', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('partially filled orders should not show close/edit buttons', () => {
|
it('partially filled orders should not show close/edit buttons', () => {
|
||||||
|
const partiallyFilledId =
|
||||||
|
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
|
||||||
|
|
||||||
cy.getByTestId('tab-orders').should('be.visible');
|
cy.getByTestId('tab-orders').should('be.visible');
|
||||||
cy.get('[row-index="4"]').within(() => {
|
cy.get(`[row-id="${partiallyFilledId}"]`).within(() => {
|
||||||
cy.get(`[col-id='${orderStatus}']`).should(
|
cy.get(`[col-id='${orderStatus}']`).should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'PartiallyFilled'
|
'PartiallyFilled'
|
||||||
@ -95,12 +98,21 @@ describe('orders', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
cy.getByTestId('tab-orders')
|
cy.getByTestId('tab-orders')
|
||||||
.get(`[col-id='${orderSymbol}']`)
|
.get(`.ag-center-cols-container [col-id='${orderSymbol}']`)
|
||||||
.should('have.length.at.least', 4)
|
.should('have.length.at.least', 5)
|
||||||
.each(($symbol, index) => {
|
.then(($symbols) => {
|
||||||
if (index != 0) {
|
const symbolNames: string[] = [];
|
||||||
cy.wrap($symbol).should('have.text', expectedOrderList[index - 1]);
|
cy.wrap($symbols)
|
||||||
}
|
.each(($symbol) => {
|
||||||
|
cy.wrap($symbol)
|
||||||
|
.invoke('text')
|
||||||
|
.then((text) => {
|
||||||
|
symbolNames.push(text);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
expect(symbolNames).to.include.ordered.members(expectedOrderList);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useCallback, useMemo } from 'react';
|
import React, { useRef, useMemo } from 'react';
|
||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
@ -20,52 +20,53 @@ interface AccountsManagerProps {
|
|||||||
partyId: string;
|
partyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const accountsManagerUpdate =
|
||||||
|
(gridRef: React.RefObject<AgGridReact>) =>
|
||||||
|
({ delta: deltas }: { delta: AccountEventsSubscription['accounts'] }) => {
|
||||||
|
const update: AccountFieldsFragment[] = [];
|
||||||
|
const add: AccountFieldsFragment[] = [];
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const api = gridRef.current.api;
|
||||||
|
deltas.forEach((delta) => {
|
||||||
|
const rowNode = api.getRowNode(getId(delta));
|
||||||
|
if (rowNode) {
|
||||||
|
const updatedData = produce<AccountFieldsFragment>(
|
||||||
|
rowNode.data,
|
||||||
|
(draft: AccountFieldsFragment) => {
|
||||||
|
merge(draft, delta);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (updatedData !== rowNode.data) {
|
||||||
|
update.push(updatedData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// #TODO handle new account (or leave it to data provider to handle it)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (update.length || add.length) {
|
||||||
|
gridRef.current.api.applyTransactionAsync({
|
||||||
|
update,
|
||||||
|
add,
|
||||||
|
addIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (add.length) {
|
||||||
|
addSummaryRows(
|
||||||
|
gridRef.current.api,
|
||||||
|
gridRef.current.columnApi,
|
||||||
|
getGroupId,
|
||||||
|
getGroupSummaryRow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
|
export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
const update = useCallback(
|
const update = accountsManagerUpdate(gridRef);
|
||||||
({ delta: deltas }: { delta: AccountEventsSubscription['accounts'] }) => {
|
|
||||||
const update: AccountFieldsFragment[] = [];
|
|
||||||
const add: AccountFieldsFragment[] = [];
|
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const api = gridRef.current.api;
|
|
||||||
deltas.forEach((delta) => {
|
|
||||||
const rowNode = api.getRowNode(getId(delta));
|
|
||||||
if (rowNode) {
|
|
||||||
const updatedData = produce<AccountFieldsFragment>(
|
|
||||||
rowNode.data,
|
|
||||||
(draft: AccountFieldsFragment) => {
|
|
||||||
merge(draft, delta);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (updatedData !== rowNode.data) {
|
|
||||||
update.push(updatedData);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// #TODO handle new account (or leave it to data provider to handle it)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (update.length || add.length) {
|
|
||||||
gridRef.current.api.applyTransactionAsync({
|
|
||||||
update,
|
|
||||||
add,
|
|
||||||
addIndex: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (add.length) {
|
|
||||||
addSummaryRows(
|
|
||||||
gridRef.current.api,
|
|
||||||
gridRef.current.columnApi,
|
|
||||||
getGroupId,
|
|
||||||
getGroupSummaryRow
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[gridRef]
|
|
||||||
);
|
|
||||||
const { data, error, loading } = useDataProvider<
|
const { data, error, loading } = useDataProvider<
|
||||||
AccountFieldsFragment[],
|
AccountFieldsFragment[],
|
||||||
AccountEventsSubscription['accounts']
|
AccountEventsSubscription['accounts']
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
export * from './lib/fills-container';
|
export * from './lib/fills-container';
|
||||||
|
export * from './lib/use-fills-list';
|
||||||
|
export * from './lib/fills-data-provider';
|
||||||
export * from './lib/__generated__/FillFields';
|
export * from './lib/__generated__/FillFields';
|
||||||
export * from './lib/__generated__/Fills';
|
export * from './lib/__generated__/Fills';
|
||||||
export * from './lib/__generated__/FillsSub';
|
export * from './lib/__generated__/FillsSub';
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import { useCallback, useRef, useMemo } from 'react';
|
import { useRef } from 'react';
|
||||||
import {
|
|
||||||
useDataProvider,
|
|
||||||
makeInfiniteScrollGetRows,
|
|
||||||
} from '@vegaprotocol/react-helpers';
|
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { FillsTable } from './fills-table';
|
import { FillsTable } from './fills-table';
|
||||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||||
|
import { useFillsList } from './use-fills-list';
|
||||||
import type {
|
|
||||||
TradeWithMarket,
|
|
||||||
TradeWithMarketEdge,
|
|
||||||
} from './fills-data-provider';
|
|
||||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
|
||||||
|
|
||||||
interface FillsManagerProps {
|
interface FillsManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
@ -20,81 +11,12 @@ interface FillsManagerProps {
|
|||||||
|
|
||||||
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const dataRef = useRef<(TradeWithMarketEdge | null)[] | null>(null);
|
|
||||||
const totalCountRef = useRef<number | undefined>(undefined);
|
|
||||||
const newRows = useRef(0);
|
|
||||||
const scrolledToTop = useRef(true);
|
const scrolledToTop = useRef(true);
|
||||||
|
const { data, error, loading, addNewRows, getRows } = useFillsList({
|
||||||
const addNewRows = useCallback(() => {
|
partyId,
|
||||||
if (newRows.current === 0) {
|
gridRef,
|
||||||
return;
|
scrolledToTop,
|
||||||
}
|
});
|
||||||
if (totalCountRef.current !== undefined) {
|
|
||||||
totalCountRef.current += newRows.current;
|
|
||||||
}
|
|
||||||
newRows.current = 0;
|
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
({
|
|
||||||
data,
|
|
||||||
delta,
|
|
||||||
}: {
|
|
||||||
data: (TradeWithMarketEdge | null)[] | null;
|
|
||||||
delta: TradeWithMarket[];
|
|
||||||
}) => {
|
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!scrolledToTop.current) {
|
|
||||||
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
|
||||||
if (createdAt) {
|
|
||||||
newRows.current += delta.filter(
|
|
||||||
(trade) => trade.createdAt > createdAt
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataRef.current = data;
|
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const insert = useCallback(
|
|
||||||
({
|
|
||||||
data,
|
|
||||||
totalCount,
|
|
||||||
}: {
|
|
||||||
data: (TradeWithMarketEdge | null)[] | null;
|
|
||||||
totalCount?: number;
|
|
||||||
}) => {
|
|
||||||
dataRef.current = data;
|
|
||||||
totalCountRef.current = totalCount;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
|
||||||
|
|
||||||
const { data, error, loading, load, totalCount } = useDataProvider<
|
|
||||||
(TradeWithMarketEdge | null)[],
|
|
||||||
TradeWithMarket[]
|
|
||||||
>({ dataProvider: fillsWithMarketProvider, update, insert, variables });
|
|
||||||
totalCountRef.current = totalCount;
|
|
||||||
dataRef.current = data;
|
|
||||||
|
|
||||||
const getRows = makeInfiniteScrollGetRows<TradeWithMarketEdge>(
|
|
||||||
newRows,
|
|
||||||
dataRef,
|
|
||||||
totalCountRef,
|
|
||||||
load
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
if (event.top === 0) {
|
if (event.top === 0) {
|
||||||
|
93
libs/fills/src/lib/use-fills-list.spec.ts
Normal file
93
libs/fills/src/lib/use-fills-list.spec.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useFillsList } from './use-fills-list';
|
||||||
|
import type { TradeWithMarketEdge } from './fills-data-provider';
|
||||||
|
|
||||||
|
let mockData = null;
|
||||||
|
let mockDataProviderData = {
|
||||||
|
data: mockData as (TradeWithMarketEdge | null)[] | null,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateMock: jest.Mock;
|
||||||
|
const mockDataProvider = jest.fn((args) => {
|
||||||
|
updateMock = args.update;
|
||||||
|
return mockDataProviderData;
|
||||||
|
});
|
||||||
|
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||||
|
useDataProvider: jest.fn((args) => mockDataProvider(args)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useFillsList Hook', () => {
|
||||||
|
const mockRefreshAgGridApi = jest.fn();
|
||||||
|
const partyId = 'partyId';
|
||||||
|
const gridRef = {
|
||||||
|
current: {
|
||||||
|
api: {
|
||||||
|
refreshInfiniteCache: mockRefreshAgGridApi,
|
||||||
|
},
|
||||||
|
} as unknown as AgGridReact,
|
||||||
|
};
|
||||||
|
const scrolledToTop = {
|
||||||
|
current: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return proper dataProvider results', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useFillsList({ partyId, gridRef, scrolledToTop }),
|
||||||
|
{
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.current).toMatchObject({
|
||||||
|
data: null,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
addNewRows: expect.any(Function),
|
||||||
|
getRows: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return proper mocked results', () => {
|
||||||
|
mockData = [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_1',
|
||||||
|
},
|
||||||
|
} as unknown as TradeWithMarketEdge,
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_2',
|
||||||
|
},
|
||||||
|
} as unknown as TradeWithMarketEdge,
|
||||||
|
];
|
||||||
|
mockDataProviderData = {
|
||||||
|
...mockDataProviderData,
|
||||||
|
data: mockData,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useFillsList({ partyId, gridRef, scrolledToTop }),
|
||||||
|
{
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.current).toMatchObject({
|
||||||
|
data: mockData,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
addNewRows: expect.any(Function),
|
||||||
|
getRows: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
|
||||||
|
updateMock({ data: {} });
|
||||||
|
expect(mockRefreshAgGridApi).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
96
libs/fills/src/lib/use-fills-list.ts
Normal file
96
libs/fills/src/lib/use-fills-list.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { RefObject } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
makeInfiniteScrollGetRows,
|
||||||
|
useDataProvider,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
TradeWithMarket,
|
||||||
|
TradeWithMarketEdge,
|
||||||
|
} from './fills-data-provider';
|
||||||
|
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
gridRef: RefObject<AgGridReact>;
|
||||||
|
scrolledToTop: RefObject<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFillsList = ({ partyId, gridRef, scrolledToTop }: Props) => {
|
||||||
|
const dataRef = useRef<(TradeWithMarketEdge | null)[] | null>(null);
|
||||||
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
|
||||||
|
const addNewRows = useCallback(() => {
|
||||||
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
|
totalCountRef.current += newRows.current;
|
||||||
|
}
|
||||||
|
newRows.current = 0;
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
}, [gridRef]);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: (TradeWithMarketEdge | null)[] | null;
|
||||||
|
delta: TradeWithMarket[];
|
||||||
|
}) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataRef.current = data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[gridRef, scrolledToTop]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
data: (TradeWithMarketEdge | null)[] | null;
|
||||||
|
totalCount?: number;
|
||||||
|
}) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
|
||||||
|
const { data, error, loading, load, totalCount } = useDataProvider<
|
||||||
|
(TradeWithMarketEdge | null)[],
|
||||||
|
TradeWithMarket[]
|
||||||
|
>({ dataProvider: fillsWithMarketProvider, update, insert, variables });
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const getRows = makeInfiniteScrollGetRows<TradeWithMarketEdge>(
|
||||||
|
newRows,
|
||||||
|
dataRef,
|
||||||
|
totalCountRef,
|
||||||
|
load
|
||||||
|
);
|
||||||
|
return { data, error, loading, addNewRows, getRows };
|
||||||
|
};
|
@ -8,7 +8,7 @@ export const marketProvider = makeDerivedDataProvider<Market, never>(
|
|||||||
([markets], variables) => {
|
([markets], variables) => {
|
||||||
if (markets) {
|
if (markets) {
|
||||||
const market = (markets as Market[]).find(
|
const market = (markets as Market[]).find(
|
||||||
(market) => market.id === variables?.marketId
|
(market) => market.id === variables?.['marketId']
|
||||||
);
|
);
|
||||||
if (market) {
|
if (market) {
|
||||||
return market;
|
return market;
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export * from './order-list-manager';
|
export * from './order-list-manager';
|
||||||
|
export * from './use-order-list-data';
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import { useRef } from 'react';
|
||||||
useDataProvider,
|
|
||||||
makeInfiniteScrollGetRows,
|
|
||||||
} from '@vegaprotocol/react-helpers';
|
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
|
||||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
|
||||||
import { OrderList, ordersWithMarketProvider } from '../';
|
import { OrderList } from '../';
|
||||||
import type { OrderWithMarketEdge, OrderWithMarket } from '../';
|
import { useOrderListData } from './use-order-list-data';
|
||||||
|
|
||||||
interface OrderListManagerProps {
|
interface OrderListManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
@ -16,82 +12,13 @@ interface OrderListManagerProps {
|
|||||||
|
|
||||||
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const dataRef = useRef<(OrderWithMarketEdge | null)[] | null>(null);
|
|
||||||
const totalCountRef = useRef<number | undefined>(undefined);
|
|
||||||
const newRows = useRef(0);
|
|
||||||
const scrolledToTop = useRef(true);
|
const scrolledToTop = useRef(true);
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
|
||||||
|
|
||||||
const addNewRows = useCallback(() => {
|
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||||
if (newRows.current === 0) {
|
partyId,
|
||||||
return;
|
gridRef,
|
||||||
}
|
scrolledToTop,
|
||||||
if (totalCountRef.current !== undefined) {
|
|
||||||
totalCountRef.current += newRows.current;
|
|
||||||
}
|
|
||||||
newRows.current = 0;
|
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const update = useCallback(
|
|
||||||
({
|
|
||||||
data,
|
|
||||||
delta,
|
|
||||||
}: {
|
|
||||||
data: (OrderWithMarketEdge | null)[];
|
|
||||||
delta: OrderWithMarket[];
|
|
||||||
}) => {
|
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!scrolledToTop.current) {
|
|
||||||
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
|
||||||
if (createdAt) {
|
|
||||||
newRows.current += delta.filter(
|
|
||||||
(trade) => trade.createdAt > createdAt
|
|
||||||
).length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataRef.current = data;
|
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const insert = useCallback(
|
|
||||||
({
|
|
||||||
data,
|
|
||||||
totalCount,
|
|
||||||
}: {
|
|
||||||
data: (OrderWithMarketEdge | null)[];
|
|
||||||
totalCount?: number;
|
|
||||||
}) => {
|
|
||||||
dataRef.current = data;
|
|
||||||
totalCountRef.current = totalCount;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data, error, loading, load, totalCount } = useDataProvider({
|
|
||||||
dataProvider: ordersWithMarketProvider,
|
|
||||||
update,
|
|
||||||
insert,
|
|
||||||
variables,
|
|
||||||
});
|
});
|
||||||
totalCountRef.current = totalCount;
|
|
||||||
dataRef.current = data;
|
|
||||||
|
|
||||||
const getRows = makeInfiniteScrollGetRows<OrderWithMarketEdge>(
|
|
||||||
newRows,
|
|
||||||
dataRef,
|
|
||||||
totalCountRef,
|
|
||||||
load
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||||
if (event.top === 0) {
|
if (event.top === 0) {
|
||||||
|
@ -0,0 +1,163 @@
|
|||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
import { useOrderListData } from './use-order-list-data';
|
||||||
|
import type { Orders_party_ordersConnection_edges } from '../order-data-provider/__generated__/Orders';
|
||||||
|
import type { IGetRowsParams } from 'ag-grid-community';
|
||||||
|
|
||||||
|
const loadMock = jest.fn();
|
||||||
|
|
||||||
|
let mockData = null;
|
||||||
|
let mockDataProviderData = {
|
||||||
|
data: mockData as (Orders_party_ordersConnection_edges | null)[] | null,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
load: loadMock,
|
||||||
|
totalCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let updateMock: jest.Mock;
|
||||||
|
const mockDataProvider = jest.fn((args) => {
|
||||||
|
updateMock = args.update;
|
||||||
|
return mockDataProviderData;
|
||||||
|
});
|
||||||
|
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||||
|
useDataProvider: jest.fn((args) => mockDataProvider(args)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useOrderListData Hook', () => {
|
||||||
|
const mockRefreshAgGridApi = jest.fn();
|
||||||
|
const partyId = 'partyId';
|
||||||
|
const gridRef = {
|
||||||
|
current: {
|
||||||
|
api: {
|
||||||
|
refreshInfiniteCache: mockRefreshAgGridApi,
|
||||||
|
},
|
||||||
|
} as unknown as AgGridReact,
|
||||||
|
};
|
||||||
|
const scrolledToTop = {
|
||||||
|
current: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return proper dataProvider results', () => {
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
|
||||||
|
{
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.current).toMatchObject({
|
||||||
|
data: null,
|
||||||
|
error: undefined,
|
||||||
|
loading: true,
|
||||||
|
addNewRows: expect.any(Function),
|
||||||
|
getRows: expect.any(Function),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return proper mocked results', () => {
|
||||||
|
mockData = [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_1',
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_2',
|
||||||
|
createdAt: 2,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
];
|
||||||
|
mockDataProviderData = {
|
||||||
|
...mockDataProviderData,
|
||||||
|
data: mockData,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
|
||||||
|
{
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
expect(result.current).toMatchObject({
|
||||||
|
data: mockData,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
addNewRows: expect.any(Function),
|
||||||
|
getRows: expect.any(Function),
|
||||||
|
});
|
||||||
|
expect(mockRefreshAgGridApi).not.toHaveBeenCalled();
|
||||||
|
updateMock({ data: [], delta: [] });
|
||||||
|
expect(mockRefreshAgGridApi).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('methods for pagination should works', async () => {
|
||||||
|
const successCallback = jest.fn();
|
||||||
|
mockData = [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_1',
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_2',
|
||||||
|
createdAt: 2,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
];
|
||||||
|
mockDataProviderData = {
|
||||||
|
...mockDataProviderData,
|
||||||
|
data: mockData,
|
||||||
|
loading: false,
|
||||||
|
totalCount: 4,
|
||||||
|
};
|
||||||
|
const mockDelta = [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_3',
|
||||||
|
createdAt: 3,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
id: 'data_id_4',
|
||||||
|
createdAt: 4,
|
||||||
|
},
|
||||||
|
} as unknown as Orders_party_ordersConnection_edges,
|
||||||
|
];
|
||||||
|
const mockNextData = [...mockData, ...mockDelta];
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useOrderListData({ partyId, gridRef, scrolledToTop }),
|
||||||
|
{
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
updateMock({ data: mockNextData, delta: mockDelta });
|
||||||
|
|
||||||
|
const getRowsParams = {
|
||||||
|
successCallback,
|
||||||
|
failCallback: jest.fn(),
|
||||||
|
startRow: 2,
|
||||||
|
endRow: 4,
|
||||||
|
} as unknown as IGetRowsParams;
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
await result.current.getRows(getRowsParams);
|
||||||
|
});
|
||||||
|
expect(loadMock).toHaveBeenCalled();
|
||||||
|
expect(successCallback).toHaveBeenLastCalledWith(
|
||||||
|
mockDelta.map((item) => item.node),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,99 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import {
|
||||||
|
makeInfiniteScrollGetRows,
|
||||||
|
useDataProvider,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { ordersWithMarketProvider } from '../';
|
||||||
|
import type { OrderWithMarketEdge, OrderWithMarket } from '../';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
gridRef: RefObject<AgGridReact>;
|
||||||
|
scrolledToTop: RefObject<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOrderListData = ({
|
||||||
|
partyId,
|
||||||
|
gridRef,
|
||||||
|
scrolledToTop,
|
||||||
|
}: Props) => {
|
||||||
|
const dataRef = useRef<(OrderWithMarketEdge | null)[] | null>(null);
|
||||||
|
const totalCountRef = useRef<number | undefined>(undefined);
|
||||||
|
const newRows = useRef(0);
|
||||||
|
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
|
||||||
|
const addNewRows = useCallback(() => {
|
||||||
|
if (newRows.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (totalCountRef.current !== undefined) {
|
||||||
|
totalCountRef.current += newRows.current;
|
||||||
|
}
|
||||||
|
newRows.current = 0;
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
}, [gridRef]);
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
data: (OrderWithMarketEdge | null)[];
|
||||||
|
delta: OrderWithMarket[];
|
||||||
|
}) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!scrolledToTop.current) {
|
||||||
|
const createdAt = dataRef.current?.[0]?.node.createdAt;
|
||||||
|
if (createdAt) {
|
||||||
|
newRows.current += delta.filter(
|
||||||
|
(trade) => trade.createdAt > createdAt
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataRef.current = data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[gridRef, scrolledToTop]
|
||||||
|
);
|
||||||
|
|
||||||
|
const insert = useCallback(
|
||||||
|
({
|
||||||
|
data,
|
||||||
|
totalCount,
|
||||||
|
}: {
|
||||||
|
data: (OrderWithMarketEdge | null)[];
|
||||||
|
totalCount?: number;
|
||||||
|
}) => {
|
||||||
|
dataRef.current = data;
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading, load, totalCount } = useDataProvider({
|
||||||
|
dataProvider: ordersWithMarketProvider,
|
||||||
|
update,
|
||||||
|
insert,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
totalCountRef.current = totalCount;
|
||||||
|
dataRef.current = data;
|
||||||
|
|
||||||
|
const getRows = makeInfiniteScrollGetRows<OrderWithMarketEdge>(
|
||||||
|
newRows,
|
||||||
|
dataRef,
|
||||||
|
totalCountRef,
|
||||||
|
load
|
||||||
|
);
|
||||||
|
return { loading, error, data, addNewRows, getRows };
|
||||||
|
};
|
@ -1,2 +1,3 @@
|
|||||||
export * from './order-list.stories';
|
export * from './order-list.stories';
|
||||||
export * from './order-list';
|
export * from './order-list';
|
||||||
|
export * from './order-edit-dialog';
|
||||||
|
@ -320,7 +320,7 @@ export const OrderListTable = forwardRef<AgGridReact, OrderListTableProps>(
|
|||||||
/**
|
/**
|
||||||
* Check if an order is active to determine if it can be edited or cancelled
|
* Check if an order is active to determine if it can be edited or cancelled
|
||||||
*/
|
*/
|
||||||
const isOrderActive = (status: OrderStatus) => {
|
export const isOrderActive = (status: OrderStatus) => {
|
||||||
return ![
|
return ![
|
||||||
OrderStatus.STATUS_CANCELLED,
|
OrderStatus.STATUS_CANCELLED,
|
||||||
OrderStatus.STATUS_REJECTED,
|
OrderStatus.STATUS_REJECTED,
|
||||||
@ -331,7 +331,9 @@ const isOrderActive = (status: OrderStatus) => {
|
|||||||
].includes(status);
|
].includes(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEditDialogTitle = (status?: OrderStatus): string | undefined => {
|
export const getEditDialogTitle = (
|
||||||
|
status?: OrderStatus
|
||||||
|
): string | undefined => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -358,7 +360,9 @@ const getEditDialogTitle = (status?: OrderStatus): string | undefined => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCancelDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
export const getCancelDialogIntent = (
|
||||||
|
status?: OrderStatus
|
||||||
|
): Intent | undefined => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -371,7 +375,9 @@ const getCancelDialogIntent = (status?: OrderStatus): Intent | undefined => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCancelDialogTitle = (status?: OrderStatus): string | undefined => {
|
export const getCancelDialogTitle = (
|
||||||
|
status?: OrderStatus
|
||||||
|
): string | undefined => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3,3 +3,4 @@ export * from './order-event-query';
|
|||||||
export * from './use-order-cancel';
|
export * from './use-order-cancel';
|
||||||
export * from './use-order-submit';
|
export * from './use-order-submit';
|
||||||
export * from './use-order-validation';
|
export * from './use-order-validation';
|
||||||
|
export * from './use-order-edit';
|
||||||
|
@ -4,7 +4,7 @@ import type { OrderEvent_busEvents_event_Order } from './';
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { useOrderEvent } from './use-order-event';
|
import { useOrderEvent } from './use-order-event';
|
||||||
|
|
||||||
interface CancelOrderArgs {
|
export interface CancelOrderArgs {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
marketId: string;
|
marketId: string;
|
||||||
}
|
}
|
||||||
|
@ -5,3 +5,5 @@ export * from './lib/positions-data-providers';
|
|||||||
export * from './lib/positions-table';
|
export * from './lib/positions-table';
|
||||||
export * from './lib/use-close-position';
|
export * from './lib/use-close-position';
|
||||||
export * from './lib/use-position-event';
|
export * from './lib/use-position-event';
|
||||||
|
export * from './lib/use-positions-data';
|
||||||
|
export * from './lib/use-positions-assets';
|
||||||
|
@ -1,21 +1,14 @@
|
|||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
|
||||||
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
|
||||||
import type { Position } from './positions-data-providers';
|
import type { Position } from './positions-data-providers';
|
||||||
import { Positions } from './positions';
|
import { Positions } from './positions';
|
||||||
import { useClosePosition } from '../';
|
import { useClosePosition, usePositionsAssets } from '../';
|
||||||
|
|
||||||
interface PositionsManagerProps {
|
interface PositionsManagerProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSymbols = (positions: Position[]) =>
|
|
||||||
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
|
|
||||||
|
|
||||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
|
||||||
const assetSymbols = useRef<string[] | undefined>();
|
|
||||||
const { submit, Dialog } = useClosePosition();
|
const { submit, Dialog } = useClosePosition();
|
||||||
const onClose = useCallback(
|
const onClose = useCallback(
|
||||||
(position: Position) => {
|
(position: Position) => {
|
||||||
@ -23,37 +16,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
|||||||
},
|
},
|
||||||
[submit]
|
[submit]
|
||||||
);
|
);
|
||||||
const update = useCallback(({ data }: { data: Position[] | null }) => {
|
|
||||||
if (data?.length) {
|
const { data, error, loading, assetSymbols } = usePositionsAssets({
|
||||||
const newAssetSymbols = getSymbols(data);
|
partyId,
|
||||||
if (
|
|
||||||
!newAssetSymbols.every(
|
|
||||||
(symbol) =>
|
|
||||||
assetSymbols.current && assetSymbols.current.includes(symbol)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}, []);
|
|
||||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
|
||||||
dataProvider,
|
|
||||||
update,
|
|
||||||
variables,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AsyncRenderer loading={loading} error={error} data={assetSymbols}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
{data &&
|
{assetSymbols?.map((assetSymbol) => (
|
||||||
getSymbols(data)?.map((assetSymbol) => (
|
<Positions
|
||||||
<Positions
|
partyId={partyId}
|
||||||
partyId={partyId}
|
assetSymbol={assetSymbol}
|
||||||
assetSymbol={assetSymbol}
|
key={assetSymbol}
|
||||||
key={assetSymbol}
|
onClose={onClose}
|
||||||
onClose={onClose}
|
/>
|
||||||
/>
|
))}
|
||||||
))}
|
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<p>Your position was not closed! This is still not implemented. </p>
|
<p>Your position was not closed! This is still not implemented. </p>
|
||||||
|
@ -41,7 +41,7 @@ interface Props extends AgGridReactProps {
|
|||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PositionsTableValueFormatterParams = Omit<
|
export type PositionsTableValueFormatterParams = Omit<
|
||||||
ValueFormatterParams,
|
ValueFormatterParams,
|
||||||
'data' | 'value'
|
'data' | 'value'
|
||||||
> & {
|
> & {
|
||||||
@ -83,7 +83,7 @@ export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
|
|||||||
<ProgressBar
|
<ProgressBar
|
||||||
value={valueFormatted.value}
|
value={valueFormatted.value}
|
||||||
intent={valueFormatted.intent}
|
intent={valueFormatted.intent}
|
||||||
className="mt-2"
|
className="mt-2 w-full"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -1,85 +1,27 @@
|
|||||||
import { useRef, useCallback, useMemo, memo } from 'react';
|
import { useRef, memo } from 'react';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
import { BigNumber } from 'bignumber.js';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
|
|
||||||
import type { AgGridReact } from 'ag-grid-react';
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
import filter from 'lodash/filter';
|
|
||||||
import PositionsTable from './positions-table';
|
import PositionsTable from './positions-table';
|
||||||
import type { GetRowsParams } from './positions-table';
|
|
||||||
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
|
||||||
import { AssetBalance } from '@vegaprotocol/accounts';
|
|
||||||
import type { Position } from './positions-data-providers';
|
import type { Position } from './positions-data-providers';
|
||||||
|
import { AssetBalance } from '@vegaprotocol/accounts';
|
||||||
|
import { usePositionsData } from './use-positions-data';
|
||||||
|
|
||||||
interface PositionsProps {
|
interface PositionsProps {
|
||||||
partyId: string;
|
partyId: string;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
onClose: (position: Position) => void;
|
onClose: (position: Position) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSummaryRow = (positions: Position[]) => {
|
|
||||||
const summaryRow = {
|
|
||||||
notional: new BigNumber(0),
|
|
||||||
realisedPNL: BigInt(0),
|
|
||||||
unrealisedPNL: BigInt(0),
|
|
||||||
};
|
|
||||||
positions.forEach((position) => {
|
|
||||||
summaryRow.notional = summaryRow.notional.plus(
|
|
||||||
toBigNum(position.notional, position.marketDecimalPlaces)
|
|
||||||
);
|
|
||||||
summaryRow.realisedPNL += BigInt(position.realisedPNL);
|
|
||||||
summaryRow.unrealisedPNL += BigInt(position.unrealisedPNL);
|
|
||||||
});
|
|
||||||
const decimals = positions[0]?.decimals || 0;
|
|
||||||
return {
|
|
||||||
marketName: t('Total'),
|
|
||||||
// we are using asset decimals instead of market decimals because each market can have different decimals
|
|
||||||
notional: summaryRow.notional
|
|
||||||
.multipliedBy(10 ** decimals)
|
|
||||||
.toFixed()
|
|
||||||
.toString(),
|
|
||||||
realisedPNL: summaryRow.realisedPNL.toString(),
|
|
||||||
unrealisedPNL: summaryRow.unrealisedPNL.toString(),
|
|
||||||
decimals,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Positions = memo(
|
export const Positions = memo(
|
||||||
({ partyId, assetSymbol, onClose }: PositionsProps) => {
|
({ partyId, assetSymbol, onClose }: PositionsProps) => {
|
||||||
const gridRef = useRef<AgGridReact | null>(null);
|
const gridRef = useRef<AgGridReact | null>(null);
|
||||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
const { data, error, loading, getRows } = usePositionsData({
|
||||||
const dataRef = useRef<Position[] | null>(null);
|
partyId,
|
||||||
const update = useCallback(
|
assetSymbol,
|
||||||
({ data }: { data: Position[] | null }) => {
|
gridRef,
|
||||||
if (!gridRef.current?.api) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
dataRef.current = filter(data, { assetSymbol });
|
|
||||||
gridRef.current.api.refreshInfiniteCache();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[assetSymbol]
|
|
||||||
);
|
|
||||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
|
||||||
dataProvider,
|
|
||||||
update,
|
|
||||||
variables,
|
|
||||||
});
|
});
|
||||||
dataRef.current = filter(data, { assetSymbol });
|
|
||||||
const getRows = async ({
|
|
||||||
successCallback,
|
|
||||||
startRow,
|
|
||||||
endRow,
|
|
||||||
}: GetRowsParams) => {
|
|
||||||
const rowsThisBlock = dataRef.current
|
|
||||||
? dataRef.current.slice(startRow, endRow)
|
|
||||||
: [];
|
|
||||||
const lastRow = dataRef.current?.length ?? -1;
|
|
||||||
successCallback(rowsThisBlock, lastRow);
|
|
||||||
if (gridRef.current?.api) {
|
|
||||||
gridRef.current.api.setPinnedBottomRowData([
|
|
||||||
getSummaryRow(rowsThisBlock),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
<div className="flex justify-between items-center px-4 pt-3 pb-1">
|
<div className="flex justify-between items-center px-4 pt-3 pb-1">
|
||||||
|
41
libs/positions/src/lib/use-positions-assets.ts
Normal file
41
libs/positions/src/lib/use-positions-assets.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { Position } from './positions-data-providers';
|
||||||
|
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSymbols = (positions: Position[]) =>
|
||||||
|
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
|
||||||
|
|
||||||
|
export const usePositionsAssets = ({ partyId }: Props) => {
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
const assetSymbols = useRef<string[] | undefined>();
|
||||||
|
const update = useCallback(({ data }: { data: Position[] | null }) => {
|
||||||
|
if (data?.length) {
|
||||||
|
const newAssetSymbols = getSymbols(data);
|
||||||
|
if (
|
||||||
|
!newAssetSymbols.every(
|
||||||
|
(symbol) =>
|
||||||
|
assetSymbols.current && assetSymbols.current.includes(symbol)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
assetSymbols.current = newAssetSymbols;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||||
|
dataProvider,
|
||||||
|
update,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
if (!assetSymbols.current && data) {
|
||||||
|
assetSymbols.current = getSymbols(data);
|
||||||
|
}
|
||||||
|
return { data, error, loading, assetSymbols: assetSymbols.current };
|
||||||
|
};
|
86
libs/positions/src/lib/use-positions-data.tsx
Normal file
86
libs/positions/src/lib/use-positions-data.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { BigNumber } from 'bignumber.js';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { GetRowsParams } from './positions-table';
|
||||||
|
import type { Position } from './positions-data-providers';
|
||||||
|
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
||||||
|
import filter from 'lodash/filter';
|
||||||
|
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
partyId: string;
|
||||||
|
assetSymbol?: string;
|
||||||
|
gridRef: RefObject<AgGridReact>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSummaryRow = (positions: Position[]) => {
|
||||||
|
const summaryRow = {
|
||||||
|
notional: new BigNumber(0),
|
||||||
|
realisedPNL: BigInt(0),
|
||||||
|
unrealisedPNL: BigInt(0),
|
||||||
|
};
|
||||||
|
positions.forEach((position) => {
|
||||||
|
summaryRow.notional = summaryRow.notional.plus(
|
||||||
|
toBigNum(position.notional, position.marketDecimalPlaces)
|
||||||
|
);
|
||||||
|
summaryRow.realisedPNL += BigInt(position.realisedPNL);
|
||||||
|
summaryRow.unrealisedPNL += BigInt(position.unrealisedPNL);
|
||||||
|
});
|
||||||
|
const decimals = positions[0]?.decimals || 0;
|
||||||
|
return {
|
||||||
|
marketName: t('Total'),
|
||||||
|
// we are using asset decimals instead of market decimals because each market can have different decimals
|
||||||
|
notional: summaryRow.notional
|
||||||
|
.multipliedBy(10 ** decimals)
|
||||||
|
.toFixed()
|
||||||
|
.toString(),
|
||||||
|
realisedPNL: summaryRow.realisedPNL.toString(),
|
||||||
|
unrealisedPNL: summaryRow.unrealisedPNL.toString(),
|
||||||
|
decimals,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePositionsData = ({ partyId, assetSymbol, gridRef }: Props) => {
|
||||||
|
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||||
|
const dataRef = useRef<Position[] | null>(null);
|
||||||
|
const update = useCallback(
|
||||||
|
({ data }: { data: Position[] | null }) => {
|
||||||
|
if (!gridRef.current?.api) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data;
|
||||||
|
gridRef.current.api.refreshInfiniteCache();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[assetSymbol, gridRef]
|
||||||
|
);
|
||||||
|
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||||
|
dataProvider,
|
||||||
|
update,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
dataRef.current = assetSymbol ? filter(data, { assetSymbol }) : data;
|
||||||
|
const getRows = async ({
|
||||||
|
successCallback,
|
||||||
|
startRow,
|
||||||
|
endRow,
|
||||||
|
}: GetRowsParams) => {
|
||||||
|
const rowsThisBlock = dataRef.current
|
||||||
|
? dataRef.current.slice(startRow, endRow)
|
||||||
|
: [];
|
||||||
|
const lastRow = dataRef.current?.length ?? -1;
|
||||||
|
successCallback(rowsThisBlock, lastRow);
|
||||||
|
if (gridRef.current?.api) {
|
||||||
|
gridRef.current.api.setPinnedBottomRowData([
|
||||||
|
getSummaryRow(rowsThisBlock),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
getRows,
|
||||||
|
};
|
||||||
|
};
|
@ -2,10 +2,12 @@ export const positiveClassNames = 'text-vega-green-dark dark:text-vega-green';
|
|||||||
export const negativeClassNames = 'text-vega-red-dark dark:text-vega-red';
|
export const negativeClassNames = 'text-vega-red-dark dark:text-vega-red';
|
||||||
|
|
||||||
const isPositive = ({ value }: { value: string | bigint | number }) =>
|
const isPositive = ({ value }: { value: string | bigint | number }) =>
|
||||||
value && ((typeof value === 'string' && !value.startsWith('-')) || value > 0);
|
!!value &&
|
||||||
|
((typeof value === 'string' && !value.startsWith('-')) || value > 0);
|
||||||
|
|
||||||
const isNegative = ({ value }: { value: string | bigint | number }) =>
|
const isNegative = ({ value }: { value: string | bigint | number }) =>
|
||||||
value && ((typeof value === 'string' && value.startsWith('-')) || value < 0);
|
!!value &&
|
||||||
|
((typeof value === 'string' && value.startsWith('-')) || value < 0);
|
||||||
|
|
||||||
export const signedNumberCssClass = (value: string | bigint | number) => {
|
export const signedNumberCssClass = (value: string | bigint | number) => {
|
||||||
if (isPositive({ value })) {
|
if (isPositive({ value })) {
|
||||||
|
Loading…
Reference in New Issue
Block a user