Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
460c33ecd6 | ||
|
|
66015819c0 | ||
|
|
c440abc77d | ||
|
|
fbafc726a4 | ||
|
|
dd1890d8c6 | ||
|
|
c8e624eaba |
@@ -39,7 +39,6 @@ context('Market page', { tags: '@regression' }, function () {
|
||||
cy.contains('Test market 1').click();
|
||||
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
|
||||
cy.validate_element_from_table('Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Market ID', this.createdMarketId);
|
||||
cy.validate_element_from_table('Trading Mode', 'Opening auction');
|
||||
cy.validate_element_from_table('Market Decimal Places', '5');
|
||||
cy.validate_element_from_table('Position Decimal Places', '5');
|
||||
|
||||
@@ -537,10 +537,10 @@ describe(
|
||||
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
|
||||
cy.reload();
|
||||
getProposalFromTitle('Governance transfer proposal').within(() => {
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(governanceTransferToggle).click();
|
||||
cy.getByTestId('proposal-transfer-details-table').within(() => {
|
||||
getProposalInformationFromTable('Source Type')
|
||||
@@ -590,7 +590,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
|
||||
getProposalInformationFromTable('Error details')
|
||||
.invoke('text')
|
||||
.and('eq', 'Governance transfer invalid transfer id not found');
|
||||
|
||||
@@ -909,6 +909,14 @@
|
||||
"BenefitTierReferralDiscountFactorDescription": "The proportion of the referee's taker fees to be discounted",
|
||||
"BenefitTierReferralRewardFactor": "Referral reward factor",
|
||||
"BenefitTierReferralRewardFactorDescription": "The proportion of the referee's taker fees to be rewarded to the referrer",
|
||||
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
|
||||
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
|
||||
"BenefitTierMinimumQuantumBalance": "Minimum quantum balance",
|
||||
"BenefitTierMinimumQuantumBalanceDescription": "The minimum amount of the vesting token to qualify",
|
||||
"BenefitTierVestingMultiplier": "Vesting multiplier",
|
||||
"BenefitTierVestingMultiplierDescription": "Vesting multiplier for the tier",
|
||||
"BenefitTierRewardMultiplier": "Reward multiplier",
|
||||
"BenefitTierRewardMultiplierDescription": "The multiplier",
|
||||
"StakingTiers": "Staking tiers",
|
||||
"StakingTierMinimumStakedTokens": "Minimum staked tokens",
|
||||
"StakingTierMinimumStakedTokensDescription": "Required number of governance tokens ($VEGA) a referrer must have staked to receive the multiplier",
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './proposal-update-benefit-tiers-details';
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
appState: {
|
||||
decimals: 2,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockVestingBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockActivityStreakBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
describe('ProposalUpdateBenefitTiers', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalUpdateBenefitTiers proposal={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateNetworkParameter', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are relevant fields that are empty', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a valid vesting benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 3')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum quantum balance').length).toBe(3);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(3);
|
||||
|
||||
expect(screen.getByText('0.00000000000001')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.05x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.0000005')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('0.00001')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render a valid activity streak benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers
|
||||
proposal={mockActivityStreakBenefitTierProposal}
|
||||
/>
|
||||
);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tier 2')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Minimum activity streak').length).toBe(2);
|
||||
expect(screen.getAllByText('Vesting multiplier').length).toBe(2);
|
||||
expect(screen.getAllByText('Reward multiplier').length).toBe(2);
|
||||
|
||||
expect(screen.getByText('10000')).toBeInTheDocument();
|
||||
expect(screen.getByText('5x')).toBeInTheDocument();
|
||||
expect(screen.getByText('0.1x')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('10000000000000')).toBeInTheDocument();
|
||||
expect(screen.getByText('100x')).toBeInTheDocument();
|
||||
expect(screen.getByText('10x')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
formatMinimumStakedTokens,
|
||||
formatReferralRewardMultiplier,
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
// These types are not generated as it's not known how dynamic these are
|
||||
type VestingBenefitTier = {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
};
|
||||
|
||||
type ActivityStreakBenefitTier = {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
};
|
||||
|
||||
export type BenefitTiers =
|
||||
| Array<ActivityStreakBenefitTier>
|
||||
| Array<VestingBenefitTier>;
|
||||
|
||||
export function getBenefitTiers(json: string): BenefitTiers {
|
||||
try {
|
||||
const parsed = JSON.parse(json);
|
||||
return parsed.tiers;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export const formatVolumeDiscountFactor = (value: string) => {
|
||||
return formatNumberPercentage(new BigNumber(value).times(100));
|
||||
};
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Special rendered for network proposals that change any benefit tiers,
|
||||
* which is detected by:
|
||||
* 1) it being a network parameter change
|
||||
* 2) the name of the field ending in `.benefitTiers`
|
||||
*
|
||||
* It only renders known fields so that they can be formatted correctly.
|
||||
*/
|
||||
export const ProposalUpdateBenefitTiers = ({
|
||||
proposal,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (
|
||||
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
|
||||
const benefitTiers = getBenefitTiers(benefitTiersString);
|
||||
|
||||
if (!benefitTiers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-testid="proposal-update-benefit-tiers">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
{benefitTiers && (
|
||||
<div
|
||||
className="mb-6"
|
||||
data-testid="proposal-volume-discount-program-benefit-tiers"
|
||||
>
|
||||
<h3 className="mb-3 uppercase font-semibold text-lg">
|
||||
{t('BenefitTiers')}
|
||||
</h3>
|
||||
<KeyValueTable>
|
||||
{benefitTiers
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Number(a.reward_multiplier) - Number(b.reward_multiplier)
|
||||
)
|
||||
.map((benefitTier, index) => (
|
||||
<div className="mb-4" key={index}>
|
||||
<h4 className="font-semibold uppercase">
|
||||
Tier {index + 1}
|
||||
</h4>
|
||||
{'minimum_activity_streak' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mas-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumActivityStreakDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumActivityStreak')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{benefitTier.minimum_activity_streak}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'minimum_quantum_balance' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`mqb-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'BenefitTierMinimumQuantumBalanceDescription'
|
||||
)}
|
||||
>
|
||||
<span>{t('BenefitTierMinimumQuantumBalance')}</span>
|
||||
</Tooltip>
|
||||
|
||||
{formatMinimumStakedTokens(
|
||||
benefitTier.minimum_quantum_balance,
|
||||
18
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'vesting_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`vm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip
|
||||
description={t('BenefitTierVestingMultiplier')}
|
||||
>
|
||||
<span>{t('BenefitTierVestingMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.vesting_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
{'reward_multiplier' in benefitTier && (
|
||||
<KeyValueTableRow
|
||||
data-testid={`rm-${benefitTier.reward_multiplier}`}
|
||||
>
|
||||
<Tooltip description={t('BenefitTierRewardMultiplier')}>
|
||||
<span>{t('BenefitTierRewardMultiplier')}</span>
|
||||
</Tooltip>
|
||||
{formatReferralRewardMultiplier(
|
||||
benefitTier.reward_multiplier
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
)}
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
@@ -243,6 +244,14 @@ export const Proposal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
proposal.terms.change.networkParameter.key.slice(-13) ===
|
||||
'.benefitTiers' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateBenefitTiers proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{governanceTransferDetails}
|
||||
|
||||
<div className="mb-10">
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
NetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { MarketFees } from './market-fees';
|
||||
@@ -284,7 +284,7 @@ export const CurrentVolume = ({
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatNumber(windowLengthVolume)}
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', epochs.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -88,14 +88,18 @@ export const getReferralBenefitTier = (
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
*
|
||||
* Formula for calculating the adjusted fees
|
||||
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
|
||||
*/
|
||||
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||
const totalDiscount = discounts.reduce(
|
||||
(sum, d) => sum.plus(d),
|
||||
new BigNumber(0)
|
||||
);
|
||||
return totalFee
|
||||
.times(BigNumber.max(0, new BigNumber(1).minus(totalDiscount)))
|
||||
.toNumber();
|
||||
|
||||
const combinedFactors = discounts.reduce((acc, d) => {
|
||||
return acc.times(new BigNumber(1).minus(d));
|
||||
}, new BigNumber(1));
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
};
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CandlesMenu } from './candles-menu';
|
||||
import {
|
||||
useCandlesChartSettingsStore,
|
||||
DEFAULT_CHART_SETTINGS,
|
||||
} from './use-candles-chart-settings';
|
||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||
|
||||
describe('CandlesMenu', () => {
|
||||
it('should render with the correct default studies', async () => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
const openDropdown = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Studies',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked');
|
||||
expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useCandlesChartSettingsStore.setState({ overlays: [], studies: [] });
|
||||
});
|
||||
|
||||
it('should render with the correct default overlays', async () => {
|
||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(overlayLabels[overlay as Overlay])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with the correct default studies and overlays', async () => {
|
||||
useCandlesChartSettingsStore.setState(DEFAULT_CHART_SETTINGS);
|
||||
|
||||
render(<CandlesMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Overlays',
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Moving average')).toHaveAttribute(
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
|
||||
@@ -107,7 +107,7 @@ export const CandlesMenu = () => {
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Overlays')}
|
||||
{t('Indicators')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
@@ -132,18 +132,6 @@ export const CandlesMenu = () => {
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownCheckboxItem>
|
||||
))}
|
||||
</TradingDropdownContent>
|
||||
</TradingDropdown>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Studies')}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
>
|
||||
<TradingDropdownContent align={contentAlign}>
|
||||
{Object.values(Study).map((study) => (
|
||||
<TradingDropdownCheckboxItem
|
||||
key={study}
|
||||
|
||||
@@ -24,7 +24,7 @@ const STUDY_ORDER: Study[] = [
|
||||
Study.VOLUME,
|
||||
];
|
||||
|
||||
const DEFAULT_CHART_SETTINGS = {
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
|
||||
@@ -3,7 +3,12 @@ import throttle from 'lodash/throttle';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import type { OperationVariables } from '@apollo/client';
|
||||
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
|
||||
import type {
|
||||
Subscribe,
|
||||
Load,
|
||||
UpdateCallback,
|
||||
PageInfo,
|
||||
} from './generic-data-provider';
|
||||
import { variablesIsEqualCustomizer } from './generic-data-provider';
|
||||
|
||||
export interface useDataProviderParams<
|
||||
@@ -12,13 +17,23 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -30,7 +45,7 @@ export interface useDataProviderParams<
|
||||
* @param dataProvider subscribe function created by makeDataProvider
|
||||
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||
* @param variables optional
|
||||
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
*/
|
||||
export const useDataProvider = <
|
||||
Data,
|
||||
@@ -48,6 +63,7 @@ export const useDataProvider = <
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const loadRef = useRef<Load<Data> | undefined>(undefined);
|
||||
@@ -93,6 +109,7 @@ export const useDataProvider = <
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
pageInfo,
|
||||
} = args;
|
||||
setError(error);
|
||||
setLoading(!loaded && loading);
|
||||
@@ -104,21 +121,22 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data })))
|
||||
updateRef.current({ delta, data, pageInfo })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data })
|
||||
insertRef.current({ insertionData, data, pageInfo })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setData(data);
|
||||
setPageInfo(pageInfo);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data });
|
||||
updateRef.current({ data, pageInfo });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -136,15 +154,13 @@ export const useDataProvider = <
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setPageInfo(null);
|
||||
setError(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
updateRef.current({ data: null, pageInfo: null });
|
||||
}
|
||||
if (skip) {
|
||||
setLoading(false);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -165,6 +181,7 @@ export const useDataProvider = <
|
||||
}, [client, dataProvider, callback, variables, skip]);
|
||||
return {
|
||||
data,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
flush,
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
|
||||
variables: {
|
||||
partyId: 'pubKey',
|
||||
filter: { liveOnly: true },
|
||||
pagination: { first: 5000 },
|
||||
pagination: { first: 1000 },
|
||||
},
|
||||
},
|
||||
result: {
|
||||
|
||||
@@ -42,8 +42,12 @@ fragment FillEdge on TradeEdge {
|
||||
cursor
|
||||
}
|
||||
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills(
|
||||
$filter: TradesFilter
|
||||
$pagination: Pagination
|
||||
$dateRange: DateRange
|
||||
) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node:
|
||||
export type FillsQueryVariables = Types.Exact<{
|
||||
filter?: Types.InputMaybe<Types.TradesFilter>;
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -95,8 +96,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
|
||||
}
|
||||
${TradeFeeFieldsFragmentDoc}`;
|
||||
export const FillsDocument = gql`
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
@@ -124,6 +125,7 @@ export const FillsDocument = gql`
|
||||
* variables: {
|
||||
* filter: // value for 'filter'
|
||||
* pagination: // value for 'pagination'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -22,26 +23,60 @@ export const FillsManager = ({
|
||||
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
|
||||
partyIds: [partyId],
|
||||
};
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
gridRef.current?.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
variables: { filter },
|
||||
});
|
||||
|
||||
return (
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No fills matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
negativeClassNames,
|
||||
MarketNameCell,
|
||||
COL_DEFS,
|
||||
DateRangeFilter,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
@@ -120,6 +121,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
},
|
||||
{
|
||||
headerName: t('Date'),
|
||||
filter: DateRangeFilter,
|
||||
field: 'createdAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 5000,
|
||||
first: 1000,
|
||||
},
|
||||
resetDelay: 1000,
|
||||
additionalContext: { isEnlargedTimeout: true },
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
render(generateJsx());
|
||||
|
||||
@@ -45,6 +46,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
@@ -62,6 +64,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -12,7 +11,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -48,33 +47,11 @@ export const OrderListManager = ({
|
||||
? { partyId, filter: { liveOnly: true } }
|
||||
: { partyId };
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, pageInfo, load } = useDataProvider({
|
||||
dataProvider: ordersWithMarketProvider,
|
||||
variables,
|
||||
update: ({ data }) => {
|
||||
if (data && gridRef.current?.api) {
|
||||
gridRef.current.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
@@ -96,6 +73,17 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
@@ -103,20 +91,60 @@ export const OrderListManager = ({
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
if (onFilterChanged) {
|
||||
onFilterChanged(event);
|
||||
}
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{variables.filter?.liveOnly
|
||||
? null
|
||||
: t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button
|
||||
size="extra-small"
|
||||
className="ml-1"
|
||||
onClick={() => load()}
|
||||
>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No orders matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('StopOrdersManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
|
||||
@@ -4,10 +4,13 @@ import { TradesTable } from './trades-table';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
gridProps: ReturnType<typeof useDataGridEvents>;
|
||||
}
|
||||
|
||||
export const TradesManager = ({
|
||||
@@ -16,19 +19,60 @@ export const TradesManager = ({
|
||||
}: TradesContainerProps) => {
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: tradesWithMarketProvider,
|
||||
variables: { marketId },
|
||||
});
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No trades matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,3 +179,33 @@ export const toNumberParts = (
|
||||
export const isNumeric = (
|
||||
value?: string | number | BigNumber | bigint | null
|
||||
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
|
||||
|
||||
/**
|
||||
* Format a number greater than 1 million with m for million, b for billion
|
||||
* and t for trillion
|
||||
*/
|
||||
export const formatNumberRounded = (num: BigNumber) => {
|
||||
let value = '';
|
||||
|
||||
const format = (divisor: string) => {
|
||||
const result = num.dividedBy(divisor);
|
||||
return result.isInteger() ? result.toString() : result.toFixed(1);
|
||||
};
|
||||
|
||||
if (num.isGreaterThan(new BigNumber('1e14'))) {
|
||||
value = '>100t';
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
|
||||
// Trillion
|
||||
value = `${format('1e12')}t`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
|
||||
// Billion
|
||||
value = `${format('1e9')}b`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
|
||||
// Million
|
||||
value = `${format('1e6')}m`;
|
||||
} else {
|
||||
value = formatNumber(num);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,2 +1,4 @@
|
||||
[functions]
|
||||
included_files = ["!node_modules/@sentry/cli/sentry-cli"]
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
Reference in New Issue
Block a user