chore: single position table (1645) (#1749)

* chore: single position table (1645)

chore: single position table (1645)

* chore: tests fixed

* chore: remove unused withSummaryRow arg

* chore: use ag grid value formatter type helper

* chore: update console-lite to use value formatter params helper

* chore: fix e2e test by ignoring pinned row

Co-authored-by: Rado <szpiechrados@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Art 2022-10-20 01:59:36 +02:00 committed by GitHub
parent ce283aeee7
commit 8d2fe118ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 161 deletions

View File

@ -7,10 +7,7 @@ import {
signedNumberCssClassRules,
t,
} from '@vegaprotocol/react-helpers';
import type {
PositionsTableValueFormatterParams,
Position,
} from '@vegaprotocol/positions';
import type { Position } from '@vegaprotocol/positions';
import { AmountCell } from '@vegaprotocol/positions';
import type {
CellRendererSelectorResult,
@ -20,6 +17,7 @@ import type {
ColDef,
} from 'ag-grid-community';
import { MarketTradingMode } from '@vegaprotocol/types';
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
import { Intent, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
const EmptyCell = () => '';
@ -77,9 +75,7 @@ const useColumnDefinitions = () => {
value,
data,
node,
}: PositionsTableValueFormatterParams & {
value: Position['openVolume'];
}) => {
}: VegaValueFormatterParams<Position, 'openVolume'>) => {
let ret;
if (value && data) {
ret = node?.rowPinned
@ -107,9 +103,7 @@ const useColumnDefinitions = () => {
value,
data,
node,
}: PositionsTableValueFormatterParams & {
value: Position['markPrice'];
}) => {
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (
data &&
value &&
@ -186,9 +180,8 @@ const useColumnDefinitions = () => {
valueFormatter: ({
value,
node,
}: PositionsTableValueFormatterParams & {
value: Position['currentLeverage'];
}) => (value === undefined ? '' : formatNumber(value.toString(), 1)),
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? '' : formatNumber(value.toString(), 1),
},
{
colId: 'marginallocated',
@ -229,10 +222,8 @@ const useColumnDefinitions = () => {
valueFormatter: ({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['realisedPNL'];
}) =>
value === undefined
}: VegaValueFormatterParams<Position, 'realisedPNL'>) =>
value === undefined || data === undefined
? ''
: addDecimalsFormatNumber(value.toString(), data.decimals),
cellRenderer: 'PriceFlashCell',
@ -249,10 +240,8 @@ const useColumnDefinitions = () => {
valueFormatter: ({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['unrealisedPNL'];
}) =>
value === undefined
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
value === undefined || data === undefined
? ''
: addDecimalsFormatNumber(value.toString(), data.decimals),
cellRenderer: 'PriceFlashCell',
@ -266,9 +255,7 @@ const useColumnDefinitions = () => {
type: 'rightAligned',
valueFormatter: ({
value,
}: PositionsTableValueFormatterParams & {
value: Position['updatedAt'];
}) => {
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return '';
}

View File

@ -31,36 +31,35 @@ describe('positions', { tags: '@smoke' }, () => {
cy.wrap($marketSymbol).invoke('text').should('not.be.empty');
});
cy.get('[col-id="openVolume"]').each(($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
});
cy.get('.ag-center-cols-container [col-id="openVolume"]').each(
($openVolume) => {
cy.wrap($openVolume).invoke('text').should('not.be.empty');
}
);
// includes average entry price, mark price, realised PNL & leverage
cy.getByTestId('flash-cell').each(($prices) => {
cy.wrap($prices).invoke('text').should('not.be.empty');
});
cy.get('[col-id="averageEntryPrice"]')
cy.get('[col-id="liquidationPrice"]')
.should('contain.text', '85,093.38') // entry price
.should('contain.text', '0.00'); // liquidation price
cy.get('[col-id="currentLeverage"]').should('contain.text', '0.8');
cy.get('[col-id="capitalUtilisation"]') // margin allocated
.should('contain.text', '0.00%')
.should('contain.text', '1,000.01000');
cy.get('[col-id="marginAccountBalance"]') // margin allocated
.should('contain.text', '1,000.00000');
cy.get('[col-id="unrealisedPNL"]').each(($unrealisedPnl) => {
cy.wrap($unrealisedPnl).invoke('text').should('not.be.empty');
});
cy.getByTestId('flash-cell').should('contain.text', '276,761.40348'); // Total tDAI position
cy.getByTestId('flash-cell').should('contain.text', '0.00000'); // Total Realised PNL
cy.get('[col-id="notional"]').should('contain.text', '276,761.40348'); // Total tDAI position
cy.get('[col-id="realisedPNL"]').should('contain.text', '0.00100'); // Total Realised PNL
cy.get('[col-id="unrealisedPNL"]').should('contain.text', '8.95000'); // Total Unrealised PNL
});
cy.getByTestId('balance').eq(1).should('have.text', '1,000.01000'); // Asset balance
cy.getByTestId('close-position').should('be.visible').and('have.length', 3);
}
});

View File

@ -42,6 +42,7 @@ interface PositionRejoined {
export interface Position {
marketName: string;
averageEntryPrice: string;
marginAccountBalance: BigNumber;
capitalUtilisation: number;
currentLeverage: number;
decimals: number;
@ -151,6 +152,7 @@ export const getMetrics = (
metrics.push({
marketName: market.tradableInstrument.instrument.name,
averageEntryPrice: position.averageEntryPrice,
marginAccountBalance,
capitalUtilisation: Math.round(capitalUtilisation.toNumber()),
currentLeverage: currentLeverage.toNumber(),
marketDecimalPlaces,

View File

@ -1,8 +1,8 @@
import { useCallback } from 'react';
import { useCallback, useRef } from 'react';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import type { Position } from './positions-data-providers';
import { Positions } from './positions';
import { useClosePosition, usePositionsAssets } from '../';
import { PositionsTable, useClosePosition, usePositionsData } from '../';
import type { AgGridReact } from 'ag-grid-react';
interface PositionsManagerProps {
partyId: string;
@ -17,18 +17,20 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
[submit]
);
const { data, error, loading, assetSymbols } = usePositionsAssets(partyId);
const gridRef = useRef<AgGridReact | null>(null);
const { data, error, loading, getRows } = usePositionsData(partyId, gridRef);
return (
<>
<AsyncRenderer loading={loading} error={error} data={data}>
{assetSymbols?.map((assetSymbol) => (
<Positions
partyId={partyId}
assetSymbol={assetSymbol}
key={assetSymbol}
onClose={onClose}
/>
))}
<PositionsTable
domLayout="autoHeight"
style={{ width: '100%' }}
ref={gridRef}
rowModelType={data?.length ? 'infinite' : 'clientSide'}
rowData={data?.length ? undefined : []}
datasource={{ getRows }}
onClose={onClose}
/>
</AsyncRenderer>
<Dialog>

View File

@ -3,28 +3,31 @@ import { act, render, screen } from '@testing-library/react';
import PositionsTable from './positions-table';
import type { Position } from './positions-data-providers';
import { MarketTradingMode } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import React from 'react';
const singleRow: Position = {
marketName: 'ETH/BTC (31 july 2022)',
averageEntryPrice: '133', // 13.3
capitalUtilisation: 11, // 11.00%
averageEntryPrice: '133',
capitalUtilisation: 11,
currentLeverage: 1.1,
marketDecimalPlaces: 1,
positionDecimalPlaces: 0,
decimals: 2,
totalBalance: '123456',
assetSymbol: 'BTC',
liquidationPrice: '83', // 8.3
liquidationPrice: '83',
lowMarginLevel: false,
marketId: 'string',
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
markPrice: '123', // 12.3
notional: '12300', // 1230.0
openVolume: '100', // 100
realisedPNL: '123', // 1.23
unrealisedPNL: '456', // 4.56
markPrice: '123',
notional: '12300',
openVolume: '100',
realisedPNL: '123',
unrealisedPNL: '456',
searchPrice: '0',
updatedAt: '2022-07-27T15:02:58.400Z',
marginAccountBalance: new BigNumber(123456),
};
const singleRowData = [singleRow];
@ -42,14 +45,17 @@ it('Render correct columns', async () => {
});
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(9);
expect(headers).toHaveLength(12);
expect(
headers.map((h) => h.querySelector('[ref="eText"]')?.textContent?.trim())
).toEqual([
'Market',
'Size',
'Notional size',
'Open volume',
'Mark price',
'Settlement asset',
'Entry price',
'Liquidation price (est)',
'Leverage',
'Margin allocated',
'Realised PNL',
@ -84,22 +90,21 @@ it('add color and sign to amount, displays positive notional value', async () =>
result = render(<PositionsTable rowData={singleRowData} />);
});
let cells = screen.getAllByRole('gridcell');
let values = cells[1].querySelectorAll('.text-right');
expect(values[0].classList.contains('text-vega-green-dark')).toBeTruthy();
expect(values[0].classList.contains('text-vega-red-dark')).toBeFalsy();
expect(values[0].textContent).toEqual('+100');
expect(values[1].textContent).toEqual('1,230.0');
expect(cells[2].classList.contains('text-vega-green-dark')).toBeTruthy();
expect(cells[2].classList.contains('text-vega-red-dark')).toBeFalsy();
expect(cells[2].textContent).toEqual('+100');
expect(cells[1].textContent).toEqual('123.00');
await act(async () => {
result.rerender(
<PositionsTable rowData={[{ ...singleRow, openVolume: '-100' }]} />
);
});
cells = screen.getAllByRole('gridcell');
values = cells[1].querySelectorAll('.text-right');
expect(values[0].classList.contains('text-vega-green-dark')).toBeFalsy();
expect(values[0].classList.contains('text-vega-red-dark')).toBeTruthy();
expect(values[0].textContent?.startsWith('-100')).toBeTruthy();
expect(values[1].textContent).toEqual('1,230.0');
expect(cells[2].classList.contains('text-vega-green-dark')).toBeFalsy();
expect(cells[2].classList.contains('text-vega-red-dark')).toBeTruthy();
expect(cells[2].textContent?.startsWith('-100')).toBeTruthy();
expect(cells[1].textContent).toEqual('123.00');
});
it('displays mark price', async () => {
@ -109,7 +114,7 @@ it('displays mark price', async () => {
});
let cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('12.3');
expect(cells[3].textContent).toEqual('12.3');
await act(async () => {
result.rerender(
@ -125,7 +130,7 @@ it('displays mark price', async () => {
});
cells = screen.getAllByRole('gridcell');
expect(cells[2].textContent).toEqual('-');
expect(cells[3].textContent).toEqual('-');
});
it("displays properly entry, liquidation price and liquidation bar and it's intent", async () => {
@ -134,11 +139,10 @@ it("displays properly entry, liquidation price and liquidation bar and it's inte
result = render(<PositionsTable rowData={singleRowData} />);
});
let cells = screen.getAllByRole('gridcell');
let cell = cells[3];
const entryPrice = cell.firstElementChild?.firstElementChild?.textContent;
const entryPrice = cells[5].firstElementChild?.firstElementChild?.textContent;
const liquidationPrice =
cell.firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cell.lastElementChild;
cells[6].firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cells[6].lastElementChild;
let progressBar = progressBarTrack?.firstElementChild as HTMLElement;
const progressBarWidth = progressBar?.style?.width;
expect(entryPrice).toEqual('13.3');
@ -151,8 +155,7 @@ it("displays properly entry, liquidation price and liquidation bar and it's inte
);
});
cells = screen.getAllByRole('gridcell');
cell = cells[3];
progressBar = cell.lastElementChild?.firstElementChild as HTMLElement;
progressBar = cells[6].lastElementChild?.firstElementChild as HTMLElement;
expect(progressBar?.classList.contains('bg-warning')).toEqual(true);
});
@ -161,24 +164,16 @@ it('displays leverage', async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[4].textContent).toEqual('1.1');
expect(cells[7].textContent).toEqual('1.1');
});
it('displays allocated margin and margin bar', async () => {
it('displays allocated margin', async () => {
await act(async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
const cell = cells[5];
const capitalUtilisation =
cell.firstElementChild?.firstElementChild?.textContent;
const totalBalance = cell.firstElementChild?.lastElementChild?.textContent;
const progressBarTrack = cell.lastElementChild;
const progressBar = progressBarTrack?.firstElementChild as HTMLElement;
const progressBarWidth = progressBar?.style?.width;
expect(capitalUtilisation).toEqual('11.00%');
expect(totalBalance).toEqual('1,234.56');
expect(progressBarWidth).toEqual('11%');
const cell = cells[8];
expect(cell.textContent).toEqual('123,456.00');
});
it('displays realised and unrealised PNL', async () => {
@ -186,6 +181,6 @@ it('displays realised and unrealised PNL', async () => {
render(<PositionsTable rowData={singleRowData} />);
});
const cells = screen.getAllByRole('gridcell');
expect(cells[6].textContent).toEqual('1.23');
expect(cells[7].textContent).toEqual('4.56');
expect(cells[9].textContent).toEqual('1.23');
expect(cells[10].textContent).toEqual('4.56');
});

View File

@ -2,12 +2,13 @@ import classNames from 'classnames';
import { forwardRef } from 'react';
import type { CSSProperties } from 'react';
import type {
ValueFormatterParams,
ValueGetterParams,
ICellRendererParams,
CellRendererSelectorResult,
} from 'ag-grid-community';
import type { ValueProps as PriceCellProps } from '@vegaprotocol/ui-toolkit';
import type {
ValueProps as PriceCellProps,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { EmptyCell, ProgressBarCell } from '@vegaprotocol/ui-toolkit';
import {
PriceFlashCell,
@ -43,13 +44,6 @@ interface Props extends AgGridReactProps {
style?: CSSProperties;
}
export type PositionsTableValueFormatterParams = Omit<
ValueFormatterParams,
'data' | 'value'
> & {
data: Position;
};
export interface MarketNameCellProps {
valueFormatted?: [string, string];
}
@ -118,7 +112,7 @@ const ButtonCell = ({
const progressBarValueFormatter = ({
data,
node,
}: PositionsTableValueFormatterParams):
}: VegaValueFormatterParams<Position, 'liquidationPrice'>):
| PriceCellProps['valueFormatted']
| undefined => {
if (!data || node?.rowPinned) {
@ -151,7 +145,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
resizable: true,
tooltipComponent: TooltipCellComponent,
}}
components={{ PriceFlashCell, ProgressBarCell }}
components={{ AmountCell, PriceFlashCell, ProgressBarCell }}
{...props}
>
<AgGridColumn
@ -160,9 +154,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
cellRenderer={MarketNameCell}
valueFormatter={({
value,
}: PositionsTableValueFormatterParams & {
value: Position['marketName'];
}) => {
}: VegaValueFormatterParams<Position, 'marketName'>) => {
if (!value) {
return undefined;
}
@ -175,33 +167,38 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
/>
<AgGridColumn
headerName={t('Size')}
field="openVolume"
valueGetter={({ node, data }: ValueGetterParams) => {
return node?.rowPinned ? data?.notional : data?.openVolume;
}}
headerName={t('Notional size')}
field="notional"
type="rightAligned"
cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? PriceFlashCell : AmountCell,
};
}}
cellClass="font-mono text-right"
valueFormatter={({
value,
data,
node,
}: PositionsTableValueFormatterParams & {
value: Position['openVolume'];
}): AmountCellProps['valueFormatted'] | string => {
}: VegaValueFormatterParams<Position, 'notional'>): string => {
if (!value || !data) {
return '';
}
return addDecimalsFormatNumber(value, data.decimals);
}}
/>
<AgGridColumn
headerName={t('Open volume')}
field="openVolume"
type="rightAligned"
cellClass="font-mono text-right"
cellClassRules={signedNumberCssClassRules}
valueFormatter={({
value,
data,
}: VegaValueFormatterParams<Position, 'openVolume'>):
| string
| undefined => {
if (!value || !data) {
return undefined;
}
if (node?.rowPinned) {
return addDecimalsFormatNumber(value, data.decimals);
}
return data;
return volumePrefix(
addDecimalsFormatNumber(value, data.positionDecimalPlaces)
);
}}
/>
<AgGridColumn
@ -219,9 +216,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
value,
data,
node,
}: PositionsTableValueFormatterParams & {
value: Position['markPrice'];
}) => {
}: VegaValueFormatterParams<Position, 'markPrice'>) => {
if (!data || !value || node?.rowPinned) {
return undefined;
}
@ -237,16 +232,34 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
);
}}
/>
<AgGridColumn headerName={t('Settlement asset')} field="assetSymbol" />
<AgGridColumn
headerName={t('Entry price')}
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>',
type="rightAligned"
cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
};
}}
valueFormatter={({
data,
value,
node,
}: VegaValueFormatterParams<Position, 'averageEntryPrice'>):
| string
| undefined => {
if (!data || node?.rowPinned || !value) {
return undefined;
}
return addDecimalsFormatNumber(value, data.marketDecimalPlaces);
}}
/>
<AgGridColumn
headerName={t('Liquidation price (est)')}
field="liquidationPrice"
flex={2}
headerTooltip={t(
'Liquidation prices are based on the amount of collateral you have available, the risk of your position and the liquidity on the order book. They can change rapidly based on the profit and loss of your positions and any changes to collateral from opening/closing other positions and making deposits/withdrawals.'
@ -273,41 +286,32 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
}}
valueFormatter={({
value,
node,
}: PositionsTableValueFormatterParams & {
value: Position['currentLeverage'];
}) =>
}: VegaValueFormatterParams<Position, 'currentLeverage'>) =>
value === undefined ? undefined : formatNumber(value.toString(), 1)
}
/>
<AgGridColumn
headerName={t('Margin allocated')}
field="capitalUtilisation"
field="marginAccountBalance"
type="rightAligned"
flex={2}
cellRenderer="ProgressBarCell"
cellRendererSelector={(
params: ICellRendererParams
): CellRendererSelectorResult => {
return {
component: params.node.rowPinned ? EmptyCell : ProgressBarCell,
component: params.node.rowPinned ? EmptyCell : PriceFlashCell,
};
}}
valueFormatter={({
data,
value,
node,
}: PositionsTableValueFormatterParams & {
value: Position['capitalUtilisation'];
}): PriceCellProps['valueFormatted'] | undefined => {
if (!data || node?.rowPinned) {
}: VegaValueFormatterParams<Position, 'marginAccountBalance'>):
| string
| undefined => {
if (!data || node?.rowPinned || !value) {
return undefined;
}
return {
low: `${formatNumber(value, 2)}%`,
high: addDecimalsFormatNumber(data.totalBalance, data.decimals),
value: Number(value),
};
return formatNumber(value, data.decimals);
}}
/>
<AgGridColumn
@ -318,10 +322,8 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['realisedPNL'];
}) =>
value === undefined
}: VegaValueFormatterParams<Position, 'realisedPNL'>) =>
value === undefined || data === undefined
? undefined
: addDecimalsFormatNumber(value.toString(), data.decimals)
}
@ -338,10 +340,8 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
valueFormatter={({
value,
data,
}: PositionsTableValueFormatterParams & {
value: Position['unrealisedPNL'];
}) =>
value === undefined
}: VegaValueFormatterParams<Position, 'unrealisedPNL'>) =>
value === undefined || data === undefined
? undefined
: addDecimalsFormatNumber(value.toString(), data.decimals)
}
@ -356,9 +356,7 @@ export const PositionsTable = forwardRef<AgGridReact, Props>(
type="rightAligned"
valueFormatter={({
value,
}: PositionsTableValueFormatterParams & {
value: Position['updatedAt'];
}) => {
}: VegaValueFormatterParams<Position, 'updatedAt'>) => {
if (!value) {
return value;
}