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,
|
||||
disconnectVegaWallet,
|
||||
} 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', () => {
|
||||
afterEach(() => {
|
||||
@ -19,19 +25,91 @@ describe('Portfolio page', () => {
|
||||
it('certain tabs should exist', () => {
|
||||
cy.visit('/portfolio');
|
||||
connectVegaWallet();
|
||||
cy.getByTestId('Assets').should('exist');
|
||||
cy.getByTestId('tab-assets').should('exist');
|
||||
|
||||
cy.getByTestId('Positions').click();
|
||||
cy.getByTestId('tab-positions').should('exist');
|
||||
cy.getByTestId('assets').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/assets');
|
||||
|
||||
cy.getByTestId('Orders').click();
|
||||
cy.getByTestId('tab-orders').should('exist');
|
||||
cy.getByTestId('positions').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/positions');
|
||||
|
||||
cy.getByTestId('Fills').click();
|
||||
cy.getByTestId('tab-fills').should('exist');
|
||||
cy.getByTestId('orders').click();
|
||||
cy.location('pathname').should('eq', '/portfolio/orders');
|
||||
|
||||
cy.getByTestId('Deposits').click();
|
||||
cy.getByTestId('tab-deposits').should('exist');
|
||||
cy.getByTestId('fills').click();
|
||||
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);
|
||||
};
|
||||
|
||||
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 { AccountsContainer } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import React, { useMemo } from 'react';
|
||||
import { AccountManager } from './accounts';
|
||||
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 { 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 = () => {
|
||||
const { keypair } = useVegaWallet();
|
||||
if (!keypair) {
|
||||
return (
|
||||
<section className="xl:w-1/2">
|
||||
<ConnectWallet />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
const params = useParams<RouterParams>();
|
||||
|
||||
const module = useMemo(() => {
|
||||
if (!keypair) {
|
||||
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 (
|
||||
<Tabs>
|
||||
<Tab id="assets" name={t('Assets')}>
|
||||
<AccountsContainer />
|
||||
</Tab>
|
||||
<Tab id="positions" name={t('Positions')}>
|
||||
<PositionsContainer />
|
||||
</Tab>
|
||||
<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>
|
||||
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content_1fr]">
|
||||
<HorizontalMenu
|
||||
active={params?.module}
|
||||
items={constants.PORTFOLIO_ITEMS}
|
||||
/>
|
||||
{module}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 { themelite as theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||
import colors from 'tailwindcss/colors';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||
|
||||
export const STATES_FILTER = [
|
||||
{ value: 'all', text: t('All') },
|
||||
@ -198,4 +198,11 @@ export const ROW_CLASS_RULES = {
|
||||
|
||||
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%' };
|
||||
|
@ -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 type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ThemeContext } from '@vegaprotocol/react-helpers';
|
||||
import type { MarketState } from '@vegaprotocol/types';
|
||||
import useMarketsFilterData from './use-markets-filter-data';
|
||||
import useColumnDefinitions from './use-column-definitions';
|
||||
import * as constants from './constants';
|
||||
import SimpleMarketToolbar from './simple-market-toolbar';
|
||||
import { IS_MARKET_TRADABLE } from '../../constants';
|
||||
import type {
|
||||
CellKeyDownEvent,
|
||||
FullWidthCellKeyDownEvent,
|
||||
} from 'ag-grid-community/dist/lib/events';
|
||||
import type {
|
||||
GetRowIdParams,
|
||||
TabToNextCellParams,
|
||||
} from 'ag-grid-community/dist/lib/entities/iCallbackParams';
|
||||
import { ConsoleLiteGrid } from '../console-lite-grid';
|
||||
import type { Market, MarketsListData } from '@vegaprotocol/market-list';
|
||||
import { useMarketList } from '@vegaprotocol/market-list';
|
||||
|
||||
@ -34,10 +23,9 @@ export type RouterParams = Partial<{
|
||||
}>;
|
||||
|
||||
const SimpleMarketList = () => {
|
||||
const { isMobile, screenSize } = useScreenDimensions();
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams<RouterParams>();
|
||||
const theme = useContext(ThemeContext);
|
||||
const statusesRef = useRef<Record<string, MarketState | ''>>({});
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
|
||||
@ -63,8 +51,6 @@ const SimpleMarketList = () => {
|
||||
|
||||
const { columnDefs, defaultColDef } = useColumnDefinitions({ isMobile });
|
||||
|
||||
const getRowId = useCallback(({ data }: GetRowIdParams) => data.id, []);
|
||||
|
||||
const handleRowClicked = useCallback(
|
||||
({ data }: { data: Market }) => {
|
||||
if (IS_MARKET_TRADABLE(data)) {
|
||||
@ -74,65 +60,16 @@ const SimpleMarketList = () => {
|
||||
[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 (
|
||||
<div className="h-full p-4 md:p-6 grid grid-rows-[min-content,1fr]">
|
||||
<SimpleMarketToolbar data={data?.markets || []} />
|
||||
<AsyncRenderer loading={loading} error={error} data={localData}>
|
||||
<AgGrid
|
||||
className="mb-32 min-h-[300px] w-full"
|
||||
style={constants.AG_GRID_CONTAINER_STYLES}
|
||||
defaultColDef={defaultColDef}
|
||||
<ConsoleLiteGrid<MarketWithPercentChange>
|
||||
classNamesParam="mb-32 min-h-[300px]"
|
||||
columnDefs={columnDefs}
|
||||
rowData={localData}
|
||||
rowHeight={60}
|
||||
customThemeParams={
|
||||
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}
|
||||
data={localData}
|
||||
defaultColDef={defaultColDef}
|
||||
handleRowClicked={handleRowClicked}
|
||||
/>
|
||||
</AsyncRenderer>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate, useParams, Link } from 'react-router-dom';
|
||||
import {
|
||||
@ -7,7 +7,6 @@ import {
|
||||
} from '@radix-ui/react-dropdown-menu';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { themelite as theme } from '@vegaprotocol/tailwindcss-config';
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
@ -16,8 +15,10 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import useMarketFiltersData from '../../hooks/use-markets-filter';
|
||||
import { STATES_FILTER } from './constants';
|
||||
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 {
|
||||
data: Markets_marketsConnection_edges_node[];
|
||||
@ -28,37 +29,6 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
const params = useParams();
|
||||
const { products, assetsPerProduct } = useMarketFiltersData(data);
|
||||
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(
|
||||
(activeState: string) => {
|
||||
@ -74,53 +44,33 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
[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 (
|
||||
<div className="w-full max-w-full mb-2 md:mb-8 font-alpha">
|
||||
<ul
|
||||
ref={slideContRef}
|
||||
className="grid grid-flow-col auto-cols-min gap-4 relative pb-2 mb-2"
|
||||
<HorizontalMenu
|
||||
active={params.product || constants.ALL_PRODUCTS_ITEM.id}
|
||||
items={productItems}
|
||||
data-testid="market-products-menu"
|
||||
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="pb-2">
|
||||
<DropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
|
||||
@ -130,7 +80,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
<div className="w-full justify-between uppercase inline-flex items-center justify-center box-border">
|
||||
{STATES_FILTER.find(
|
||||
{constants.STATES_FILTER.find(
|
||||
(state) =>
|
||||
state.value === params.state ||
|
||||
(!params.state && state.value === MarketState.STATE_ACTIVE)
|
||||
@ -147,7 +97,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{STATES_FILTER.map(({ value, text }) => (
|
||||
{constants.STATES_FILTER.map(({ value, text }) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
className="uppercase text-ui"
|
||||
key={value}
|
||||
@ -169,7 +119,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
<div className="hidden md:block text-deemphasise dark:text-midGrey">
|
||||
|
|
||||
</div>
|
||||
{activeNumber > 0 && (
|
||||
{params.product && (
|
||||
<ul
|
||||
className="md:gap-x-6 gap-x-4 gap-y-1 pb-2 md:ml-2 flex flex-wrap"
|
||||
data-testid="market-assets-menu"
|
||||
@ -189,7 +139,7 @@ const SimpleMarketToolbar = ({ data }: Props) => {
|
||||
{t('All')}
|
||||
</Link>
|
||||
</li>
|
||||
{assetsPerProduct[products[activeNumber - 1]]?.map((asset) => (
|
||||
{assetsPerProduct[params?.product]?.map((asset) => (
|
||||
<li key={asset}>
|
||||
<Link
|
||||
to={`/markets/${params.state}/${params.product}/${asset}`}
|
||||
|
@ -51,6 +51,12 @@ export const routerConfig = [
|
||||
name: 'Portfolio',
|
||||
text: t('Portfolio'),
|
||||
element: <Portfolio />,
|
||||
children: [
|
||||
{
|
||||
path: ':module',
|
||||
element: <Portfolio />,
|
||||
},
|
||||
],
|
||||
icon: 'portfolio',
|
||||
isNavItem: true,
|
||||
},
|
||||
|
@ -73,8 +73,11 @@ describe('orders', () => {
|
||||
});
|
||||
|
||||
it('partially filled orders should not show close/edit buttons', () => {
|
||||
const partiallyFilledId =
|
||||
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
|
||||
|
||||
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(
|
||||
'have.text',
|
||||
'PartiallyFilled'
|
||||
@ -95,12 +98,21 @@ describe('orders', () => {
|
||||
];
|
||||
|
||||
cy.getByTestId('tab-orders')
|
||||
.get(`[col-id='${orderSymbol}']`)
|
||||
.should('have.length.at.least', 4)
|
||||
.each(($symbol, index) => {
|
||||
if (index != 0) {
|
||||
cy.wrap($symbol).should('have.text', expectedOrderList[index - 1]);
|
||||
}
|
||||
.get(`.ag-center-cols-container [col-id='${orderSymbol}']`)
|
||||
.should('have.length.at.least', 5)
|
||||
.then(($symbols) => {
|
||||
const symbolNames: string[] = [];
|
||||
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 merge from 'lodash/merge';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
@ -20,52 +20,53 @@ interface AccountsManagerProps {
|
||||
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) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const update = useCallback(
|
||||
({ 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 update = accountsManagerUpdate(gridRef);
|
||||
const { data, error, loading } = useDataProvider<
|
||||
AccountFieldsFragment[],
|
||||
AccountEventsSubscription['accounts']
|
||||
|
@ -1,4 +1,6 @@
|
||||
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__/Fills';
|
||||
export * from './lib/__generated__/FillsSub';
|
||||
|
@ -1,18 +1,9 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useCallback, useRef, useMemo } from 'react';
|
||||
import {
|
||||
useDataProvider,
|
||||
makeInfiniteScrollGetRows,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useRef } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||
|
||||
import type {
|
||||
TradeWithMarket,
|
||||
TradeWithMarketEdge,
|
||||
} from './fills-data-provider';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
import { useFillsList } from './use-fills-list';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@ -20,81 +11,12 @@ interface FillsManagerProps {
|
||||
|
||||
export const FillsManager = ({ partyId }: FillsManagerProps) => {
|
||||
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 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();
|
||||
}, []);
|
||||
|
||||
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 { data, error, loading, addNewRows, getRows } = useFillsList({
|
||||
partyId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
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) => {
|
||||
if (markets) {
|
||||
const market = (markets as Market[]).find(
|
||||
(market) => market.id === variables?.marketId
|
||||
(market) => market.id === variables?.['marketId']
|
||||
);
|
||||
if (market) {
|
||||
return market;
|
||||
|
@ -1 +1,2 @@
|
||||
export * from './order-list-manager';
|
||||
export * from './use-order-list-data';
|
||||
|
@ -1,14 +1,10 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useDataProvider,
|
||||
makeInfiniteScrollGetRows,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import type { BodyScrollEvent, BodyScrollEndEvent } from 'ag-grid-community';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
|
||||
import { OrderList, ordersWithMarketProvider } from '../';
|
||||
import type { OrderWithMarketEdge, OrderWithMarket } from '../';
|
||||
import { OrderList } from '../';
|
||||
import { useOrderListData } from './use-order-list-data';
|
||||
|
||||
interface OrderListManagerProps {
|
||||
partyId: string;
|
||||
@ -16,82 +12,13 @@ interface OrderListManagerProps {
|
||||
|
||||
export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
||||
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 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();
|
||||
}, []);
|
||||
|
||||
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,
|
||||
const { data, error, loading, addNewRows, getRows } = useOrderListData({
|
||||
partyId,
|
||||
gridRef,
|
||||
scrolledToTop,
|
||||
});
|
||||
totalCountRef.current = totalCount;
|
||||
dataRef.current = data;
|
||||
|
||||
const getRows = makeInfiniteScrollGetRows<OrderWithMarketEdge>(
|
||||
newRows,
|
||||
dataRef,
|
||||
totalCountRef,
|
||||
load
|
||||
);
|
||||
|
||||
const onBodyScrollEnd = (event: BodyScrollEndEvent) => {
|
||||
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';
|
||||
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
|
||||
*/
|
||||
const isOrderActive = (status: OrderStatus) => {
|
||||
export const isOrderActive = (status: OrderStatus) => {
|
||||
return ![
|
||||
OrderStatus.STATUS_CANCELLED,
|
||||
OrderStatus.STATUS_REJECTED,
|
||||
@ -331,7 +331,9 @@ const isOrderActive = (status: OrderStatus) => {
|
||||
].includes(status);
|
||||
};
|
||||
|
||||
const getEditDialogTitle = (status?: OrderStatus): string | undefined => {
|
||||
export const getEditDialogTitle = (
|
||||
status?: OrderStatus
|
||||
): string | undefined => {
|
||||
if (!status) {
|
||||
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) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
@ -3,3 +3,4 @@ export * from './order-event-query';
|
||||
export * from './use-order-cancel';
|
||||
export * from './use-order-submit';
|
||||
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 { useOrderEvent } from './use-order-event';
|
||||
|
||||
interface CancelOrderArgs {
|
||||
export interface CancelOrderArgs {
|
||||
orderId: string;
|
||||
marketId: string;
|
||||
}
|
||||
|
@ -5,3 +5,5 @@ export * from './lib/positions-data-providers';
|
||||
export * from './lib/positions-table';
|
||||
export * from './lib/use-close-position';
|
||||
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 { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { positionsMetricsDataProvider as dataProvider } from './positions-data-providers';
|
||||
import type { Position } from './positions-data-providers';
|
||||
import { Positions } from './positions';
|
||||
import { useClosePosition } from '../';
|
||||
import { useClosePosition, usePositionsAssets } from '../';
|
||||
|
||||
interface PositionsManagerProps {
|
||||
partyId: string;
|
||||
}
|
||||
|
||||
const getSymbols = (positions: Position[]) =>
|
||||
Array.from(new Set(positions.map((position) => position.assetSymbol))).sort();
|
||||
|
||||
export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
const assetSymbols = useRef<string[] | undefined>();
|
||||
const { submit, Dialog } = useClosePosition();
|
||||
const onClose = useCallback(
|
||||
(position: Position) => {
|
||||
@ -23,37 +16,22 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
||||
},
|
||||
[submit]
|
||||
);
|
||||
const update = useCallback(({ data }: { data: Position[] | null }) => {
|
||||
if (data?.length) {
|
||||
const newAssetSymbols = getSymbols(data);
|
||||
if (
|
||||
!newAssetSymbols.every(
|
||||
(symbol) =>
|
||||
assetSymbols.current && assetSymbols.current.includes(symbol)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, []);
|
||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||
dataProvider,
|
||||
update,
|
||||
variables,
|
||||
|
||||
const { data, error, loading, assetSymbols } = usePositionsAssets({
|
||||
partyId,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer loading={loading} error={error} data={assetSymbols}>
|
||||
{data &&
|
||||
getSymbols(data)?.map((assetSymbol) => (
|
||||
<Positions
|
||||
partyId={partyId}
|
||||
assetSymbol={assetSymbol}
|
||||
key={assetSymbol}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
{assetSymbols?.map((assetSymbol) => (
|
||||
<Positions
|
||||
partyId={partyId}
|
||||
assetSymbol={assetSymbol}
|
||||
key={assetSymbol}
|
||||
onClose={onClose}
|
||||
/>
|
||||
))}
|
||||
</AsyncRenderer>
|
||||
<Dialog>
|
||||
<p>Your position was not closed! This is still not implemented. </p>
|
||||
|
@ -41,7 +41,7 @@ interface Props extends AgGridReactProps {
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
type PositionsTableValueFormatterParams = Omit<
|
||||
export type PositionsTableValueFormatterParams = Omit<
|
||||
ValueFormatterParams,
|
||||
'data' | 'value'
|
||||
> & {
|
||||
@ -83,7 +83,7 @@ export const ProgressBarCell = ({ valueFormatted }: PriceCellProps) => {
|
||||
<ProgressBar
|
||||
value={valueFormatted.value}
|
||||
intent={valueFormatted.intent}
|
||||
className="mt-2"
|
||||
className="mt-2 w-full"
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
|
@ -1,85 +1,27 @@
|
||||
import { useRef, useCallback, useMemo, memo } from 'react';
|
||||
import { useRef, memo } from 'react';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
import { t, toBigNum, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import filter from 'lodash/filter';
|
||||
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 { AssetBalance } from '@vegaprotocol/accounts';
|
||||
import { usePositionsData } from './use-positions-data';
|
||||
|
||||
interface PositionsProps {
|
||||
partyId: string;
|
||||
assetSymbol: string;
|
||||
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(
|
||||
({ partyId, assetSymbol, onClose }: PositionsProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
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 = filter(data, { assetSymbol });
|
||||
gridRef.current.api.refreshInfiniteCache();
|
||||
return true;
|
||||
},
|
||||
[assetSymbol]
|
||||
);
|
||||
const { data, error, loading } = useDataProvider<Position[], never>({
|
||||
dataProvider,
|
||||
update,
|
||||
variables,
|
||||
const { data, error, loading, getRows } = usePositionsData({
|
||||
partyId,
|
||||
assetSymbol,
|
||||
gridRef,
|
||||
});
|
||||
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 (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
<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';
|
||||
|
||||
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 }) =>
|
||||
value && ((typeof value === 'string' && value.startsWith('-')) || value < 0);
|
||||
!!value &&
|
||||
((typeof value === 'string' && value.startsWith('-')) || value < 0);
|
||||
|
||||
export const signedNumberCssClass = (value: string | bigint | number) => {
|
||||
if (isPositive({ value })) {
|
||||
|
Loading…
Reference in New Issue
Block a user