Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
097bf7c08c | ||
|
|
236e35a92b | ||
|
|
abf84b9d45 | ||
|
|
6d2a2ea0a0 | ||
|
|
6ac79e9f6d | ||
|
|
835ee64243 | ||
|
|
e7c3b5054c | ||
|
|
9dc9588a14 | ||
|
|
a0844d41bf | ||
|
|
3d1aa74128 |
+1
-1
@@ -1,2 +1,2 @@
|
||||
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
|
||||
* @vegaprotocol/frontend
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { selectAsset } from '../support/helpers';
|
||||
|
||||
const amountField = 'input[name="amount"]';
|
||||
const transferText = 'transfer-intro-text';
|
||||
const errorText = 'input-error-text';
|
||||
const formFieldError = 'input-error-text';
|
||||
const keyID = `[data-testid="${transferText}"] > .rounded-md`;
|
||||
const manageVegaWallet = 'manage-vega-wallet';
|
||||
const submitTransferBtn = '[type="submit"]';
|
||||
const toAddressField = '[name="toAddress"]';
|
||||
const transferForm = 'transfer-form';
|
||||
const walletTransfer = 'wallet-transfer';
|
||||
|
||||
const ASSET_EURO = 1;
|
||||
const ASSET_SEPOLIA_TBTC = 2;
|
||||
|
||||
describe(
|
||||
'transfer form validation',
|
||||
{ tags: '@regression', testIsolation: true },
|
||||
() => {
|
||||
beforeEach(() => {
|
||||
cy.mockWeb3Provider();
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.setVegaWallet();
|
||||
|
||||
cy.visit('/#/portfolio');
|
||||
cy.getByTestId(manageVegaWallet).click();
|
||||
cy.getByTestId(walletTransfer).click();
|
||||
|
||||
cy.wait('@Accounts');
|
||||
cy.wait('@Assets');
|
||||
|
||||
cy.mockVegaWalletTransaction();
|
||||
});
|
||||
|
||||
it('transfer Text', () => {
|
||||
// 1003-TRAN-003
|
||||
cy.getByTestId(transferText)
|
||||
.should('exist')
|
||||
.get(keyID)
|
||||
.invoke('text')
|
||||
.should('match', /[\w.]{6}…[\w.]{6}/);
|
||||
});
|
||||
|
||||
it('invalid vega key validation', () => {
|
||||
//1003-TRAN-013
|
||||
//1003-TRAN-004
|
||||
cy.getByTestId(transferForm).should('be.visible');
|
||||
cy.contains('Enter manually').click();
|
||||
cy.getByTestId(transferForm).find(toAddressField).type('asd');
|
||||
cy.getByTestId(transferForm).find(submitTransferBtn).click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Invalid Vega key');
|
||||
cy.contains('label', 'Vega key').should('be.visible');
|
||||
cy.contains('label', 'Asset').should('be.visible');
|
||||
cy.contains('label', 'Amount').should('be.visible');
|
||||
});
|
||||
|
||||
it('empty fields', () => {
|
||||
// 1003-TRAN-012
|
||||
cy.getByTestId(transferForm).find(submitTransferBtn).click();
|
||||
cy.getByTestId(formFieldError).should('contain.text', 'Required');
|
||||
cy.getByTestId(formFieldError).should('have.length', 3);
|
||||
});
|
||||
|
||||
it('min amount', () => {
|
||||
// 1002-WITH-010
|
||||
// 1003-TRAN-014
|
||||
selectAsset(ASSET_SEPOLIA_TBTC);
|
||||
cy.get(amountField).clear().type('0');
|
||||
cy.getByTestId(transferForm).find(submitTransferBtn).click();
|
||||
cy.getByTestId(errorText).should(
|
||||
'contain.text',
|
||||
'Value is below minimum'
|
||||
);
|
||||
});
|
||||
|
||||
it('max amount', () => {
|
||||
// 1003-TRAN-002
|
||||
// 1003-TRAN-011
|
||||
// 1003-TRAN-002
|
||||
selectAsset(ASSET_EURO); // Will be above maximum because the vega wallet doesn't have any collateral
|
||||
cy.get(amountField).clear().type('1001', { delay: 100 });
|
||||
cy.getByTestId(transferForm).find(submitTransferBtn).click();
|
||||
cy.getByTestId(errorText).should(
|
||||
'contain.text',
|
||||
'You cannot transfer more than your available collateral'
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -21,6 +21,7 @@ const market = {
|
||||
} as unknown as Market;
|
||||
|
||||
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||
let mockDataMarketState: Market['state'] | null = null;
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||
@@ -43,6 +44,12 @@ jest.mock('@vegaprotocol/utils', () => ({
|
||||
let mockCandles = {};
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketState: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataMarketState,
|
||||
}
|
||||
: { data: undefined },
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
@@ -81,30 +88,6 @@ describe('MarketSuccessorBanner', () => {
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('successor market not in continuous mode', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
tradingMode: Types.MarketTradingMode.TRADING_MODE_NO_TRADING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('successor market is not active', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
state: Types.MarketState.STATE_PENDING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be displayed', () => {
|
||||
@@ -120,6 +103,17 @@ describe('MarketSuccessorBanner', () => {
|
||||
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||
});
|
||||
|
||||
it('no successor market data, market settled', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally successor volume', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
@@ -137,7 +131,9 @@ describe('MarketSuccessorBanner', () => {
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('has a 24h trading volume of 101.367')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally duration', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useMarketState,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
@@ -29,7 +30,9 @@ export const MarketSuccessorBanner = ({
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const { data: successorData } = useSuccessorMarket(market?.id);
|
||||
const { data: marketState } = useMarketState(market?.id);
|
||||
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
@@ -45,11 +48,6 @@ export const MarketSuccessorBanner = ({
|
||||
? intervalToDuration({ start: new Date(), end: expiry })
|
||||
: null;
|
||||
|
||||
const isInContinuesMode =
|
||||
successorData?.state === Types.MarketState.STATE_ACTIVE &&
|
||||
successorData?.tradingMode ===
|
||||
Types.MarketTradingMode.TRADING_MODE_CONTINUOUS;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorData?.id,
|
||||
});
|
||||
@@ -66,7 +64,7 @@ export const MarketSuccessorBanner = ({
|
||||
)
|
||||
: null;
|
||||
|
||||
if (isInContinuesMode && visible) {
|
||||
if (!loading && (isSettled || successorData) && visible) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
@@ -74,34 +72,47 @@ export const MarketSuccessorBanner = ({
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{t('This market has been succeeded')}
|
||||
</div>
|
||||
<div>
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}{' '}
|
||||
{t('The successor market')}{' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span> {t('has %s 24h vol.', [successorVolume])}</span>
|
||||
)}
|
||||
<div className="uppercase">
|
||||
{successorData
|
||||
? t('This market has been succeeded')
|
||||
: t('This market has been settled')}
|
||||
</div>
|
||||
{(duration || successorData) && (
|
||||
<div className="mt-1">
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}
|
||||
{successorData && (
|
||||
<>
|
||||
{' '}
|
||||
{t('The successor market')}
|
||||
{!successorVolume ? ' is ' : ' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span>
|
||||
{' '}
|
||||
{t('has a 24h trading volume of %s', [successorVolume])}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ const MarketData = ({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const marketData = data?.marketsData[0];
|
||||
@@ -70,6 +71,8 @@ const MarketData = ({
|
||||
|
||||
const marketTradingMode = marketData
|
||||
? marketData.marketTradingMode
|
||||
: market.data
|
||||
? market.data.marketTradingMode
|
||||
: market.tradingMode;
|
||||
|
||||
const mode = [
|
||||
@@ -95,7 +98,7 @@ const MarketData = ({
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="flex items-baseline">
|
||||
<span className="text-sm lg:text-base text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</span>
|
||||
{allProducts && productType && (
|
||||
|
||||
@@ -2,7 +2,10 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -36,6 +39,10 @@ describe('MarketSelector', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
@@ -56,7 +63,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
@@ -77,7 +87,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -91,7 +104,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
@@ -112,6 +128,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'cd',
|
||||
@@ -132,7 +152,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const activeMarkets = markets.filter((m) => isMarketActive(m.state));
|
||||
const activeMarkets = markets.filter((m) =>
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useMarketSelectorList } from './use-market-selector-list';
|
||||
import { isMarketActive } from '../../lib/utils';
|
||||
import { Product } from './product-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import type { Filter } from './market-selector';
|
||||
@@ -31,22 +34,40 @@ describe('useMarketSelectorList', () => {
|
||||
|
||||
it('returns all markets active and suspended markets', () => {
|
||||
const markets = [
|
||||
createMarketFragment({ id: 'market-0' }),
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
state: MarketState.STATE_PENDING,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_PENDING,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
@@ -56,7 +77,8 @@ describe('useMarketSelectorList', () => {
|
||||
});
|
||||
const { result } = setup();
|
||||
const expectedFilteredMarkets = markets.filter((m) =>
|
||||
isMarketActive(m.state)
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
data: markets,
|
||||
@@ -70,6 +92,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -90,6 +116,10 @@ describe('useMarketSelectorList', () => {
|
||||
// }),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -135,6 +165,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -148,6 +182,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -161,6 +199,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -174,6 +216,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -238,6 +284,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'abc',
|
||||
@@ -247,6 +297,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'def',
|
||||
@@ -256,6 +310,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'defg',
|
||||
@@ -265,6 +323,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'ggg',
|
||||
@@ -333,11 +395,11 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -349,9 +411,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -363,9 +426,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -377,9 +441,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -414,6 +479,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -426,6 +495,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -438,6 +511,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -482,18 +559,30 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 3).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 2).toISOString(),
|
||||
},
|
||||
|
||||
@@ -22,8 +22,12 @@ export const useMarketSelectorList = ({
|
||||
const markets = useMemo(() => {
|
||||
if (!data?.length) return [];
|
||||
const markets = data
|
||||
// only active
|
||||
.filter((m) => isMarketActive(m.state))
|
||||
// show only active markets, using m.data.marketState as this will be
|
||||
// data that will get refreshed when calling reload
|
||||
.filter((m) => {
|
||||
if (!m.data) return false;
|
||||
return isMarketActive(m.data.marketState);
|
||||
})
|
||||
// only selected product type
|
||||
.filter((m) => {
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TelemetryApproval } from './telemetry-approval';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useOnboardingStore } from '../welcome-dialog/use-get-onboarding-step';
|
||||
|
||||
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_tost_id';
|
||||
const TELEMETRY_APPROVAL_TOAST_ID = 'telemetry_toast_id';
|
||||
|
||||
export const Telemetry = () => {
|
||||
const onboardingDissmissed = useOnboardingStore((store) => store.dismissed);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const SSRLoader = () => {
|
||||
const randomDelay = () => {
|
||||
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
|
||||
};
|
||||
import { pseudoRandom } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export const SSRLoader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -38,10 +38,9 @@ export const SSRLoader = () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
animation: 'flickering 0.4s linear alternate infinite',
|
||||
animationDelay: `-${randomDelay()}s`,
|
||||
animationDelay: `-${generate()}s`,
|
||||
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
|
||||
background: 'black',
|
||||
opacity: Math.random() > 0.5 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useEthTransactionUpdater,
|
||||
useEthWithdrawApprovalsManager,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { useLedgerDownloadManager } from '@vegaprotocol/ledger';
|
||||
|
||||
export const TransactionHandlers = () => {
|
||||
useVegaTransactionManager();
|
||||
@@ -14,5 +15,6 @@ export const TransactionHandlers = () => {
|
||||
useEthTransactionManager();
|
||||
useEthTransactionUpdater();
|
||||
useEthWithdrawApprovalsManager();
|
||||
useLedgerDownloadManager();
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -88,6 +88,9 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
it('validates a manually entered address', async () => {
|
||||
// 1003-TRAN-012
|
||||
// 1003-TRAN-013
|
||||
// 1003-TRAN-004
|
||||
render(<TransferForm {...props} />);
|
||||
submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3);
|
||||
@@ -116,6 +119,11 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
// 1003-TRAN-002
|
||||
// 1003-TRAN-003
|
||||
// 1002-WITH-010
|
||||
// 1003-TRAN-011
|
||||
// 1003-TRAN-014
|
||||
render(<TransferForm {...props} />);
|
||||
|
||||
// check current pubkey not shown
|
||||
|
||||
@@ -57,7 +57,16 @@ export const rows: Rows = [
|
||||
key: AssetDetail.ID,
|
||||
label: t('ID'),
|
||||
tooltip: '',
|
||||
value: (asset) => truncateMiddle(asset.id),
|
||||
value: (asset) => (
|
||||
<>
|
||||
{truncateMiddle(asset.id)}{' '}
|
||||
<CopyWithTooltip text={asset.id}>
|
||||
<button title={t('Copy id to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.TYPE,
|
||||
|
||||
@@ -14,16 +14,17 @@ export const MarketProductPill = ({
|
||||
}: {
|
||||
productType?: ProductType;
|
||||
}) => {
|
||||
if (!productType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
productType && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
)
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
Intent,
|
||||
ExternalLink,
|
||||
Pill,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
@@ -385,7 +386,21 @@ export const DealTicketMarginDetails = ({
|
||||
value={liquidationPriceEstimateRange}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={quoteName}
|
||||
labelDescription={LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>{LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}</span>{' '}
|
||||
<span>
|
||||
{t('For full details please see ')}
|
||||
<ExternalLink
|
||||
href={
|
||||
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
|
||||
}
|
||||
>
|
||||
{t('liquidation price estimate documentation.')}
|
||||
</ExternalLink>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{partyId && (
|
||||
<AccountBreakdownDialog
|
||||
|
||||
@@ -60,7 +60,7 @@ export const EST_FEES_TOOLTIP_TEXT = t(
|
||||
);
|
||||
|
||||
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
|
||||
'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.'
|
||||
'This is an approximation 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(
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './lib/ledger-export-form';
|
||||
export * from './lib/ledger-download-store';
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { create } from 'zustand';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
|
||||
type DownloadSettings = {
|
||||
title: string;
|
||||
link: string;
|
||||
filename?: string;
|
||||
isDownloaded?: boolean;
|
||||
isChanged?: boolean;
|
||||
isError?: boolean;
|
||||
errorMessage?: string;
|
||||
isDelayed?: boolean;
|
||||
intent?: Intent;
|
||||
blob?: Blob;
|
||||
};
|
||||
|
||||
export type LedgerDownloadFileStore = {
|
||||
queue: DownloadSettings[];
|
||||
hasItem: (link: string) => boolean;
|
||||
removeItem: (link: string) => void;
|
||||
updateQueue: (item: DownloadSettings) => void;
|
||||
};
|
||||
|
||||
export const useLedgerDownloadFile = create<LedgerDownloadFileStore>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
queue: [],
|
||||
hasItem: (link: string) =>
|
||||
get().queue.findIndex((item) => item.link === link) > -1,
|
||||
removeItem: (link: string) => {
|
||||
const queue = get().queue;
|
||||
const index = queue.findIndex((item) => item.link === link);
|
||||
if (index > -1) {
|
||||
queue.splice(index, 1);
|
||||
set({ queue: [...queue] });
|
||||
}
|
||||
},
|
||||
updateQueue: (newitem: DownloadSettings) => {
|
||||
const queue = get().queue;
|
||||
const index = queue.findIndex((item) => item.link === newitem.link);
|
||||
if (index > -1) {
|
||||
queue[index] = { ...queue[index], ...newitem };
|
||||
set({ queue: [...queue] });
|
||||
} else {
|
||||
set({ queue: [newitem, ...queue] });
|
||||
}
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
const ErrorContent = ({ message }: { message?: string }) => (
|
||||
<>
|
||||
<h4 className="mb-1 text-sm">{t('Something went wrong')}</h4>
|
||||
<p>{message || t('Try again later')}</p>
|
||||
</>
|
||||
);
|
||||
|
||||
const InfoContent = ({ progress = false }) => (
|
||||
<>
|
||||
<p>{t('Please note this can take several minutes.')}</p>
|
||||
<p>{t('You will be notified here when your file is ready.')}</p>
|
||||
<h4 className="my-2">
|
||||
{progress ? t('Still in progress') : t('Download has been started')}
|
||||
</h4>
|
||||
</>
|
||||
);
|
||||
|
||||
export const useLedgerDownloadManager = () => {
|
||||
const queue = useLedgerDownloadFile((store) => store.queue);
|
||||
const updateQueue = useLedgerDownloadFile((store) => store.updateQueue);
|
||||
const removeItem = useLedgerDownloadFile((store) => store.removeItem);
|
||||
const [setToast, updateToast, hasToast, removeToast] = useToasts((store) => [
|
||||
store.setToast,
|
||||
store.update,
|
||||
store.hasToast,
|
||||
store.remove,
|
||||
]);
|
||||
|
||||
const onDownloadClose = useCallback(
|
||||
(id: string) => {
|
||||
removeToast(id);
|
||||
removeItem(id);
|
||||
},
|
||||
[removeToast, removeItem]
|
||||
);
|
||||
|
||||
const createToast = (item: DownloadSettings) => {
|
||||
let content: ReactNode;
|
||||
switch (true) {
|
||||
case item.isError:
|
||||
content = <ErrorContent message={item.errorMessage} />;
|
||||
break;
|
||||
case Boolean(item.blob):
|
||||
content = (
|
||||
<>
|
||||
<h4 className="mb-1 text-sm">{t('Your file is ready')}</h4>
|
||||
<a
|
||||
onClick={() => onDownloadClose(item.link)}
|
||||
href={URL.createObjectURL(item.blob as Blob)}
|
||||
download={item.filename}
|
||||
className="underline"
|
||||
>
|
||||
{t('Get file here')}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
content = <InfoContent progress={item.isDelayed} />;
|
||||
}
|
||||
const toast: Toast = {
|
||||
id: item.link,
|
||||
intent: item.intent || Intent.Primary,
|
||||
content: (
|
||||
<>
|
||||
<h3 className="mb-1 text-md uppercase">{item.title}</h3>
|
||||
{content}
|
||||
</>
|
||||
),
|
||||
onClose: () => onDownloadClose(item.link),
|
||||
loader: !item.isDownloaded && !item.isError,
|
||||
};
|
||||
if (hasToast(toast.id)) {
|
||||
updateToast(toast.id, toast);
|
||||
} else {
|
||||
setToast(toast);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queue.forEach((item) => {
|
||||
if (item.isChanged) {
|
||||
createToast(item);
|
||||
updateQueue({ ...item, isChanged: false });
|
||||
}
|
||||
});
|
||||
}, [queue, createToast, updateQueue]);
|
||||
};
|
||||
@@ -1,10 +1,21 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { createDownloadUrl, LedgerExportForm } from './ledger-export-form';
|
||||
import { formatForInput, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useLedgerDownloadManager,
|
||||
useLedgerDownloadFile,
|
||||
} from './ledger-download-store';
|
||||
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const mockSetToast = jest.fn();
|
||||
jest.mock('@vegaprotocol/ui-toolkit', () => ({
|
||||
...jest.requireActual('@vegaprotocol/ui-toolkit'),
|
||||
useToasts: jest.fn(() => [mockSetToast, jest.fn(), jest.fn(() => false)]),
|
||||
}));
|
||||
const vegaUrl = 'https://vega-url.co.uk/querystuff';
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
headers: { get: jest.fn() },
|
||||
blob: () => '',
|
||||
};
|
||||
@@ -28,6 +39,7 @@ describe('LedgerExportForm', () => {
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be properly rendered', async () => {
|
||||
@@ -43,8 +55,6 @@ describe('LedgerExportForm', () => {
|
||||
// userEvent does not work with faked timers
|
||||
fireEvent.click(screen.getByTestId('ledger-download-button'));
|
||||
|
||||
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
|
||||
@@ -52,9 +62,6 @@ describe('LedgerExportForm', () => {
|
||||
}&dateRange.startTimestamp=1691057410000000000`
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('assetID should be properly change request url', async () => {
|
||||
@@ -75,8 +82,6 @@ describe('LedgerExportForm', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('ledger-download-button'));
|
||||
|
||||
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
|
||||
@@ -84,9 +89,6 @@ describe('LedgerExportForm', () => {
|
||||
}&dateRange.startTimestamp=1691057410000000000`
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('date-from should properly change request url', async () => {
|
||||
@@ -110,8 +112,6 @@ describe('LedgerExportForm', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('ledger-download-button'));
|
||||
|
||||
expect(screen.getByTestId('download-spinner')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
`https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
|
||||
@@ -119,10 +119,6 @@ describe('LedgerExportForm', () => {
|
||||
}&dateRange.startTimestamp=${toNanoSeconds(newDate)}`
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('date-to should properly change request url', async () => {
|
||||
@@ -156,10 +152,6 @@ describe('LedgerExportForm', () => {
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('download-spinner')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Time zone sentence should be properly displayed', () => {
|
||||
@@ -205,6 +197,50 @@ describe('LedgerExportForm', () => {
|
||||
screen.queryByText(/^The downloaded file uses the UTC/)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('A toast notification should be displayed', async () => {
|
||||
useLedgerDownloadFile.setState({ queue: [] });
|
||||
const TestWrapper = () => {
|
||||
useLedgerDownloadManager();
|
||||
return (
|
||||
<LedgerExportForm
|
||||
partyId={partyId}
|
||||
vegaUrl={vegaUrl}
|
||||
assets={assetsMock}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(<TestWrapper />);
|
||||
expect(screen.getByText('symbol asset-id')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('ledger-download-button'));
|
||||
|
||||
const link = `https://vega-url.co.uk/api/v2/ledgerentry/export?partyId=${partyId}&assetId=${
|
||||
Object.keys(assetsMock)[0]
|
||||
}&dateRange.startTimestamp=1691057410000000000`;
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetToast).toHaveBeenCalledWith({
|
||||
id: link,
|
||||
content: expect.any(Object),
|
||||
onClose: expect.any(Function),
|
||||
intent: Intent.Primary,
|
||||
loader: true,
|
||||
});
|
||||
|
||||
expect(global.fetch).toHaveBeenCalledWith(link);
|
||||
});
|
||||
|
||||
mockSetToast.mockClear();
|
||||
(global.fetch as jest.Mock).mockClear();
|
||||
fireEvent.click(screen.getByTestId('ledger-download-button')); // click again
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetToast).toHaveBeenCalled();
|
||||
expect(global.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDownloadUrl', () => {
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import {
|
||||
TradingButton,
|
||||
Intent,
|
||||
Loader,
|
||||
TradingButton,
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
TradingSelect,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { toNanoSeconds, VEGA_ID_REGEX } from '@vegaprotocol/utils';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
formatForInput,
|
||||
toNanoSeconds,
|
||||
VEGA_ID_REGEX,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import { subDays } from 'date-fns';
|
||||
import { useLedgerDownloadFile } from './ledger-download-store';
|
||||
|
||||
const DEFAULT_EXPORT_FILE_NAME = 'ledger_entries.csv';
|
||||
|
||||
@@ -73,10 +78,14 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
const maxFromDate = formatForInput(new Date(dateTo || now.current));
|
||||
const maxToDate = formatForInput(now.current);
|
||||
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
|
||||
const protohost = getProtoHost(vegaUrl);
|
||||
const disabled = Boolean(!assetId || isDownloading);
|
||||
const disabled = Boolean(!assetId);
|
||||
|
||||
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
|
||||
const updateDownloadQueue = useLedgerDownloadFile(
|
||||
(store) => store.updateQueue
|
||||
);
|
||||
|
||||
const assetDropDown = (
|
||||
<TradingSelect
|
||||
@@ -87,7 +96,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
}}
|
||||
className="w-full"
|
||||
data-testid="select-ledger-asset"
|
||||
disabled={isDownloading}
|
||||
>
|
||||
{Object.keys(assets).map((assetKey) => (
|
||||
<option key={assetKey} value={assetKey}>
|
||||
@@ -97,32 +105,78 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
</TradingSelect>
|
||||
);
|
||||
|
||||
const link = createDownloadUrl({
|
||||
protohost,
|
||||
partyId,
|
||||
assetId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
});
|
||||
|
||||
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
const link = createDownloadUrl({
|
||||
protohost,
|
||||
partyId,
|
||||
assetId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
|
||||
const title = t('Downloading for %s from %s till %s', [
|
||||
assets[assetId],
|
||||
format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
|
||||
format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
|
||||
]);
|
||||
|
||||
const downloadStoreItem = {
|
||||
title,
|
||||
link,
|
||||
isChanged: true,
|
||||
};
|
||||
if (hasItem(link)) {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
return;
|
||||
}
|
||||
const ts = setTimeout(() => {
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
intent: Intent.Warning,
|
||||
isDelayed: true,
|
||||
isChanged: true,
|
||||
});
|
||||
setIsDownloading(true);
|
||||
}, 1000 * 30);
|
||||
|
||||
try {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
const resp = await fetch(link);
|
||||
if (!resp?.ok) {
|
||||
if (resp?.status === 429) {
|
||||
throw new Error('Too many requests. Try again later.');
|
||||
}
|
||||
throw new Error('Download of ledger entries failed');
|
||||
}
|
||||
const { headers } = resp;
|
||||
const nameHeader = headers.get('content-disposition');
|
||||
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
filename,
|
||||
});
|
||||
const blob = await resp.blob();
|
||||
if (blob) {
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
blob,
|
||||
isDownloaded: true,
|
||||
isChanged: true,
|
||||
intent: Intent.Success,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
intent: Intent.Danger,
|
||||
isError: true,
|
||||
isChanged: true,
|
||||
errorMessage: (err as Error).message || undefined,
|
||||
});
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
clearTimeout(ts);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -145,7 +199,6 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
id="date-from"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
disabled={disabled}
|
||||
max={maxFromDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
@@ -156,19 +209,10 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
id="date-to"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
disabled={disabled}
|
||||
max={maxToDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<div className="relative text-sm" title={t('Download all to .csv file')}>
|
||||
{isDownloading && (
|
||||
<div
|
||||
className="absolute flex items-center justify-center w-full h-full"
|
||||
data-testid="download-spinner"
|
||||
>
|
||||
<Loader size="small" />
|
||||
</div>
|
||||
)}
|
||||
<TradingButton
|
||||
fill
|
||||
disabled={disabled}
|
||||
|
||||
+3
-2
@@ -3,12 +3,12 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type MarketsDataQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
|
||||
export const MarketsDataFieldsFragmentDoc = gql`
|
||||
fragment MarketsDataFields on MarketData {
|
||||
@@ -20,6 +20,7 @@ export const MarketsDataFieldsFragmentDoc = gql`
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -126,6 +126,16 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||
);
|
||||
|
||||
export const marketStateProvider = makeDerivedDataProvider<
|
||||
MarketDataFieldsFragment['marketState'] | undefined,
|
||||
never,
|
||||
MarketDataQueryVariables
|
||||
>(
|
||||
[marketDataProvider],
|
||||
(parts, variables, prevData) =>
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketState
|
||||
);
|
||||
|
||||
export const fundingRateProvider = makeDerivedDataProvider<
|
||||
string,
|
||||
never,
|
||||
@@ -176,3 +186,11 @@ export const useMarketTradingMode = (marketId?: string, skip?: boolean) => {
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarketState = (marketId?: string, skip?: boolean) => {
|
||||
return useDataProvider({
|
||||
dataProvider: marketStateProvider,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ fragment MarketsDataFields on MarketData {
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -34,6 +34,7 @@ export const createMarketsDataFragment = (
|
||||
__typename: 'Market',
|
||||
},
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
staticMidPrice: '0',
|
||||
indicativePrice: '0',
|
||||
bestStaticBidPrice: '0',
|
||||
|
||||
@@ -2,8 +2,8 @@ import { render, act, screen } from '@testing-library/react';
|
||||
import { AsyncRenderer } from './async-renderer';
|
||||
|
||||
describe('AsyncRenderer', () => {
|
||||
const reload = jest.fn();
|
||||
it('timeout error should render button', async () => {
|
||||
const reload = jest.fn();
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
@@ -20,4 +20,22 @@ describe('AsyncRenderer', () => {
|
||||
});
|
||||
expect(reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors should be handled properly', async () => {
|
||||
const message = 'Node has been collapsed';
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
reload={reload}
|
||||
error={new Error(message)}
|
||||
loading={false}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(`Something went wrong: ${message}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AsyncRenderer<T = object>({
|
||||
reload,
|
||||
}: AsyncRendererProps<T>) {
|
||||
if (error) {
|
||||
if (!data) {
|
||||
if (!data || (Array.isArray(data) && !data.length)) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="h-12 flex flex-col items-center">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import styles from './loader.module.scss';
|
||||
|
||||
const pseudoRandom = (seed: number) => {
|
||||
export const pseudoRandom = (seed: number) => {
|
||||
let value = seed;
|
||||
return () => {
|
||||
value = (value * 16807) % 2147483647;
|
||||
@@ -10,6 +9,8 @@ const pseudoRandom = (seed: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export interface LoaderProps {
|
||||
size?: 'small' | 'large';
|
||||
forceTheme?: 'dark' | 'light';
|
||||
@@ -27,8 +28,6 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
|
||||
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
|
||||
const items = size === 'small' ? 9 : 25;
|
||||
|
||||
const generate = useMemo(() => pseudoRandom(1), []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center" data-testid="loader">
|
||||
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||
|
||||
@@ -41,7 +41,7 @@ const ethereumRequest = <T>(args: RequestArguments): Promise<T> => {
|
||||
|
||||
export const LOCAL_SNAP_ID = 'local:http://localhost:8080';
|
||||
export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap';
|
||||
export const DEFAULT_SNAP_VERSION = '0.2.0';
|
||||
export const DEFAULT_SNAP_VERSION = '0.3.1';
|
||||
|
||||
type GetSnapsResponse = Record<string, Snap>;
|
||||
|
||||
|
||||
@@ -8,5 +8,13 @@ When I enter on ledger entries tab in portfolio page
|
||||
- in the form **Must** see a dropdown for select an asset, in which reports will be downloaded (<a name="7007-LEEN-002" href="#7007-LEEN-002">7007-LEEN-002</a>)
|
||||
- in the form **Must** see inputs for select time period, in which reports will be downloaded (<a name="7007-LEEN-003" href="#7007-LEEN-003">7007-LEEN-003</a>)
|
||||
- default preselected period **Must** be the last 7 days (<a name="7007-LEEN-004" href="#7007-LEEN-004">7007-LEEN-004</a>)
|
||||
- during download a loader component **Must** be visible and all interactive elements in the form **Must** be disabled (<a name="7007-LEEN-005" href="#7007-LEEN-005">7007-LEEN-005</a>)
|
||||
- **Must** see a note about time in file are in UTC and timezone of the user relative to UTC (<a name="7007-LEEN-006" href="#7007-LEEN-006">7007-LEEN-006</a>)
|
||||
- As a user, I **must** see a message saying that this can take several minutes (<a name="7007-LEEN-007" href="#7007-LEEN-007">7007-LEEN-007</a>)
|
||||
- After half a minute, the message is updated to say something like 'Still in progress' (<a name="7007-LEEN-008" href="#7007-LEEN-008">7007-LEEN-008</a>)
|
||||
- A toast is shown when the download is complete (<a name="7007-LEEN-009" href="#7007-LEEN-009">7007-LEEN-009</a>)
|
||||
- The download button should never be disabled
|
||||
- If user tries to download file which is already in download: (<a name="7007-LEEN-010" href="#7007-LEEN-010">7007-LEEN-010</a>)
|
||||
- if notification stayed open, nothing happens
|
||||
- If notification was closed, will be open, no any new request will be fired
|
||||
- If something has changed in the form (asset, dates, `Date.now`) new download will start.
|
||||
- The state of the download form should be in sync with the download itself if you navigate away from the page or reload (<a name="7007-LEEN-011" href="#7007-LEEN-011">7007-LEEN-011</a>)
|
||||
|
||||
Reference in New Issue
Block a user