chore: format limits (547) (#1742)

This commit is contained in:
Art 2022-10-19 16:23:18 +02:00 committed by GitHub
parent 5d04efe52d
commit 50611a4ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 275 additions and 102 deletions

View File

@ -77,18 +77,24 @@ describe('withdraw', { tags: '@smoke' }, () => {
}, },
}); });
selectAsset(asset1Name); selectAsset(asset1Name);
cy.getByTestId('balance-available') cy.getByTestId('BALANCE_AVAILABLE_label').should(
.should('contain.text', 'Balance available') 'contain.text',
.find('td') 'Balance available'
.should('have.text', '1,000.00000'); );
cy.getByTestId('withdrawal-threshold') cy.getByTestId('BALANCE_AVAILABLE_value').should(
.should('contain.text', 'Delayed withdrawal threshold') 'have.text',
.find('td') '1,000.00000'
.should('contain.text', '1m+'); );
cy.getByTestId('delay-time') cy.getByTestId('WITHDRAWAL_THRESHOLD_label').should(
.should('contain.text', 'Delay time') 'contain.text',
.find('td') 'Delayed withdrawal threshold'
.should('have.text', 'None'); );
cy.getByTestId('WITHDRAWAL_THRESHOLD_value').should(
'contain.text',
'100.00000'
);
cy.getByTestId('DELAY_TIME_label').should('contain.text', 'Delay time');
cy.getByTestId('DELAY_TIME_value').should('have.text', 'None');
cy.get(amountField).clear().type('10'); cy.get(amountField).clear().type('10');
cy.getByTestId(submitWithdrawBtn).click(); cy.getByTestId(submitWithdrawBtn).click();
cy.getByTestId('dialog-title').should( cy.getByTestId('dialog-title').should(

View File

@ -227,20 +227,12 @@ describe('Deposit form', () => {
); );
// Check deposit limit is displayed // Check deposit limit is displayed
expect( expect(screen.getByTestId('BALANCE_AVAILABLE_value')).toHaveTextContent(
screen.getByText('Balance available', { selector: 'th' }) '50'
.nextElementSibling );
).toHaveTextContent(balance.toString()); expect(screen.getByTestId('MAX_LIMIT_value')).toHaveTextContent('20');
expect( expect(screen.getByTestId('DEPOSITED_value')).toHaveTextContent('10');
screen.getByText('Maximum total deposit amount', { selector: 'th' }) expect(screen.getByTestId('REMAINING_value')).toHaveTextContent('10');
.nextElementSibling
).toHaveTextContent(max.toString());
expect(
screen.getByText('Deposited', { selector: 'th' }).nextElementSibling
).toHaveTextContent(deposited.toString());
expect(
screen.getByText('Remaining', { selector: 'th' }).nextElementSibling
).toHaveTextContent(max.minus(deposited).toString());
fireEvent.change(screen.getByLabelText('Amount'), { fireEvent.change(screen.getByLabelText('Amount'), {
target: { value: '8' }, target: { value: '8' },

View File

@ -220,7 +220,12 @@ export const DepositForm = ({
</FormGroup> </FormGroup>
{selectedAsset && max && deposited && ( {selectedAsset && max && deposited && (
<div className="mb-6"> <div className="mb-6">
<DepositLimits max={max} deposited={deposited} balance={balance} /> <DepositLimits
max={max}
deposited={deposited}
balance={balance}
asset={selectedAsset}
/>
</div> </div>
)} )}
<FormGroup label={t('Amount')} labelFor="amount"> <FormGroup label={t('Amount')} labelFor="amount">

View File

@ -1,60 +1,65 @@
import { formatNumber, t } from '@vegaprotocol/react-helpers'; import type { Asset } from '@vegaprotocol/assets';
import { compactNumber, t } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js'; import type BigNumber from 'bignumber.js';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
interface DepositLimitsProps { interface DepositLimitsProps {
max: BigNumber; max: BigNumber;
deposited: BigNumber; deposited: BigNumber;
asset: Asset;
balance?: BigNumber; balance?: BigNumber;
} }
export const DepositLimits = ({ export const DepositLimits = ({
max, max,
deposited, deposited,
asset,
balance, balance,
}: DepositLimitsProps) => { }: DepositLimitsProps) => {
let maxLimit = ''; const limits = [
if (max.isEqualTo(Infinity)) { {
maxLimit = t('No limit'); key: 'BALANCE_AVAILABLE',
} else if (max.isGreaterThan(1_000_000)) { label: t('Balance available'),
maxLimit = t('1m+'); rawValue: balance,
} else { value: balance ? compactNumber(balance, asset.decimals) : '-',
maxLimit = max.toString(); },
} {
key: 'MAX_LIMIT',
let remaining = ''; label: t('Maximum total deposit amount'),
if (deposited.isEqualTo(0)) { rawValue: max,
remaining = maxLimit; value: compactNumber(max, asset.decimals),
} else { },
const amountRemaining = max.minus(deposited); {
remaining = amountRemaining.isGreaterThan(1_000_000) key: 'DEPOSITED',
? t('1m+') label: t('Deposited'),
: amountRemaining.toString(); rawValue: deposited,
} value: compactNumber(deposited, asset.decimals),
},
{
key: 'REMAINING',
label: t('Remaining'),
rawValue: max.minus(deposited),
value: compactNumber(max.minus(deposited), asset.decimals),
},
];
return ( return (
<table className="w-full text-sm"> <KeyValueTable>
<tbody> {limits.map(({ key, label, rawValue, value }) => (
<tr> <KeyValueTableRow>
<th className="text-left font-normal">{t('Balance available')}</th> <div data-testid={`${key}_label`}>{label}</div>
<td className="text-right"> <div
{balance ? formatNumber(balance) : '-'} data-testid={`${key}_value`}
</td> className="truncate"
</tr> title={rawValue?.toString()}
<tr> >
<th className="text-left font-normal"> {value}
{t('Maximum total deposit amount')} </div>
</th> </KeyValueTableRow>
<td className="text-right">{maxLimit}</td> ))}
</tr> </KeyValueTable>
<tr>
<th className="text-left font-normal">{t('Deposited')}</th>
<td className="text-right">{formatNumber(deposited)}</td>
</tr>
<tr>
<th className="text-left font-normal">{t('Remaining')}</th>
<td className="text-right">{remaining}</td>
</tr>
</tbody>
</table>
); );
}; };

View File

@ -4,6 +4,7 @@ import {
formatNumberPercentage, formatNumberPercentage,
toNumberParts, toNumberParts,
isNumeric, isNumeric,
compactNumber,
} from './number'; } from './number';
describe('formatNumber and formatNumberPercentage', () => { describe('formatNumber and formatNumberPercentage', () => {
@ -89,3 +90,93 @@ describe('isNumeric', () => {
} }
); );
}); });
describe('compactNumber', () => {
const short: [BigNumber, string | JSX.Element, number | 'infer'][] = [
[new BigNumber(Infinity), '∞', 'infer'],
[new BigNumber(-Infinity), '-∞', 'infer'],
[new BigNumber(0), '0', 'infer'],
[new BigNumber(1), '1', 'infer'],
[new BigNumber(100), '100', 'infer'],
[new BigNumber(100.456601), '100.456601', 'infer'],
[new BigNumber(1_000), '1,000', 'infer'],
[new BigNumber(999_999), '999,999', 'infer'],
[new BigNumber(1_000_000), '1M', 'infer'],
[new BigNumber(100_000_000), '100M', 'infer'],
[new BigNumber(1_000_000_000), '1B', 'infer'],
[new BigNumber(1_000_000_000_000), '1T', 'infer'],
[new BigNumber(3.23e12), '3.23T', 2],
[new BigNumber(3.23e12), '3.23000T', 5],
[
new BigNumber(3.23e24),
<span>
3.23000{' '}
<span>
&times; 10<sup>24</sup>
</span>
</span>,
5,
],
[
new BigNumber(1.579208923731619e59),
<span>
1.57921{' '}
<span>
&times; 10
<sup>59</sup>
</span>
</span>,
5,
],
];
it.each(short)(
'compacts %d to %p (decimal places: %p)',
(input, output, decimals) => {
expect(compactNumber(input, decimals)).toEqual(output);
}
);
const long: [BigNumber, string | JSX.Element, number | 'infer'][] = [
[new BigNumber(Infinity), '∞', 'infer'],
[new BigNumber(-Infinity), '-∞', 'infer'],
[new BigNumber(0), '0', 'infer'],
[new BigNumber(1), '1', 'infer'],
[new BigNumber(100), '100', 'infer'],
[new BigNumber(100.456601), '100.456601', 'infer'],
[new BigNumber(1_000), '1,000', 'infer'],
[new BigNumber(999_999), '999,999', 'infer'],
[new BigNumber(1_000_000), '1 million', 'infer'],
[new BigNumber(100_000_000), '100 million', 'infer'],
[new BigNumber(1_000_000_000), '1 billion', 'infer'],
[new BigNumber(1_000_000_000_000), '1 trillion', 'infer'],
[new BigNumber(3.23e12), '3.23 trillion', 2],
[new BigNumber(3.23e12), '3.23000 trillion', 5],
[
new BigNumber(3.23e24),
<span>
3.23000{' '}
<span>
&times; 10<sup>24</sup>
</span>
</span>,
5,
],
[
new BigNumber(1.579208923731619e59),
<span>
1.57921{' '}
<span>
&times; 10
<sup>59</sup>
</span>
</span>,
5,
],
];
it.each(long)(
'compacts %d to %p (decimal places: %p)',
(input, output, decimals) => {
expect(compactNumber(input, decimals, 'long')).toEqual(output);
}
);
});

View File

@ -100,3 +100,67 @@ export const useNumberParts = (
export const isNumeric = ( export const isNumeric = (
value?: string | number | BigNumber | null value?: string | number | BigNumber | null
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value)); ): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
const INFINITY = '∞';
const DEFAULT_COMPACT_ABOVE = 1_000_000;
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
/**
* Compacts given number to human readable format.
* @param number
* @param decimals Number of decimal places
* @param compactDisplay Display mode; short -> 1e6 == 1M; ling -> 1e6 1 million
* @param compactAbove Compact number above threshold
* @param cap Use scientific notation above threshold
*/
export const compactNumber = (
number: BigNumber,
decimals: number | 'infer' = 'infer',
compactDisplay: 'short' | 'long' = 'short',
compactAbove = DEFAULT_COMPACT_ABOVE,
cap = DEFAULT_COMPACT_CAP
) => {
if (!number.isFinite()) return `${number.isNegative() ? '-' : ''}${INFINITY}`;
const decimalPlaces =
(decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
return formatNumber(number, decimalPlaces);
}
/**
* Note: it compacts number up to 1_000_000_000_000 (1e12) -> 1T, all above is formatted as iteration of T.
* Example: 1.579208923731619e59 -> 157,920,892,373,161,900,000,000,000,000,000,000,000,000,000,000T
*/
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.max(0, decimalPlaces),
maximumFractionDigits: Math.max(0, decimalPlaces),
notation: 'compact',
compactDisplay,
});
const scientificNumFormat = new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.max(0, decimalPlaces),
maximumFractionDigits: Math.max(0, decimalPlaces),
notation: 'scientific',
});
if (number.isGreaterThan(DEFAULT_COMPACT_CAP)) {
const r = /E(\d+)$/i;
const formatted = scientificNumFormat.format(Number(number));
const eNotation = formatted.match(r);
if (eNotation && eNotation.length > 1) {
const power = eNotation[1];
return (
<span>
{formatted.replace(r, '')}{' '}
<span>
&times; 10
<sup>{power}</sup>
</span>
</span>
);
}
}
return compactNumFormat.format(Number(number));
};

View File

@ -1,5 +1,4 @@
import type { Asset } from '@vegaprotocol/assets'; import type { Asset } from '@vegaprotocol/assets';
import { formatNumber } from '@vegaprotocol/react-helpers';
import { import {
ethereumAddress, ethereumAddress,
minSafe, minSafe,
@ -147,7 +146,8 @@ export const WithdrawForm = ({
amount={amount} amount={amount}
threshold={threshold} threshold={threshold}
delay={delay} delay={delay}
balance={formatNumber(balance, selectedAsset.decimals)} balance={balance}
asset={selectedAsset}
/> />
</div> </div>
)} )}

View File

@ -1,12 +1,15 @@
import { t } from '@vegaprotocol/react-helpers'; import type { Asset } from '@vegaprotocol/assets';
import { compactNumber, t } from '@vegaprotocol/react-helpers';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
interface WithdrawLimitsProps { interface WithdrawLimitsProps {
amount: string; amount: string;
threshold: BigNumber; threshold: BigNumber;
balance: string; balance: BigNumber;
delay: number | undefined; delay: number | undefined;
asset: Asset;
} }
export const WithdrawLimits = ({ export const WithdrawLimits = ({
@ -14,40 +17,47 @@ export const WithdrawLimits = ({
threshold, threshold,
balance, balance,
delay, delay,
asset,
}: WithdrawLimitsProps) => { }: WithdrawLimitsProps) => {
let text = '';
if (threshold.isEqualTo(Infinity)) {
text = t('No limit');
} else if (threshold.isGreaterThan(1_000_000)) {
text = t('1m+');
} else {
text = threshold.toString();
}
const delayTime = const delayTime =
new BigNumber(amount).isGreaterThan(threshold) && delay new BigNumber(amount).isGreaterThan(threshold) && delay
? formatDistanceToNow(Date.now() + delay * 1000) ? formatDistanceToNow(Date.now() + delay * 1000)
: t('None'); : t('None');
const limits = [
{
key: 'BALANCE_AVAILABLE',
label: t('Balance available'),
rawValue: balance,
value: balance ? compactNumber(balance, asset.decimals) : '-',
},
{
key: 'WITHDRAWAL_THRESHOLD',
label: t('Delayed withdrawal threshold'),
rawValue: threshold,
value: compactNumber(threshold, asset.decimals),
},
{
key: 'DELAY_TIME',
label: t('Delay time'),
value: delayTime,
},
];
return ( return (
<table className="w-full text-sm"> <KeyValueTable>
<tbody> {limits.map(({ key, label, rawValue, value }) => (
<tr data-testid="balance-available"> <KeyValueTableRow>
<th className="text-left font-normal">{t('Balance available')}</th> <div data-testid={`${key}_label`}>{label}</div>
<td className="text-right">{balance}</td> <div
</tr> data-testid={`${key}_value`}
<tr data-testid="withdrawal-threshold"> className="truncate"
<th className="text-left font-normal"> title={rawValue?.toString()}
{t('Delayed withdrawal threshold')} >
</th> {value}
<td className="text-right">{text}</td> </div>
</tr> </KeyValueTableRow>
<tr data-testid="delay-time"> ))}
<th className="text-left font-normal">{t('Delay time')}</th> </KeyValueTable>
<td className="text-right">{delayTime}</td>
</tr>
</tbody>
</table>
); );
}; };