feat(trading): margin required progressive disclosure (#4755)

This commit is contained in:
m.ray 2023-09-12 22:06:47 +03:00 committed by GitHub
parent 7f5e8ebb15
commit 250a654544
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 125 additions and 61 deletions

View File

@ -1,27 +1,28 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_VEGA_ENV=STAGNET1 NX_VEGA_ENV=TESTNET
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"} NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
NX_VEGA_WALLET_URL=http://localhost:1789 NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket

View File

@ -11,8 +11,7 @@ import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils'; import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as Accordion from '@radix-ui/react-accordion';
import { import {
MARGIN_DIFF_TOOLTIP_TEXT, MARGIN_DIFF_TOOLTIP_TEXT,
@ -24,7 +23,13 @@ import {
} from '../../constants'; } from '../../constants';
import { useEstimateFees } from '../../hooks'; import { useEstimateFees } from '../../hooks';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit'; import {
Accordion,
AccordionChevron,
AccordionPanel,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const emptyValue = '-'; const emptyValue = '-';
@ -246,31 +251,56 @@ export const DealTicketMarginDetails = ({
const quoteName = market.tradableInstrument.instrument.product.quoteName; const quoteName = market.tradableInstrument.instrument.product.quoteName;
return ( return (
<> <div className="flex flex-col gap-2 w-full">
<Accordion.Root type="single" collapsible> <Accordion>
<Accordion.Item value="margin"> <AccordionPanel
<KeyValue itemId="margin"
id="margin-required" trigger={
label={ <AccordionPrimitive.Trigger
<Accordion.Trigger className={TOOLTIP_TRIGGER_CLASS_NAME}> data-testid="accordion-toggle"
{t('Margin required')} className={classNames(
</Accordion.Trigger> 'w-full pt-2',
} 'flex items-center gap-2 text-xs',
value={formatRange( 'group'
marginRequiredBestCase, )}
marginRequiredWorstCase, >
assetDecimals <div
)} data-testid={`deal-ticket-fee-margin-required`}
formattedValue={formatRange( key={'value-dropdown'}
marginRequiredBestCase, className="flex items-center gap-2 justify-between w-full"
marginRequiredWorstCase, >
assetDecimals, <div className="flex items-center gap-1">
quantum <Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
)} <span className="text-muted">{t('Margin required')}</span>
labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)} </Tooltip>
symbol={assetSymbol}
/> <AccordionChevron size={10} />
<Accordion.Content> </div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
noUnderline
>
<div className="font-mono text-right">
{formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col gap-2 w-full">
<KeyValue <KeyValue
label={t('Total margin available')} label={t('Total margin available')}
indent indent
@ -310,12 +340,12 @@ export const DealTicketMarginDetails = ({
quantum quantum
)} )}
/> />
</Accordion.Content> </div>
</Accordion.Item> </AccordionPanel>
</Accordion.Root> </Accordion>
{projectedMargin} {projectedMargin}
<KeyValue <KeyValue
label={t('Liquidation price estimate')} label={t('Liquidation')}
value={liquidationPriceEstimate} value={liquidationPriceEstimate}
formattedValue={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
symbol={quoteName} symbol={quoteName}
@ -329,6 +359,6 @@ export const DealTicketMarginDetails = ({
onClose={onAccountBreakdownDialogClose} onClose={onAccountBreakdownDialogClose}
/> />
)} )}
</> </div>
); );
}; };

View File

@ -538,7 +538,7 @@ const NotionalAndFees = ({
market.positionDecimalPlaces market.positionDecimalPlaces
); );
return ( return (
<div className="mb-4"> <div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue <KeyValue
label={t('Notional')} label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)} value={formatValue(notionalSize, market.decimalPlaces)}

View File

@ -464,7 +464,7 @@ export const DealTicket = ({
)} )}
/> />
)} )}
<div className="mb-4"> <div className="mb-4 flex flex-col gap-2 w-full">
<KeyValue <KeyValue
label={t('Notional')} label={t('Notional')}
value={formatValue(notionalSize, market.decimalPlaces)} value={formatValue(notionalSize, market.decimalPlaces)}

View File

@ -25,11 +25,11 @@ export const KeyValue = ({
}: KeyValuePros) => { }: KeyValuePros) => {
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`; const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
const valueElement = onClick ? ( const valueElement = onClick ? (
<button onClick={onClick} className="text-muted"> <button onClick={onClick} className="font-mono">
{displayValue} {displayValue}
</button> </button>
) : ( ) : (
<div className="text-muted">{displayValue}</div> <div className="font-mono">{displayValue}</div>
); );
return ( return (
<div <div
@ -40,14 +40,14 @@ export const KeyValue = ({
}`} }`}
key={typeof label === 'string' ? label : 'value-dropdown'} key={typeof label === 'string' ? label : 'value-dropdown'}
className={classnames( className={classnames(
'text-xs mt-2 flex justify-between items-center gap-4 flex-wrap', 'text-xs flex justify-between items-center gap-4 flex-wrap',
{ 'ml-2': indent } { 'ml-2': indent }
)} )}
> >
<Tooltip description={labelDescription}> <Tooltip description={labelDescription}>
<div>{label}</div> <div className="text-muted">{label}</div>
</Tooltip> </Tooltip>
<Tooltip description={`${value ?? '-'} ${symbol || ''}`}> <Tooltip description={`${value ?? '-'} ${symbol || ''}`} noUnderline>
{valueElement} {valueElement}
</Tooltip> </Tooltip>
</div> </div>

View File

@ -8,7 +8,7 @@ export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
[settlementAsset] [settlementAsset]
); );
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t( export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
'Estimated total margin that will cover open position, active orders and this order.' 'Estimated total margin that will cover open positions, active orders and this order.'
); );
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.'); export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) => export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
); );
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t( export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
'This is a approximation to the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.' 'This is an approximation (or a range) for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
); );
export const EST_SLIPPAGE = t( export const EST_SLIPPAGE = t(

View File

@ -1,5 +1,6 @@
import * as AccordionPrimitive from '@radix-ui/react-accordion'; import * as AccordionPrimitive from '@radix-ui/react-accordion';
import classNames from 'classnames'; import classNames from 'classnames';
import type { VegaIconProps } from '../icon';
import { VegaIcon, VegaIconNames } from '../icon'; import { VegaIcon, VegaIconNames } from '../icon';
export interface AccordionItemProps { export interface AccordionItemProps {
@ -24,6 +25,28 @@ export const Accordion = ({ panels, children }: AccordionProps) => {
); );
}; };
export const AccordionPanel = ({
trigger,
children,
itemId,
}: {
trigger: React.ReactNode;
children: React.ReactNode;
itemId: string;
}) => {
return (
<AccordionPrimitive.Item value={itemId}>
<AccordionPrimitive.Header>{trigger}</AccordionPrimitive.Header>
<AccordionPrimitive.Content
className="py-3 text-sm"
data-testid="accordion-content"
>
{children}
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
);
};
export const AccordionItem = ({ export const AccordionItem = ({
title, title,
content, content,
@ -31,7 +54,7 @@ export const AccordionItem = ({
}: AccordionPanelProps) => { }: AccordionPanelProps) => {
const triggerClassNames = classNames( const triggerClassNames = classNames(
'w-full py-2', 'w-full py-2',
'flex items-center justify-between border-b border-vega-light-200 dark:border-vega-dark-200 text-sm', 'flex items-center justify-between gap-2 border-b border-vega-light-200 dark:border-vega-dark-200 text-sm',
'group' 'group'
); );
return ( return (
@ -41,7 +64,9 @@ export const AccordionItem = ({
data-testid="accordion-toggle" data-testid="accordion-toggle"
className={triggerClassNames} className={triggerClassNames}
> >
<span data-testid="accordion-title">{title}</span> <span data-testid="accordion-title" className="flex-1 text-left">
{title}
</span>
<AccordionChevron aria-hidden /> <AccordionChevron aria-hidden />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
@ -55,15 +80,17 @@ export const AccordionItem = ({
); );
}; };
export const AccordionChevron = () => { export const AccordionChevron = ({
size = 16,
}: Pick<VegaIconProps, 'size'>) => {
return ( return (
<span <span
className={classNames( className={classNames(
'transform transition ease-in-out duration-300', 'flex transform transition ease-in-out duration-300',
'group-data-[state=open]:rotate-180' 'group-data-[state=open]:rotate-180'
)} )}
> >
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden /> <VegaIcon name={VegaIconNames.CHEVRON_DOWN} aria-hidden size={size} />
</span> </span>
); );
}; };

View File

@ -8,6 +8,7 @@ import {
Portal, Portal,
} from '@radix-ui/react-tooltip'; } from '@radix-ui/react-tooltip';
import type { ITooltipParams } from 'ag-grid-community'; import type { ITooltipParams } from 'ag-grid-community';
import classNames from 'classnames';
const tooltipContentClasses = const tooltipContentClasses =
'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs text-black dark:text-white break-word'; 'max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-2 py-1 z-20 rounded text-xs text-black dark:text-white break-word';
@ -18,10 +19,14 @@ export interface TooltipProps {
align?: 'start' | 'center' | 'end'; align?: 'start' | 'center' | 'end';
side?: 'top' | 'right' | 'bottom' | 'left'; side?: 'top' | 'right' | 'bottom' | 'left';
sideOffset?: number; sideOffset?: number;
noUnderline?: boolean;
} }
export const TOOLTIP_TRIGGER_CLASS_NAME = export const TOOLTIP_TRIGGER_CLASS_NAME = (noUnderline?: boolean) =>
'underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed'; classNames(
{ 'underline underline-offset-2': !noUnderline },
'decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed'
);
// Conditionally rendered tooltip if description content is provided. // Conditionally rendered tooltip if description content is provided.
export const Tooltip = ({ export const Tooltip = ({
@ -31,11 +36,12 @@ export const Tooltip = ({
sideOffset, sideOffset,
align = 'start', align = 'start',
side = 'bottom', side = 'bottom',
noUnderline,
}: TooltipProps) => }: TooltipProps) =>
description ? ( description ? (
<Provider delayDuration={200} skipDelayDuration={100}> <Provider delayDuration={200} skipDelayDuration={100}>
<Root open={open}> <Root open={open}>
<Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME}> <Trigger asChild className={TOOLTIP_TRIGGER_CLASS_NAME(noUnderline)}>
{children} {children}
</Trigger> </Trigger>
{description && ( {description && (