feat(positions): improve tooltips (#5481)

This commit is contained in:
Bartłomiej Głownia 2023-12-13 15:36:57 +01:00 committed by GitHub
parent 08c57b6759
commit 8b94a75ba4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 113 additions and 116 deletions

View File

@ -8,11 +8,13 @@ export const LiquidationPrice = ({
openVolume, openVolume,
collateralAvailable, collateralAvailable,
decimalPlaces, decimalPlaces,
className,
}: { }: {
marketId: string; marketId: string;
openVolume: string; openVolume: string;
collateralAvailable: string; collateralAvailable: string;
decimalPlaces: number; decimalPlaces: number;
className?: string;
}) => { }) => {
const t = useT(); const t = useT();
const { data: currentData, previousData } = useEstimatePositionQuery({ const { data: currentData, previousData } = useEstimatePositionQuery({
@ -39,22 +41,19 @@ export const LiquidationPrice = ({
return ( return (
<Tooltip <Tooltip
align="end"
description={ description={
<table> <dl className="grid grid-cols-2">
<tbody> <dt>{t('Worst case')}</dt>
<tr> <dd className="pl-2 text-right font-mono">{worstCase}</dd>
<th>{t('Worst case')}</th> <dt className="font-normal">{t('Best case')}</dt>
<td className="pl-2 text-right font-mono">{worstCase}</td> <dd className="pl-2 text-right font-mono">{bestCase}</dd>
</tr> </dl>
<tr>
<th>{t('Best case')}</th>
<td className="pl-2 text-right font-mono">{bestCase}</td>
</tr>
</tbody>
</table>
} }
> >
<span data-testid="liquidation-price">{worstCase}</span> <span data-testid="liquidation-price" className={className}>
{worstCase}
</span>
</Tooltip> </Tooltip>
); );
}; };

View File

@ -314,7 +314,9 @@ describe('Positions', () => {
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const cell = cells[1]; const cell = cells[1];
await userEvent.hover(cell); const tooltipTrigger = cell.querySelector('[data-state="closed"]');
expect(tooltipTrigger).not.toBeNull();
await userEvent.hover(tooltipTrigger as Element);
const tooltip = within(await screen.findByRole('tooltip')); const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText(data.text)).toBeInTheDocument(); expect(tooltip.getByText(data.text)).toBeInTheDocument();
}); });
@ -329,8 +331,9 @@ describe('Positions', () => {
}); });
const cells = screen.getAllByRole('gridcell'); const cells = screen.getAllByRole('gridcell');
const cell = cells[5]; const cell = cells[5];
const tooltipTrigger = cell.querySelector('[data-state="closed"]');
await userEvent.hover(cell); expect(tooltipTrigger).not.toBeNull();
await userEvent.hover(tooltipTrigger as Element);
const tooltip = within(await screen.findByRole('tooltip')); const tooltip = within(await screen.findByRole('tooltip'));
expect(tooltip.getByText('Realised PNL: 1.23')).toBeInTheDocument(); expect(tooltip.getByText('Realised PNL: 1.23')).toBeInTheDocument();
expect( expect(

View File

@ -1,5 +1,5 @@
import { useMemo, type CSSProperties, type ReactNode } from 'react'; import { useMemo, type CSSProperties, type ReactNode } from 'react';
import { type ColDef, type ITooltipParams } from 'ag-grid-community'; import { type ColDef } from 'ag-grid-community';
import { import {
AgGrid, AgGrid,
COL_DEFS, COL_DEFS,
@ -20,6 +20,7 @@ import {
ExternalLink, ExternalLink,
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
Tooltip,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { import {
volumePrefix, volumePrefix,
@ -148,15 +149,6 @@ export const PositionsTable = ({
data.positionDecimalPlaces data.positionDecimalPlaces
).toNumber(); ).toNumber();
}, },
tooltipValueGetter: ({ data }: ITooltipParams<Position>) => {
if (
!data ||
data.status === PositionStatus.POSITION_STATUS_UNSPECIFIED
) {
return null;
}
return data.status;
},
valueFormatter: ({ valueFormatter: ({
data, data,
}: VegaValueFormatterParams<Position, 'openVolume'>): string => { }: VegaValueFormatterParams<Position, 'openVolume'>): string => {
@ -171,68 +163,6 @@ export const PositionsTable = ({
return vol; return vol;
}, },
tooltipComponent: (args: ITooltipParams<Position>) => {
if (!args.data) {
return null;
}
const POSITION_RESOLUTION_LINK =
DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (args.data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough {{assetSymbol}} collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
{ assetSymbol: args.data.assetSymbol }
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
return (
<TooltipCellComponent
{...args}
value={
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: {{status}}', {
status: PositionStatusMapping[args.data.status],
})}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
}
/>
);
},
cellRenderer: OpenVolumeCell, cellRenderer: OpenVolumeCell,
}, },
{ {
@ -337,12 +267,15 @@ export const PositionsTable = ({
return '-'; return '-';
} }
return ( return (
<LiquidationPrice <div className="flex h-[45px] items-center">
marketId={data.marketId} <LiquidationPrice
openVolume={data.openVolume} className="block text-right grow"
collateralAvailable={data.totalBalance} marketId={data.marketId}
decimalPlaces={data.marketDecimalPlaces} openVolume={data.openVolume}
/> collateralAvailable={data.totalBalance}
decimalPlaces={data.marketDecimalPlaces}
/>
</div>
); );
}, },
}, },
@ -354,13 +287,13 @@ export const PositionsTable = ({
cellClass: 'font-mono text-right', cellClass: 'font-mono text-right',
filter: 'agNumberColumnFilter', filter: 'agNumberColumnFilter',
valueGetter: realisedPNLValueGetter, valueGetter: realisedPNLValueGetter,
// @ts-ignore no type overlap, but the functions are identical cellRenderer: (
tooltipValueGetter: realisedPNLValueGetter, args: VegaICellRendererParams<Position, 'realisedPNL'>
tooltipComponent: (args: ITooltipParams) => { ) => {
const LOSS_SOCIALIZATION_LINK = const LOSS_SOCIALIZATION_LINK =
DocsLinks?.LOSS_SOCIALIZATION ?? ''; DocsLinks?.LOSS_SOCIALIZATION ?? '';
if (!args.data) { if (!args.data || args.value === undefined) {
return null; return null;
} }
@ -371,7 +304,11 @@ export const PositionsTable = ({
if (losses <= 0) { if (losses <= 0) {
// eslint-disable-next-line react/jsx-no-useless-fragment // eslint-disable-next-line react/jsx-no-useless-fragment
return ( return (
<TooltipCellComponent {...args} value={args.valueFormatted} /> <Tooltip description={args.valueFormatted} align="end">
<div>
<PNLCell {...args} />
</div>
</Tooltip>
); );
} }
@ -381,20 +318,24 @@ export const PositionsTable = ({
); );
return ( return (
<TooltipCellComponent <Tooltip
{...args} align="end"
value={ description={
<> <>
<p className="mb-2"> <p className="mb-2">
{t('Realised PNL: {{value}}', { {t('Realised PNL: {{value}}', {
value: args.value, nsSeparator: '*',
replace: { value: args.value },
})} })}
</p> </p>
<p className="mb-2"> <p className="mb-2">
{t( {t(
'Lifetime loss socialisation deductions: {{losses}}', 'Lifetime loss socialisation deductions: {{losses}}',
{ {
losses: lossesFormatted, nsSeparator: '*',
replace: {
losses: lossesFormatted,
},
} }
)} )}
</p> </p>
@ -411,7 +352,11 @@ export const PositionsTable = ({
)} )}
</> </>
} }
/> >
<div>
<PNLCell {...args} />
</div>
</Tooltip>
); );
}, },
valueFormatter: ({ valueFormatter: ({
@ -428,7 +373,6 @@ export const PositionsTable = ({
headerTooltip: t( headerTooltip: t(
'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.' 'Profit or loss is realised whenever your position is reduced to zero and the margin is released back to your collateral balance. P&L excludes any fees paid.'
), ),
cellRenderer: PNLCell,
}, },
{ {
headerName: t('Unrealised PNL'), headerName: t('Unrealised PNL'),
@ -520,10 +464,66 @@ export const OpenVolumeCell = ({
valueFormatted, valueFormatted,
data, data,
}: VegaICellRendererParams<Position, 'openVolume'>) => { }: VegaICellRendererParams<Position, 'openVolume'>) => {
const t = useT();
if (!valueFormatted || !data || !data.notional) { if (!valueFormatted || !data || !data.notional) {
return <>-</>; return <>-</>;
} }
const POSITION_RESOLUTION_LINK = DocsLinks?.POSITION_RESOLUTION ?? '';
let primaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
primaryTooltip = t('Your position was closed.');
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
primaryTooltip = t('Your open orders were cancelled.');
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
primaryTooltip = t('Your position is distressed.');
break;
}
let secondaryTooltip;
switch (data.status) {
case PositionStatus.POSITION_STATUS_CLOSED_OUT:
secondaryTooltip = t(
`You did not have enough {{assetSymbol}} collateral to meet the maintenance margin requirements for your position, so it was closed by the network.`,
{ assetSymbol: data.assetSymbol }
);
break;
case PositionStatus.POSITION_STATUS_ORDERS_CLOSED:
secondaryTooltip = t(
'The position was distressed, but removing open orders from the book brought the margin level back to a point where the open position could be maintained.'
);
break;
case PositionStatus.POSITION_STATUS_DISTRESSED:
secondaryTooltip = t(
'The position was distressed, but could not be closed out - orders were removed from the book, and the open volume will be closed out once there is sufficient volume on the book.'
);
break;
default:
secondaryTooltip = t('Maintained by network');
}
const description = (
<>
<p className="mb-2">{primaryTooltip}</p>
<p className="mb-2">{secondaryTooltip}</p>
<p className="mb-2">
{t('Status: {{status}}', {
nsSeparator: '*',
replace: {
status: PositionStatusMapping[data.status],
},
})}
</p>
{POSITION_RESOLUTION_LINK && (
<ExternalLink href={POSITION_RESOLUTION_LINK}>
{t('Read more about position resolution')}
</ExternalLink>
)}
</>
);
const notional = addDecimalsFormatNumber( const notional = addDecimalsFormatNumber(
data.notional, data.notional,
data.marketDecimalPlaces data.marketDecimalPlaces
@ -539,16 +539,11 @@ export const OpenVolumeCell = ({
} }
return ( return (
<WarningCell <Tooltip description={description}>
showIcon={ <div>
// not sure why but data.status has become a union of all the enum values <WarningCell showIcon>{cellContent}</WarningCell>
// rather than just being the enum itself </div>
(data.status as PositionStatus) !== </Tooltip>
PositionStatus.POSITION_STATUS_UNSPECIFIED
}
>
{cellContent}
</WarningCell>
); );
}; };