Compare commits

..
Author SHA1 Message Date
Matthew Russell 503e68cd61 chore: use random with seed to fix hydration warning 2023-10-17 09:55:49 -07:00
70 changed files with 1364 additions and 1443 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
* @vegaprotocol/frontend
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
*.graphql @vegaprotocol/core
+1 -1
View File
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
);
};
export const GHOST = (
const GHOST = (
<svg
width="56"
height="85"
@@ -1,108 +0,0 @@
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const RestrictedPage = () => {
const errorTitle = '451 Unavailable';
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="text-[2.7rem] font-alpha calt break-words uppercase">
{errorTitle}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default RestrictedPage;
@@ -1,6 +1,5 @@
export const Routes = {
HOME: '/',
RESTRICTED: '/restricted',
TX: 'txs',
BLOCKS: 'blocks',
PARTIES: 'parties',
@@ -30,7 +30,6 @@ import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer';
import { FLAGS } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
export type Navigable = {
path: string;
@@ -357,14 +356,6 @@ export const routerConfig: Route[] = [
...validators,
],
},
{
path: Routes.RESTRICTED,
element: <RestrictedPage />,
handle: {
name: t('Restricted'),
text: t('Restricted'),
},
},
];
export const router = createBrowserRouter(routerConfig);
-12
View File
@@ -106,7 +106,6 @@ const Web3Container = ({
useEthWithdrawApprovalsManager();
return null;
};
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
store.connectors,
store.initialize,
@@ -240,9 +239,6 @@ const AppContainer = () => {
store.setDialogOpen,
]);
// Hacky skip all the loading & web3 init for geo restricted users
const isRestricted = document?.location?.pathname?.includes('/restricted');
useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') {
Sentry.init({
@@ -308,14 +304,6 @@ const AppContainer = () => {
}
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
if (isRestricted) {
return (
<Router>
<AppRouter />
</Router>
);
}
return (
<Router>
<ScrollToTop />
@@ -9,7 +9,6 @@
"pageTitleRedemptionTranche": "Redeem from Tranche",
"pageTitleTranches": "Vesting tranches",
"pageTitle404": "Page not found",
"pageTitle451": "451 unavailable",
"pageTitleNotPermitted": "Can not proceed!",
"pageTitleDisassociate": "Disassociate $VEGA tokens from a Vega key",
"pageTitleProposals": "Proposals",
@@ -12,19 +12,18 @@ import {
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalHeader, NewTransferSummary } from './proposal-header';
import { ProposalHeader } from './proposal-header';
import {
lastWeek,
nextWeek,
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { FLAGS } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -32,7 +31,6 @@ jest.mock('@vegaprotocol/proposals', () => ({
code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID',
}),
useNewTransferProposalDetails: jest.fn(),
}));
const renderComponent = (
@@ -420,78 +418,3 @@ describe('Proposal header', () => {
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
});
});
jest.mock('@vegaprotocol/proposals');
describe('<NewTransferSummary />', () => {
it('renders null if no details are provided', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
const { container } = render(<NewTransferSummary proposalId="1" />);
expect(container.firstChild).toBeNull();
});
it('handles OneOffGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: { __typename: 'OneOffGovernanceTransfer', deliverOn: null },
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) => content.includes('One off transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('handles RecurringGovernanceTransfer', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'wallet',
destination: '0x456',
destinationType: 'contract',
});
const { getByText } = render(<NewTransferSummary proposalId="1" />);
const textMatch = (content: string) =>
content.includes('Recurring transfer');
expect(getByText(textMatch)).toBeInTheDocument();
});
it('should fallback to translated sourceType when source is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: undefined,
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: '0x456',
destinationType: 'ACCOUNT_TYPE_GENERAL',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('General account')).toBeInTheDocument();
});
it('should fallback to translated destinationType when destination is not set', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue({
kind: {
__typename: 'RecurringGovernanceTransfer',
startEpoch: 1,
endEpoch: 5,
},
source: '0x123',
sourceType: 'ACCOUNT_TYPE_GENERAL',
destination: undefined,
destinationType: 'ACCOUNT_TYPE_GLOBAL_INSURANCE',
});
render(<NewTransferSummary proposalId="1" />);
expect(screen.getByText('Global insurance account')).toBeInTheDocument();
});
});
@@ -237,11 +237,7 @@ export const ProposalHeader = ({
);
};
export const SuccessorCode = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
const { t } = useTranslation();
const successor = useSuccessorMarketProposalDetails(proposalId);
@@ -258,11 +254,7 @@ export const SuccessorCode = ({
) : null;
};
export const NewTransferSummary = ({
proposalId,
}: {
proposalId?: string | null;
}) => {
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
const { t } = useTranslation();
const details = useNewTransferProposalDetails(proposalId);
@@ -273,21 +265,14 @@ export const NewTransferSummary = ({
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
{t('transfer from')}{' '}
<Lozenge>
{details.source
? truncateMiddle(details.source)
: t(details.sourceType)}
{truncateMiddle(details.source) || t(details.sourceType)}
</Lozenge>{' '}
{t('to')}{' '}
<Lozenge>
{details.destination
? truncateMiddle(details.destination)
: t(details.destinationType)}
</Lozenge>
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
</span>
);
};
export const CancelTransferSummary = ({
const CancelTransferSummary = ({
proposalId,
}: {
proposalId?: string | null;
@@ -1,112 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
import { BackgroundVideo } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
const Restricted = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const errorMessage =
'Due to uncertainty about the legal and regulatory status of the content hosted on this site, it is not available to visitors in your jurisdiction.';
return (
<div>
<BackgroundVideo className="brightness-50" />
<div
className={classNames(
'max-w-[620px] p-2 mt-[10vh]',
'mx-auto my-0',
'antialiased text-white',
'overflow-hidden relative',
'flex flex-col gap-2'
)}
>
<div className="flex gap-4">
<div>{GHOST}</div>
<h1 className="leading-relaxed mb-0 text-[2.7rem] font-alpha calt break-words uppercase">
{t('pageTitle451')}
</h1>
</div>
<div className="text-sm mt-10 overflow-auto font-mono">
{errorMessage}
</div>
</div>
</div>
);
};
const GHOST = (
<svg
width="56"
height="85"
viewBox="0 0 150 234"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.43 93.6821L0 104.097V172.435H5.13793V106.2L14.0779 97.3247L10.43 93.6821Z"
fill="black"
/>
<path d="M105.328 18.521H7.70703V172.435H105.328V18.521Z" fill="white" />
<path d="M38.5364 64.6953H33.3984V69.8258H38.5364V64.6953Z" fill="black" />
<path d="M43.6731 69.8257H38.5352V74.9561H43.6731V69.8257Z" fill="black" />
<path d="M48.8098 74.9561H43.6719V80.0865H48.8098V74.9561Z" fill="black" />
<path d="M38.5364 74.9561H33.3984V80.0865H38.5364V74.9561Z" fill="black" />
<path d="M48.8098 64.6953H43.6719V69.8258H48.8098V64.6953Z" fill="black" />
<path d="M69.3606 64.6953H64.2227V69.8258H69.3606V64.6953Z" fill="black" />
<path d="M74.5012 69.8257H69.3633V74.9561H74.5012V69.8257Z" fill="black" />
<path d="M79.6379 74.9561H74.5V80.0865H79.6379V74.9561Z" fill="black" />
<path d="M69.3606 74.9561H64.2227V80.0865H69.3606V74.9561Z" fill="black" />
<path d="M79.6379 64.6953H74.5V69.8258H79.6379V64.6953Z" fill="black" />
<path d="M79.6398 90.3477H33.3984V95.4781H79.6398V90.3477Z" fill="black" />
<path d="M48.8098 172.435H43.6719V234H48.8098V172.435Z" fill="black" />
<path d="M69.3606 172.435H64.2227V234H69.3606V172.435Z" fill="black" />
<path
d="M129.87 0.00408026L92.0273 14.3682L111.195 64.7203L149.038 50.3562L129.87 0.00408026Z"
fill="#FF0081"
/>
<path
d="M108.565 21.7836L103.762 23.6064L105.587 28.4024L110.39 26.5795L108.565 21.7836Z"
fill="white"
/>
<path
d="M115.189 24.7596L110.387 26.583L112.213 31.3785L117.015 29.5551L115.189 24.7596Z"
fill="white"
/>
<path
d="M121.826 27.7348L117.023 29.5576L118.849 34.3536L123.652 32.5307L121.826 27.7348Z"
fill="white"
/>
<path
d="M128.447 30.7059L123.645 32.5293L125.471 37.3247L130.273 35.5014L128.447 30.7059Z"
fill="white"
/>
<path
d="M135.084 33.6854L130.281 35.5083L132.107 40.3043L136.91 38.4814L135.084 33.6854Z"
fill="white"
/>
<path
d="M124.799 21.1156L119.996 22.939L121.822 27.7344L126.625 25.911L124.799 21.1156Z"
fill="white"
/>
<path
d="M127.775 14.492L122.973 16.3154L124.799 21.1109L129.601 19.2875L127.775 14.492Z"
fill="white"
/>
<path
d="M118.838 34.3499L114.035 36.1733L115.861 40.9688L120.664 39.1454L118.838 34.3499Z"
fill="white"
/>
<path
d="M115.857 40.9691L111.055 42.7925L112.881 47.5879L117.683 45.7645L115.857 40.9691Z"
fill="white"
/>
<path
d="M129.938 52.3822L139.906 91.5276L100.703 101.429L101.988 106.406L146.174 95.2215L134.922 51.0996L129.938 52.3822Z"
fill="black"
/>
</svg>
);
export default Restricted;
+1 -7
View File
@@ -4,7 +4,6 @@ import Home from './token';
import NotFound from './not-found';
import NotPermitted from './not-permitted';
import Routes from './routes';
import Restricted from './restricted';
const LazyTranches = React.lazy(
() =>
@@ -361,14 +360,9 @@ const routerConfig = [
path: Routes.DISCLAIMER,
element: <LazyDisclaimer name="Disclaimer" />,
},
{
path: Routes.RESTRICTED,
// Not lazy as loaded when a user first hits the site
element: <Restricted name="451 Unavailable" />,
},
{
path: '*',
// Also not lazy as loaded when a user first hits the site
// Not lazy as loaded when a user first hits the site
element: <NotFound name="NotFound" />,
},
...redirects,
-5
View File
@@ -7,7 +7,6 @@ const Routes = {
PROPOSALS_REJECTED: '/proposals/rejected',
PROTOCOL_UPGRADES: '/protocol-upgrades',
NOT_PERMITTED: '/not-permitted',
RESTRICTED: '/restricted',
NOT_FOUND: '/not-found',
CONTRACTS: '/contracts',
TOKEN: '/token',
@@ -34,10 +33,6 @@ export const TOP_LEVEL_ROUTES = [
name: 'Rewards',
path: Routes.REWARDS,
},
{
name: 'Restricted',
path: Routes.RESTRICTED,
},
];
export const TOKEN_DROPDOWN_ROUTES = [
@@ -0,0 +1,529 @@
import * as Schema from '@vegaprotocol/types';
import type { OrderAmendment, OrderCancellation } from '@vegaprotocol/wallet';
import {
updateOrder,
getSubscriptionMocks,
} from '../support/order-update-subscription';
import {
testOrderCancellation,
testOrderAmendment,
} from '../support/order-validation';
const orderSymbol = 'instrument-code';
const orderSize = 'size';
const orderType = 'type';
const orderStatus = 'status';
const orderRemaining = 'remaining';
const orderPrice = 'price';
const orderTimeInForce = 'timeInForce';
const orderUpdatedAt = 'updatedAt';
const cancelOrderBtn = 'cancel';
const cancelAllOrdersBtn = 'cancelAll';
const editOrderBtn = 'edit';
describe('orders list', { tags: '@smoke', testIsolation: true }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.wait('@Markets');
});
it('renders orders', () => {
cy.getByTestId('tab-orders').should('be.visible');
cy.getByTestId(cancelAllOrdersBtn).should('be.visible');
cy.getByTestId(cancelOrderBtn).should('have.length.at.least', 1);
cy.getByTestId(editOrderBtn).should('have.length.at.least', 1);
cy.getByTestId('tab-orders').within(() => {
cy.get(`[role='rowgroup']`)
.first()
.within(() => {
cy.get(`[col-id='${orderSymbol}']`).each(($symbol) => {
cy.wrap($symbol).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderRemaining}']`).each(($remaining) => {
cy.wrap($remaining).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderSize}']`).each(($size) => {
cy.wrap($size).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderType}']`).each(($type) => {
cy.wrap($type).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderStatus}']`).each(($status) => {
cy.wrap($status).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderPrice}']`).each(($price) => {
cy.wrap($price).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderTimeInForce}']`).each(($timeInForce) => {
cy.wrap($timeInForce).invoke('text').should('not.be.empty');
});
cy.get(`[col-id='${orderUpdatedAt}']`).each(($dateTime) => {
cy.wrap($dateTime).invoke('text').should('not.be.empty');
});
});
});
});
it('partially filled orders should not show close/edit buttons', () => {
const partiallyFilledId =
'94aead3ca92dc932efcb503631b03a410e2a5d4606cae6083e2406dc38e52f78';
cy.getByTestId('tab-orders').should('be.visible');
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Partially Filled').click();
cy.getByTestId('All').click();
cy.get(`[row-id="${partiallyFilledId}"]`)
.eq(0)
.within(() => {
cy.get(`[col-id='${orderStatus}']`).should(
'have.text',
'Partially Filled'
);
cy.get(`[col-id='${orderRemaining}']`).should('have.text', '7');
cy.get(`[col-id='${orderSize}']`).should('have.text', '-10');
cy.getByTestId(cancelOrderBtn).should('not.exist');
cy.getByTestId(editOrderBtn).should('not.exist');
});
});
it('orders are sorted by most recent order', () => {
// 7003-MORD-002
const expectedOrderList = [
'BTCUSD.MF21',
'SOLUSD',
'AAPL.MF21',
'BTCUSD.MF21',
'BTCUSD.MF21',
];
cy.get('.ag-header-container').within(() => {
cy.get('[col-id="status"]').realHover();
cy.get('[col-id="status"] .ag-icon-menu').click();
});
cy.contains('Reset').click();
cy.getByTestId('All').click();
cy.getByTestId('tab-orders')
.get(
`.ag-center-cols-container [col-id='${orderSymbol}'] [data-testid="market-code"]`
)
.should('have.length.at.least', expectedOrderList.length)
.then(($symbols) => {
const symbolNames: string[] = [];
cy.wrap($symbols)
.each(($symbol) => {
cy.wrap($symbol)
.invoke('text')
.then((text) => {
symbolNames.push(text);
});
})
.then(() => {
expect(symbolNames).to.include.ordered.members(expectedOrderList);
});
});
});
});
describe('subscribe orders', { tags: '@smoke' }, () => {
let orderId = '0';
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
orderId = (parseInt(orderId, 10) + 1).toString();
});
// 7002-SORD-053
// 7002-SORD-040
// 7003-MORD-001
it('must see an active order', () => {
// 7002-SORD-041
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Active');
});
it('must see an expired order', () => {
// 7002-SORD-042
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_EXPIRED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Expired');
});
it('must see a cancelled order', () => {
// 7002-SORD-043
// NOT COVERED: see the txn that cancelled it and a link to the block explorer, if cancelled by a user transaction.
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_CANCELLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Cancelled');
});
it('must see a stopped order', () => {
// 7002-SORD-044
// NOT COVERED: see an explanation of why stopped
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_STOPPED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Stopped');
});
it('must see a partially filled order', () => {
// 7002-SORD-045
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARTIALLY_FILLED,
size: '5',
remaining: '1',
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Partially Filled'
);
cy.getByTestId(`order-status-${orderId}`)
.parentsUntil(`.ag-row`)
.siblings(`[col-id=${orderRemaining}]`)
.should('have.text', '4');
});
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get(`[col-id="${orderSymbol}"]`).contains('[title="Future"]', 'Futr');
});
it('must see a rejected order', () => {
// 7002-SORD-047
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_REJECTED,
rejectionReason: Schema.OrderRejectionReason.ORDER_ERROR_INTERNAL_ERROR,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Rejected: Internal error'
);
});
it('must see a parked order', () => {
// 7002-SORD-048
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_PARKED,
});
cy.getByTestId(`order-status-${orderId}`).should(
'have.text',
'Parked: Internal error'
);
});
it('must see the size of the order and direction/side -', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '15',
side: Schema.Side.SIDE_SELL,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '-15');
});
it('must see the size of the order and direction/side +', () => {
// 7003-MORD-003
// 7003-MORD-004
updateOrder({
id: orderId,
size: '5',
side: Schema.Side.SIDE_BUY,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id="${orderSize}"]`)
.should('have.text', '+5');
});
it('for limit typy must see the Limit price that was set on the order', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
.should('have.text', '-');
});
it('must see the time in force applied to the order', () => {
// 7003-MORD-006
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_ACTIVE,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
});
cy.get(`[row-id=${orderId}]`)
.find(`[col-id='${orderTimeInForce}']`)
.should('have.text', 'GTC');
});
it('for Active order when is part of a liquidity or peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a liquidity, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
it('for Active order when is part of a peg shape, must not see an option to amend the individual order ', () => {
// 7003-MORD-008
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="cancel"]`)
.should('not.exist');
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="edit"]`)
.should('not.exist');
});
});
describe('amend and cancel order', { tags: '@smoke' }, () => {
beforeEach(() => {
const subscriptionMocks = getSubscriptionMocks();
cy.spy(subscriptionMocks, 'OrdersUpdate');
cy.mockTradingPage();
cy.mockSubscription(subscriptionMocks);
cy.setVegaWallet();
cy.visit('/#/markets/market-0');
cy.getByTestId('All').click();
cy.getByTestId('tab-orders').within(() => {
cy.get('[col-id="status"][role="columnheader"]')
.focus()
.find('.ag-header-cell-menu-button')
.click();
cy.get('.ag-filter-apply-panel-button').click();
});
cy.mockVegaWalletTransaction();
});
const orderId = '1234567890';
// this test is flakey
it('must be able to amend the price of an order', () => {
// 7003-MORD-007
// 7003-MORD-012
// 7003-MORD-014
// 7003-MORD-015
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click();
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('100');
cy.getByTestId('edit-order').find('[type="submit"]').click();
const order: OrderAmendment = {
orderId: orderId,
marketId: 'market-0',
price: '10000000',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
sizeDelta: 0,
};
testOrderAmendment(order);
});
});
it('must be able to cancel an individual order', () => {
// 7003-MORD-007
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find(`[data-testid="icon-cross"]`)
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {
orderId: orderId,
marketId: 'market-0',
};
testOrderCancellation(order);
});
});
it('must be able to cancel all orders on all markets', () => {
// 7003-MORD-009
// 7003-MORD-010
// 7003-MORD-011
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[data-testid="cancelAll"]`)
.should('have.text', 'Cancel all')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
const order: OrderCancellation = {};
testOrderCancellation(order);
});
});
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', () => {
// 7003-MORD-013
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: null,
liquidityProvisionId: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[data-testid="icon-edit"]')
.then(($btn) => {
cy.wrap($btn).click({ force: true });
cy.getByTestId('dialog-title').should('have.text', 'Edit order');
cy.get('#limitPrice').focus().clear().type('0.111111');
cy.getByTestId('edit-order').find('[type="submit"]').click();
cy.getByTestId('input-error-text').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
});
});
@@ -86,7 +86,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<MarketLiquiditySupplied
marketId={market.id}
assetDecimals={asset?.decimals || 0}
quantum={asset.quantum}
/>
{market.tradableInstrument.instrument.product.__typename === 'Future' && (
<HeaderStat
@@ -123,7 +123,7 @@ const MainGrid = memo(
<Tab
id="open-orders"
name={t('Open')}
menu={<TradingViews.activeOrders.menu />}
menu={<TradingViews.activeOrders.menu marketId={marketId} />}
>
<TradingViews.orders.component filter={Filter.Open} />
</Tab>
@@ -136,7 +136,7 @@ const MainGrid = memo(
<Tab
id="orders"
name={t('All')}
menu={<TradingViews.orders.menu />}
menu={<TradingViews.orders.menu marketId={marketId} />}
>
<TradingViews.orders.component />
</Tab>
@@ -43,7 +43,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
return (
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
<Menu marketId={market?.id || ''} />
</div>
);
}
@@ -0,0 +1,39 @@
query AccountHistory(
$partyId: ID!
$assetId: ID!
$accountTypes: [AccountType!]
$dateRange: DateRange
$marketIds: [ID!]
) {
balanceChanges(
filter: {
partyIds: [$partyId]
accountTypes: $accountTypes
assetId: $assetId
marketIds: $marketIds
}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: { partyIds: [$partyId] }, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
@@ -0,0 +1,117 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type AccountHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
assetId: Types.Scalars['ID'];
accountTypes?: Types.InputMaybe<Array<Types.AccountType> | Types.AccountType>;
dateRange?: Types.InputMaybe<Types.DateRange>;
marketIds?: Types.InputMaybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>;
}>;
export type AccountHistoryQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', timestamp: any, partyId?: string | null, balance: string, marketId?: string | null, assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export type AccountsWithBalanceQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
dateRange?: Types.InputMaybe<Types.DateRange>;
}>;
export type AccountsWithBalanceQuery = { __typename?: 'Query', balanceChanges: { __typename?: 'AggregatedBalanceConnection', edges: Array<{ __typename?: 'AggregatedBalanceEdge', node: { __typename?: 'AggregatedBalance', assetId?: string | null, accountType?: Types.AccountType | null } } | null> } };
export const AccountHistoryDocument = gql`
query AccountHistory($partyId: ID!, $assetId: ID!, $accountTypes: [AccountType!], $dateRange: DateRange, $marketIds: [ID!]) {
balanceChanges(
filter: {partyIds: [$partyId], accountTypes: $accountTypes, assetId: $assetId, marketIds: $marketIds}
dateRange: $dateRange
) {
edges {
node {
timestamp
partyId
balance
marketId
assetId
accountType
}
}
}
}
`;
/**
* __useAccountHistoryQuery__
*
* To run a query within a React component, call `useAccountHistoryQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountHistoryQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAccountHistoryQuery({
* variables: {
* partyId: // value for 'partyId'
* assetId: // value for 'assetId'
* accountTypes: // value for 'accountTypes'
* dateRange: // value for 'dateRange'
* marketIds: // value for 'marketIds'
* },
* });
*/
export function useAccountHistoryQuery(baseOptions: Apollo.QueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export function useAccountHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountHistoryQuery, AccountHistoryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountHistoryQuery, AccountHistoryQueryVariables>(AccountHistoryDocument, options);
}
export type AccountHistoryQueryHookResult = ReturnType<typeof useAccountHistoryQuery>;
export type AccountHistoryLazyQueryHookResult = ReturnType<typeof useAccountHistoryLazyQuery>;
export type AccountHistoryQueryResult = Apollo.QueryResult<AccountHistoryQuery, AccountHistoryQueryVariables>;
export const AccountsWithBalanceDocument = gql`
query AccountsWithBalance($partyId: ID!, $dateRange: DateRange) {
balanceChanges(filter: {partyIds: [$partyId]}, dateRange: $dateRange) {
edges {
node {
assetId
accountType
}
}
}
}
`;
/**
* __useAccountsWithBalanceQuery__
*
* To run a query within a React component, call `useAccountsWithBalanceQuery` and pass it any options that fit your needs.
* When your component renders, `useAccountsWithBalanceQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useAccountsWithBalanceQuery({
* variables: {
* partyId: // value for 'partyId'
* dateRange: // value for 'dateRange'
* },
* });
*/
export function useAccountsWithBalanceQuery(baseOptions: Apollo.QueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export function useAccountsWithBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>(AccountsWithBalanceDocument, options);
}
export type AccountsWithBalanceQueryHookResult = ReturnType<typeof useAccountsWithBalanceQuery>;
export type AccountsWithBalanceLazyQueryHookResult = ReturnType<typeof useAccountsWithBalanceLazyQuery>;
export type AccountsWithBalanceQueryResult = Apollo.QueryResult<AccountsWithBalanceQuery, AccountsWithBalanceQueryVariables>;
@@ -0,0 +1,349 @@
import { addDecimal, fromNanoSeconds } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import uniqBy from 'lodash/uniqBy';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { AccountHistoryQuery } from './__generated__/AccountHistory';
import { useAccountHistoryQuery } from './__generated__/AccountHistory';
import * as Schema from '@vegaprotocol/types';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import {
AsyncRenderer,
Splash,
Toggle,
TradingButton,
TradingDropdown,
TradingDropdownContent,
TradingDropdownItem,
TradingDropdownTrigger,
} from '@vegaprotocol/ui-toolkit';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { PriceChart } from 'pennant';
import 'pennant/dist/style.css';
import type { Account } from '@vegaprotocol/accounts';
import { accountsDataProvider } from '@vegaprotocol/accounts';
import {
useLocalStorageSnapshot,
useThemeSwitcher,
} from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { getAsset, type Market } from '@vegaprotocol/markets';
export const DateRange = {
RANGE_1D: '1D',
RANGE_7D: '7D',
RANGE_1M: '1M',
RANGE_3M: '3M',
RANGE_1Y: '1Y',
RANGE_YTD: 'YTD',
RANGE_ALL: 'All',
};
const dateRangeToggleItems = Object.entries(DateRange).map(([_, value]) => ({
label: t(value),
value: value,
}));
export const calculateStartDate = (range: string): string | undefined => {
const now = new Date();
switch (range) {
case DateRange.RANGE_1D:
return new Date(now.setDate(now.getDate() - 1)).toISOString();
case DateRange.RANGE_7D:
return new Date(now.setDate(now.getDate() - 7)).toISOString();
case DateRange.RANGE_1M:
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
case DateRange.RANGE_3M:
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
case DateRange.RANGE_1Y:
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
case DateRange.RANGE_YTD:
return new Date(now.setMonth(0)).toISOString();
default:
return undefined;
}
};
export const AccountHistoryContainer = () => {
const { pubKey } = useVegaWallet();
const { data: assets } = useAssetsDataProvider();
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
return (
<AsyncRenderer loading={!assets} error={undefined} data={assets}>
{assets && <AccountHistoryManager pubKey={pubKey} assetData={assets} />}
</AsyncRenderer>
);
};
const AccountHistoryManager = ({
pubKey,
assetData,
}: {
pubKey: string;
assetData: AssetFieldsFragment[];
}) => {
const [accountType, setAccountType] = useState<Schema.AccountType>(
Schema.AccountType.ACCOUNT_TYPE_GENERAL
);
const variablesForOneTimeQuery = useMemo(
() => ({
partyId: pubKey,
}),
[pubKey]
);
const { data: accounts } = useDataProvider({
dataProvider: accountsDataProvider,
variables: variablesForOneTimeQuery,
skip: !pubKey,
});
const assetIds = useMemo(
() => accounts?.map((e) => e?.asset?.id) || [],
[accounts]
);
const assets = useMemo(
() =>
assetData
.filter((a) => assetIds.includes(a.id))
.sort((a, b) => a.name.localeCompare(b.name)),
[assetData, assetIds]
);
const [assetId, setAssetId] = useLocalStorageSnapshot(
'account-history-active-asset-id'
);
const asset = useMemo(
() => assets.find((a) => a.id === assetId) || assets[0],
[assetId, assets]
);
const [range, setRange] = useState<typeof DateRange[keyof typeof DateRange]>(
DateRange.RANGE_1M
);
const [market, setMarket] = useState<Market | null>(null);
const marketFilterCb = useCallback(
(item: Market) => {
const itemAsset = getAsset(item);
return !asset?.id || itemAsset?.id === asset?.id;
},
[asset?.id]
);
const markets = useMemo<Market[] | null>(() => {
const arr =
accounts
?.filter((item: Account) => Boolean(item && item.market))
.map<Market>((item) => item.market as Market) ?? null;
return arr
? uniqBy(arr.filter(marketFilterCb), 'id').sort((a, b) =>
a.tradableInstrument.instrument.code.localeCompare(
b.tradableInstrument.instrument.code
)
)
: null;
}, [accounts, marketFilterCb]);
const resolveMarket = useCallback(
(m: Market) => {
setMarket(m);
const itemAsset = getAsset(m);
const newAssetId = itemAsset?.id;
const newAsset = assets.find((item) => item.id === newAssetId);
if ((!asset || (assets && newAssetId !== asset.id)) && newAsset) {
setAssetId(newAsset.id);
}
},
[asset, assets, setAssetId]
);
const variables = useMemo(
() => ({
partyId: pubKey,
assetId: asset?.id || '',
accountTypes: accountType ? [accountType] : undefined,
dateRange:
range === 'All' ? undefined : { start: calculateStartDate(range) },
marketIds: market?.id ? [market.id] : undefined,
}),
[pubKey, asset, accountType, range, market?.id]
);
const { data } = useAccountHistoryQuery({
variables,
skip: !asset || !pubKey,
});
return (
<div className="flex flex-col h-full gap-2">
<div className="flex flex-wrap justify-between px-1 pt-2 gap-2">
<div className="flex items-center gap-1 shrink-0">
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{accountType
? `${
AccountTypeMapping[
accountType as keyof typeof Schema.AccountType
]
} Account`
: t('Select account type')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{[
Schema.AccountType.ACCOUNT_TYPE_GENERAL,
Schema.AccountType.ACCOUNT_TYPE_BOND,
Schema.AccountType.ACCOUNT_TYPE_MARGIN,
].map((type) => (
<TradingDropdownItem
key={type}
onClick={() => {
setAccountType(type as Schema.AccountType);
// if not a margin account clear any market selection
if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) {
setMarket(null);
}
}}
>
{AccountTypeMapping[type as keyof typeof Schema.AccountType]}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{asset ? asset.symbol : t('Select asset')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{assets.map((a) => (
<TradingDropdownItem
key={a.id}
onClick={() => {
setAssetId(a.id);
// if the selected asset is different to the selected market clear the market
if (market && a.id !== getAsset(market).id) {
setMarket(null);
}
}}
>
{a.symbol}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<TradingDropdown
trigger={
<TradingDropdownTrigger>
<TradingButton size="small">
{market
? market.tradableInstrument.instrument.code
: t('Select market')}
</TradingButton>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent>
{market && (
<TradingDropdownItem key="0" onClick={() => setMarket(null)}>
{t('All markets')}
</TradingDropdownItem>
)}
{markets?.map((m) => (
<TradingDropdownItem
key={m.id}
onClick={() => resolveMarket(m)}
>
{m.tradableInstrument.instrument.code}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
</div>
<div className="justify-items-end">
<Toggle
id="account-history-date-range"
name="account-history-date-range"
toggles={dateRangeToggleItems}
checkedValue={range}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRange(e.target.value as keyof typeof DateRange)
}
size="sm"
/>
</div>
</div>
<div className="flex-1">
{asset && (
<div className="h-full">
<AccountHistoryChart
data={data}
accountType={accountType}
asset={asset}
/>
</div>
)}
</div>
</div>
);
};
export const AccountHistoryChart = ({
data,
accountType,
asset,
}: {
data: AccountHistoryQuery | undefined;
accountType: Schema.AccountType;
asset: AssetFieldsFragment;
}) => {
const { theme } = useThemeSwitcher();
const values: { cols: [string, string]; rows: [Date, number][] } | null =
useMemo(() => {
if (!data?.balanceChanges.edges.length) {
return null;
}
const valuesData = compact(data.balanceChanges.edges)
.reduce((acc, edge) => {
if (edge.node.accountType === accountType) {
acc?.push({
datetime: fromNanoSeconds(edge.node.timestamp),
balance: Number(addDecimal(edge.node.balance, asset.decimals)),
});
}
return acc;
}, [] as { datetime: Date; balance: number }[])
.reverse();
return {
cols: ['Date', `${asset.symbol} account balance`],
rows: compact(valuesData).map((d) => [d.datetime, d.balance]),
};
}, [accountType, asset.decimals, asset.symbol, data?.balanceChanges.edges]);
if (!data || !values?.rows.length) {
return <Splash> {t('No account history data')}</Splash>;
}
return <PriceChart data={values} theme={theme} />;
};
@@ -15,6 +15,7 @@ import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container';
import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container';
import { AccountHistoryContainer } from './account-history-container';
import {
ResizableGrid,
ResizableGridPanel,
@@ -65,7 +66,10 @@ export const Portfolio = () => {
<ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel minSize={75}>
<PortfolioGridChild>
<Tabs storageKey="console-portfolio-top-1">
<Tabs storageKey="console-portfolio-top">
<Tab id="account-history" name={t('Account history')}>
<AccountHistoryContainer />
</Tab>
<Tab
id="positions"
name={t('Positions')}
@@ -9,36 +9,10 @@ import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { Splash } from '@vegaprotocol/ui-toolkit';
const calculateStartDate = (range: string): string | undefined => {
const now = new Date();
switch (range) {
case DateRange.RANGE_1D:
return new Date(now.setDate(now.getDate() - 1)).toISOString();
case DateRange.RANGE_7D:
return new Date(now.setDate(now.getDate() - 7)).toISOString();
case DateRange.RANGE_1M:
return new Date(now.setMonth(now.getMonth() - 1)).toISOString();
case DateRange.RANGE_3M:
return new Date(now.setMonth(now.getMonth() - 3)).toISOString();
case DateRange.RANGE_1Y:
return new Date(now.setFullYear(now.getFullYear() - 1)).toISOString();
case DateRange.RANGE_YTD:
return new Date(now.setMonth(0)).toISOString();
default:
return undefined;
}
};
const DateRange = {
RANGE_1D: '1D',
RANGE_7D: '7D',
RANGE_1M: '1M',
RANGE_3M: '3M',
RANGE_1Y: '1Y',
RANGE_YTD: 'YTD',
RANGE_ALL: 'All',
};
import {
DateRange,
calculateStartDate,
} from '../../client-pages/portfolio/account-history-container';
export const FundingContainer = ({ marketId }: { marketId: string }) => {
const { theme } = useThemeSwitcher();
@@ -18,7 +18,7 @@ import BigNumber from 'bignumber.js';
import { useCheckLiquidityStatus } from '@vegaprotocol/liquidity';
import { AuctionTrigger, MarketTradingMode } from '@vegaprotocol/types';
import {
addDecimalsFormatNumberQuantum,
addDecimalsFormatNumber,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -30,14 +30,12 @@ interface Props {
marketId?: string;
noUpdate?: boolean;
assetDecimals: number;
quantum: string;
}
export const MarketLiquiditySupplied = ({
marketId,
assetDecimals,
noUpdate = false,
quantum,
}: Props) => {
const [market, setMarket] = useState<MarketData>();
const { params } = useNetworkParams([
@@ -81,12 +79,11 @@ export const MarketLiquiditySupplied = ({
});
const supplied = market?.suppliedStake
? addDecimalsFormatNumberQuantum(
? addDecimalsFormatNumber(
new BigNumber(market?.suppliedStake)
.multipliedBy(stakeToCcyVolume || 1)
.toString(),
assetDecimals,
quantum
assetDecimals
)
: '-';
@@ -111,10 +108,9 @@ export const MarketLiquiditySupplied = ({
<span>{t('Supplied stake')}</span>
<span>
{market?.suppliedStake
? addDecimalsFormatNumberQuantum(
market?.suppliedStake,
assetDecimals,
quantum
? addDecimalsFormatNumber(
new BigNumber(market?.suppliedStake).toString(),
assetDecimals
)
: '-'}
</span>
@@ -123,10 +119,9 @@ export const MarketLiquiditySupplied = ({
<span>{t('Target stake')}</span>
<span>
{market?.targetStake
? addDecimalsFormatNumberQuantum(
market?.targetStake,
assetDecimals,
quantum
? addDecimalsFormatNumber(
new BigNumber(market?.targetStake).toString(),
assetDecimals
)
: '-'}
</span>
@@ -21,7 +21,6 @@ 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) => {
@@ -44,12 +43,6 @@ 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
? {
@@ -88,6 +81,30 @@ 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', () => {
@@ -103,17 +120,6 @@ 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,
@@ -131,9 +137,7 @@ describe('MarketSuccessorBanner', () => {
render(<MarketSuccessorBanner market={market} />, {
wrapper: MockedProvider,
});
expect(
screen.getByText('has a 24h trading volume of 101.367')
).toBeInTheDocument();
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument();
});
it('should display optionally duration', () => {
@@ -4,7 +4,6 @@ import type { Market } from '@vegaprotocol/markets';
import {
calcCandleVolume,
useCandles,
useMarketState,
useSuccessorMarket,
} from '@vegaprotocol/markets';
import {
@@ -30,9 +29,7 @@ export const MarketSuccessorBanner = ({
}: {
market: Market | null;
}) => {
const { data: marketState } = useMarketState(market?.id);
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
const { data: successorData, loading } = useSuccessorMarket(market?.id);
const { data: successorData } = useSuccessorMarket(market?.id);
const [visible, setVisible] = useState(true);
@@ -48,6 +45,11 @@ 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,
});
@@ -64,7 +66,7 @@ export const MarketSuccessorBanner = ({
)
: null;
if (!loading && (isSettled || successorData) && visible) {
if (isInContinuesMode && visible) {
return (
<NotificationBanner
intent={Intent.Primary}
@@ -72,47 +74,34 @@ export const MarketSuccessorBanner = ({
setVisible(false);
}}
>
<div className="uppercase">
{successorData
? t('This market has been succeeded')
: t('This market has been settled')}
<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>
{(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>
);
}
@@ -1,9 +1,6 @@
import { Fragment, useState } from 'react';
import {
marketViewProposalsDataProvider,
type NewMarketSuccessorFieldsFragment,
} from '@vegaprotocol/proposals';
import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals';
import { useMarketViewProposals } from '@vegaprotocol/proposals';
import {
ExternalLink,
Intent,
@@ -12,20 +9,17 @@ import {
import { t } from '@vegaprotocol/i18n';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import * as Types from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
export const MarketSuccessorProposalBanner = ({
marketId,
}: {
marketId?: string;
}) => {
const { data: proposals } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
const proposals = useMarketViewProposals({
skip: !marketId,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
typename: 'NewMarket',
});
const successors =
@@ -20,13 +20,10 @@ const marketMock = {
},
} as Market;
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
variables: { inState: Types.ProposalState.STATE_PASSED },
},
result: {
data: {
@@ -34,7 +31,7 @@ const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
edges: [
{
node: {
id: '1',
id: 'first-id',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
@@ -59,7 +56,7 @@ const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
},
{
node: {
id: '2',
id: 'second-id',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
@@ -87,99 +84,7 @@ const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
},
},
};
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
},
result: {
data: {
proposalsConnection: {
edges: [
{
node: {
id: '3',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '4',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-11T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '5',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-4',
tradableInstrument: {
instrument: {
name: 'Market four name',
code: 'Market four',
},
},
},
},
},
},
},
],
},
},
},
};
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
const mocks: MockedResponse[] = [proposalMock];
describe('MarketTerminationBanner', () => {
beforeAll(() => {
@@ -203,28 +108,4 @@ describe('MarketTerminationBanner', () => {
screen.getByTestId('termination-warning-banner-market-1')
).toBeInTheDocument();
});
it('should render link to proposals', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposals')).toBeInTheDocument();
});
it('should render link to proposal', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposal')).toBeInTheDocument();
});
});
@@ -1,67 +1,12 @@
import type { ReactNode } from 'react';
import { useState } from 'react';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
import { useMarketViewProposals } from '@vegaprotocol/proposals';
import { t } from '@vegaprotocol/i18n';
import * as Types from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/markets';
import { getQuoteName } from '@vegaprotocol/markets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import {
DApp,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useDataProvider } from '@vegaprotocol/data-provider';
const filterProposals = (
data: MarketViewProposalFieldsFragment[] | null,
marketId: string,
now: number
) =>
sortBy(
(data || []).filter(
(item) =>
item.terms.change.__typename === 'UpdateMarketState' &&
item.terms.change.market.id === marketId &&
item.terms.change.updateType ===
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
item.terms.enactmentDatetime &&
new Date(item.terms.enactmentDatetime).getTime() > now
),
(item) => item.terms.enactmentDatetime
);
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
const date = format(enactmentDatetime, 'dd MMMM');
const duration = formatDuration(
intervalToDuration({
start: new Date(),
end: enactmentDatetime,
}),
{
format: ['days', 'hours'],
}
);
const price =
proposal.terms.change.__typename === 'UpdateMarketState'
? proposal.terms.change.price
: '';
return {
date,
duration,
price,
};
};
export const MarketTerminationBanner = ({
market,
@@ -70,55 +15,57 @@ export const MarketTerminationBanner = ({
}) => {
const [visible, setVisible] = useState(true);
const skip = !market || !visible;
const { data: passedProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
const proposalsData = useMarketViewProposals({
skip,
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
inState: Types.ProposalState.STATE_PASSED,
typename: 'UpdateMarketState',
});
const { data: openProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
});
const governanceLink = useLinks(DApp.Governance);
if (!market) return null;
const now = Date.now();
const passedProposals = filterProposals(passedProposalsData, market.id, now);
const openProposals = filterProposals(openProposalsData, market.id, now);
const marketFound = (proposalsData || []).find(
(item) =>
item.terms.change.__typename === 'UpdateMarketState' &&
item.terms.change.market.id === market.id &&
item.terms.change.updateType ===
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
item.state === Types.ProposalState.STATE_PASSED // subscription doesn't have state parameter
);
const name = market.tradableInstrument.instrument.code;
if (!passedProposals.length && !openProposals.length) {
return null;
}
const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime);
const name =
marketFound?.terms.change.__typename === 'UpdateMarketState'
? marketFound.terms.change.market.tradableInstrument.instrument.code
: '';
const assetSymbol = getQuoteName(market);
const proposalLink =
!passedProposals.length && openProposals[0]?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
: undefined;
const proposalsLink =
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
let content: ReactNode;
if (passedProposals.length) {
const { date, duration, price } = getMessageVariables(passedProposals[0]);
content = (
<>
if (name && enactmentDatetime.getTime() > Date.now()) {
const dayMonthDate = format(enactmentDatetime, 'dd MMMM');
const duration = intervalToDuration({
start: new Date(),
end: enactmentDatetime,
});
const formattedDuration = formatDuration(duration, {
format: ['days', 'hours'],
});
const price =
marketFound?.terms.change.__typename === 'UpdateMarketState'
? marketFound.terms.change.price
: '';
const assetSymbol = getQuoteName(market);
return (
<NotificationBanner
intent={Intent.Warning}
onClose={() => {
setVisible(false);
}}
data-testid={`termination-warning-banner-${market.id}`}
>
<div className="uppercase mb-1">
{t('Trading on Market %s will stop on %s', [name, date])}
{t('Trading on Market %s will stop on %s', [name, dayMonthDate])}
</div>
<div>
{t(
'You will no longer be able to hold a position on this market when it closes in %s.',
[duration]
[formattedDuration]
)}{' '}
{price &&
assetSymbol &&
@@ -127,57 +74,8 @@ export const MarketTerminationBanner = ({
assetSymbol,
])}
</div>
</>
);
} else if (openProposals.length > 1) {
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market %s may stop. There are open proposals to close this market',
[name]
)}
</div>
<div>
<ExternalLink href={proposalsLink}>
{t('View proposals')}
</ExternalLink>
</div>
</>
);
} else {
const { date, price } = getMessageVariables(openProposals[0]);
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market %s may stop on %s. There is open proposal to close this market.',
[name, date]
)}
</div>
<div>
{price &&
assetSymbol &&
t('Proposed final price is %s %s.', [
addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
])}
</div>
<div>
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</div>
</>
</NotificationBanner>
);
}
return (
<NotificationBanner
intent={openProposals.length ? Intent.Warning : Intent.Info}
onClose={() => {
setVisible(false);
}}
data-testid={`termination-warning-banner-${market.id}`}
>
{content}
</NotificationBanner>
);
return null;
};
@@ -56,7 +56,6 @@ const MarketData = ({
variables: {
marketId: market.id,
},
fetchPolicy: 'no-cache',
});
const marketData = data?.marketsData[0];
@@ -71,8 +70,6 @@ const MarketData = ({
const marketTradingMode = marketData
? marketData.marketTradingMode
: market.data
? market.data.marketTradingMode
: market.tradingMode;
const mode = [
@@ -98,7 +95,7 @@ const MarketData = ({
<>
<div className="w-2/5" role="gridcell">
<h3 className="flex items-baseline">
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
<span className="text-sm lg:text-base text-ellipsis whitespace-nowrap overflow-hidden">
{market.tradableInstrument.instrument.code}
</span>
{allProducts && productType && (
@@ -2,10 +2,7 @@ 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,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
@@ -39,10 +36,6 @@ 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',
@@ -63,10 +56,7 @@ describe('MarketSelector', () => {
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_SUSPENDED,
}),
state: MarketState.STATE_SUSPENDED,
tradableInstrument: {
instrument: {
code: 'b',
@@ -87,10 +77,7 @@ describe('MarketSelector', () => {
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
state: MarketState.STATE_CLOSED,
tradableInstrument: {
instrument: {
product: {
@@ -104,10 +91,7 @@ describe('MarketSelector', () => {
}),
createMarketFragment({
id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
state: MarketState.STATE_ACTIVE,
tradableInstrument: {
instrument: {
code: 'c',
@@ -128,10 +112,6 @@ describe('MarketSelector', () => {
}),
createMarketFragment({
id: 'market-4',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
code: 'cd',
@@ -152,10 +132,7 @@ describe('MarketSelector', () => {
}),
];
const activeMarkets = markets.filter((m) =>
// @ts-ignore candles get joined outside this type
isMarketActive(m.data.marketState)
);
const activeMarkets = markets.filter((m) => isMarketActive(m.state));
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
@@ -4,10 +4,7 @@ import { useMarketSelectorList } from './use-market-selector-list';
import { isMarketActive } from '../../lib/utils';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import {
createMarketFragment,
createMarketsDataFragment,
} from '@vegaprotocol/mock';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from './market-selector';
@@ -34,40 +31,22 @@ describe('useMarketSelectorList', () => {
it('returns all markets active and suspended markets', () => {
const markets = [
createMarketFragment({
id: 'market-0',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
}),
createMarketFragment({ id: 'market-0' }),
createMarketFragment({
id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_SUSPENDED,
}),
state: MarketState.STATE_SUSPENDED,
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_CLOSED,
}),
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-4',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_PENDING,
}),
state: MarketState.STATE_PENDING,
}),
];
mockUseMarketList.mockReturnValue({
@@ -77,8 +56,7 @@ describe('useMarketSelectorList', () => {
});
const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) =>
// @ts-ignore candles get joined outside this type
isMarketActive(m.data.marketState)
isMarketActive(m.state)
);
expect(result.current).toEqual({
data: markets,
@@ -92,10 +70,6 @@ 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: {
@@ -116,10 +90,6 @@ describe('useMarketSelectorList', () => {
// }),
createMarketFragment({
id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
product: {
@@ -165,10 +135,6 @@ 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: {
@@ -182,10 +148,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
product: {
@@ -199,10 +161,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
product: {
@@ -216,10 +174,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
product: {
@@ -284,10 +238,6 @@ 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',
@@ -297,10 +247,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
code: 'def',
@@ -310,10 +256,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
code: 'defg',
@@ -323,10 +265,6 @@ describe('useMarketSelectorList', () => {
}),
createMarketFragment({
id: 'market-3',
// @ts-ignore candles get joined outside this type
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
}),
tradableInstrument: {
instrument: {
code: 'ggg',
@@ -395,11 +333,11 @@ describe('useMarketSelectorList', () => {
const markets = [
createMarketFragment({
id: 'market-0',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
data: {
markPrice: '1',
}),
},
// @ts-ignore candles not on fragment
candles: [
{
@@ -411,10 +349,9 @@ describe('useMarketSelectorList', () => {
id: 'market-1',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
data: {
markPrice: '1',
}),
},
// @ts-ignore candles not on fragment
candles: [
{
@@ -426,10 +363,9 @@ describe('useMarketSelectorList', () => {
id: 'market-2',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
data: {
markPrice: '1',
}),
},
// @ts-ignore candles not on fragment
candles: [
{
@@ -441,10 +377,9 @@ describe('useMarketSelectorList', () => {
id: 'market-3',
state: MarketState.STATE_ACTIVE,
// @ts-ignore data not on fragment
data: createMarketsDataFragment({
marketState: MarketState.STATE_ACTIVE,
data: {
markPrice: '1',
}),
},
// @ts-ignore candles not on fragment
candles: [
{
@@ -479,10 +414,6 @@ 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: [
{
@@ -495,10 +426,6 @@ 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: [
{
@@ -511,10 +438,6 @@ 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: [
{
@@ -559,30 +482,18 @@ 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,12 +22,8 @@ export const useMarketSelectorList = ({
const markets = useMemo(() => {
if (!data?.length) return [];
const markets = data
// 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 active
.filter((m) => isMarketActive(m.state))
// only selected product type
.filter((m) => {
if (
@@ -10,19 +10,6 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { OrderStatus } from '@vegaprotocol/types';
import { Links } from '../../lib/links';
const resolveNoRowsMessage = (filter?: Filter) => {
switch (filter) {
case Filter.Open:
return t('No open orders');
case Filter.Closed:
return t('No closed orders');
case Filter.Rejected:
return t('No rejected orders');
default:
return t('No orders');
}
};
export const FilterStatusValue = {
[Filter.Open]: [OrderStatus.STATUS_ACTIVE, OrderStatus.STATUS_PARKED],
[Filter.Closed]: [
@@ -56,7 +43,6 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
if (!pubKey) {
return <Splash>{t('Please connect Vega wallet')}</Splash>;
}
const noRowsMessage = resolveNoRowsMessage(filter);
return (
<OrderListManager
@@ -70,7 +56,6 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
}
isReadOnly={isReadOnly}
gridProps={gridStoreCallbacks}
noRowsMessage={noRowsMessage}
/>
);
};
+1 -10
View File
@@ -57,16 +57,7 @@ export const rows: Rows = [
key: AssetDetail.ID,
label: t('ID'),
tooltip: '',
value: (asset) => (
<>
{truncateMiddle(asset.id)}{' '}
<CopyWithTooltip text={asset.id}>
<button title={t('Copy id to clipboard')}>
<VegaIcon size={14} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
</>
),
value: (asset) => truncateMiddle(asset.id),
},
{
key: AssetDetail.TYPE,
+1 -3
View File
@@ -63,9 +63,7 @@ export const CandlesChartContainer = ({
overlays,
studies,
notEnoughDataText: (
<span className="text-xs text-center">
{t('No open orders')}
</span>
<span className="text-xs text-center">{t('No data')}</span>
),
initialNumCandlesToDisplay: candlesCount,
studySize: STUDY_SIZE,
+10 -27
View File
@@ -3,12 +3,7 @@ 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,
PageInfo,
} from './generic-data-provider';
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
import { variablesIsEqualCustomizer } from './generic-data-provider';
export interface useDataProviderParams<
@@ -17,23 +12,13 @@ export interface useDataProviderParams<
Variables extends OperationVariables | undefined = undefined
> {
dataProvider: Subscribe<Data, Delta, Variables>;
update?: ({
delta,
data,
pageInfo,
}: {
delta?: Delta;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
insert?: ({
insertionData,
data,
pageInfo,
}: {
insertionData?: Data | null;
data: Data | null;
pageInfo: PageInfo | null;
}) => boolean;
variables: Variables;
skipUpdates?: boolean;
@@ -45,7 +30,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, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
*/
export const useDataProvider = <
Data,
@@ -63,7 +48,6 @@ 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);
@@ -109,7 +93,6 @@ export const useDataProvider = <
isInsert,
isUpdate,
loaded,
pageInfo,
} = args;
setError(error);
setLoading(!loaded && loading);
@@ -121,22 +104,21 @@ export const useDataProvider = <
(skipUpdatesRef.current ||
(!skipUpdatesRef.current &&
updateRef.current &&
updateRef.current({ delta, data, pageInfo })))
updateRef.current({ delta, data })))
) {
return;
}
if (
isInsert &&
insertRef.current &&
insertRef.current({ insertionData, data, pageInfo })
insertRef.current({ insertionData, data })
) {
return;
}
}
setData(data);
setPageInfo(pageInfo);
if (!loading && !isUpdate && updateRef.current) {
updateRef.current({ data, pageInfo });
updateRef.current({ data });
}
}, []);
@@ -154,13 +136,15 @@ export const useDataProvider = <
useEffect(() => {
setData(null);
setPageInfo(null);
setError(undefined);
if (updateRef.current) {
updateRef.current({ data: null, pageInfo: null });
updateRef.current({ data: null });
}
if (skip) {
setLoading(false);
if (updateRef.current) {
updateRef.current({ data: null });
}
return;
}
setLoading(true);
@@ -181,7 +165,6 @@ export const useDataProvider = <
}, [client, dataProvider, callback, variables, skip]);
return {
data,
pageInfo,
loading,
error,
flush,
@@ -14,17 +14,16 @@ export const MarketProductPill = ({
}: {
productType?: ProductType;
}) => {
if (!productType) {
return null;
}
return (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProductTypeMapping[productType]}
>
{ProductTypeShortName[productType]}
</Pill>
productType && (
<Pill
size="xxs"
className="uppercase ml-0.5"
title={ProductTypeMapping[productType]}
>
{ProductTypeShortName[productType]}
</Pill>
)
);
};
@@ -43,7 +43,7 @@ import {
REDUCE_ONLY_TOOLTIP,
stopSubmit,
getNotionalSize,
getBaseQuoteUnit,
getAssetUnit,
} from './deal-ticket';
import { TypeToggle } from './type-selector';
import {
@@ -854,7 +854,7 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
}, [watch, market.id, updateStoredFormValues]);
const quoteName = getQuoteName(market);
const assetUnit = getBaseQuoteUnit(
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
variables: {
partyId: 'pubKey',
filter: { liveOnly: true },
pagination: { first: 1000 },
pagination: { first: 5000 },
},
},
result: {
@@ -127,7 +127,7 @@ const getDefaultValues = (
...storedValues,
});
export const getBaseQuoteUnit = (tags?: string[] | null) =>
export const getAssetUnit = (tags?: string[] | null) =>
tags
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
?.replace(/^[^:]*:/, '');
@@ -273,7 +273,9 @@ export const DealTicket = ({
const assetSymbol = getAsset(market).symbol;
const assetUnit = getQuoteName(market);
const assetUnit = getAssetUnit(
market.tradableInstrument.instrument.metadata.tags
);
const summaryError = useMemo(() => {
if (!pubKey) {
+2 -6
View File
@@ -42,12 +42,8 @@ fragment FillEdge on TradeEdge {
cursor
}
query Fills(
$filter: TradesFilter
$pagination: Pagination
$dateRange: DateRange
) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
edges {
...FillEdge
}
+2 -4
View File
@@ -12,7 +12,6 @@ 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>;
}>;
@@ -96,8 +95,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
}
${TradeFeeFieldsFragmentDoc}`;
export const FillsDocument = gql`
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
query Fills($filter: TradesFilter, $pagination: Pagination) {
trades(filter: $filter, pagination: $pagination) {
edges {
...FillEdge
}
@@ -125,7 +124,6 @@ export const FillsDocument = gql`
* variables: {
* filter: // value for 'filter'
* pagination: // value for 'pagination'
* dateRange: // value for 'dateRange'
* },
* });
*/
+18 -53
View File
@@ -1,12 +1,11 @@
import type { AgGridReact } from 'ag-grid-react';
import { useCallback, useRef, useState } from 'react';
import { useRef } 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;
@@ -23,60 +22,26 @@ export const FillsManager = ({
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
partyIds: [partyId],
};
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({
const { data, error } = useDataProvider({
dataProvider: fillsWithMarketProvider,
update: ({ data }) => {
if (data?.length && gridRef.current?.api) {
gridRef.current?.api.setRowData(data);
return true;
}
return false;
},
variables: { filter },
});
return (
<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>
return (
<FillsTable
ref={gridRef}
rowData={data}
partyId={partyId}
onMarketClick={onMarketClick}
overlayNoRowsTemplate={error ? error.message : t('No fills')}
{...gridProps}
/>
);
};
-2
View File
@@ -20,7 +20,6 @@ import {
negativeClassNames,
MarketNameCell,
COL_DEFS,
DateRangeFilter,
} from '@vegaprotocol/datagrid';
import type {
VegaValueFormatterParams,
@@ -121,7 +120,6 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
},
{
headerName: t('Date'),
filter: DateRangeFilter,
field: 'createdAt',
valueFormatter: ({
value,
+1 -1
View File
@@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
volumeFormat={volumeFormat}
priceFormat={priceFormat}
notEnoughDataText={
<span className="text-xs text-center">{t('No open orders')}</span>
<span className="text-xs text-center">{t('No data')}</span>
}
/>
)}
@@ -11,8 +11,14 @@ import type {
MarketDepthQuery,
MarketDepthQueryVariables,
MarketDepthUpdateSubscription,
PriceLevelFieldsFragment,
} from './__generated__/MarketDepth';
export type OrderbookData = {
asks: PriceLevelFieldsFragment[];
bids: PriceLevelFieldsFragment[];
};
interface OrderbookManagerProps {
marketId: string;
onClick: (args: { price?: string; size?: string }) => void;
+1 -1
View File
@@ -238,7 +238,7 @@ export const Orderbook = ({
</>
) : (
<div className="absolute inset-0">
<Splash>{t('No open orders')}</Splash>
<Splash>{t('No data')}</Splash>
</div>
)}
</div>
+2 -3
View File
@@ -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, 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 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 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, 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 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 const MarketsDataFieldsFragmentDoc = gql`
fragment MarketsDataFields on MarketData {
@@ -20,7 +20,6 @@ export const MarketsDataFieldsFragmentDoc = gql`
markPrice
trigger
staticMidPrice
marketState
marketTradingMode
indicativeVolume
indicativePrice
@@ -126,16 +126,6 @@ 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,
@@ -186,11 +176,3 @@ 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,7 +7,6 @@ fragment MarketsDataFields on MarketData {
markPrice
trigger
staticMidPrice
marketState
marketTradingMode
indicativeVolume
indicativePrice
@@ -34,7 +34,6 @@ export const createMarketsDataFragment = (
__typename: 'Market',
},
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketState: Schema.MarketState.STATE_ACTIVE,
staticMidPrice: '0',
indicativePrice: '0',
bestStaticBidPrice: '0',
@@ -4,10 +4,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaTransactionStore } from '@vegaprotocol/web3';
import { useHasAmendableOrder } from '../../order-hooks';
export const OpenOrdersMenu = () => {
export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => {
const { isReadOnly } = useVegaWallet();
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder();
const hasAmendableOrder = useHasAmendableOrder(marketId);
if (isReadOnly) {
return null;
@@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 1000,
first: 5000,
},
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },
@@ -239,11 +239,11 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
export const hasAmendableOrderProvider = makeDerivedDataProvider<
boolean,
never,
{ partyId: string }
{ partyId: string; marketId?: string }
>([activeOrdersProvider], (parts) => {
const activeOrders = parts[0] as ReturnType<typeof getData>;
const hasAmendableOrder = activeOrders.some(
(order) => !order.liquidityProvision
(order) => !(order.liquidityProvision || order.peggedOrder)
);
return hasAmendableOrder;
});
@@ -1,18 +1,23 @@
import { render, screen } from '@testing-library/react';
import type { OrderListManagerProps } from './order-list-manager';
import { OrderListManager, Filter } from './order-list-manager';
import { render, screen, act } from '@testing-library/react';
import { OrderListManager } from './order-list-manager';
import * as useDataProviderHook from '@vegaprotocol/data-provider';
import type { OrderFieldsFragment } from '../';
import * as orderListMock from '../order-list/order-list';
import { forwardRef } from 'react';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { MockedProvider } from '@apollo/client/testing';
const generateJsx = (props: Partial<OrderListManagerProps> | null = null) => {
// @ts-ignore OrderList is read only but we need to override with the forwardRef to
// avoid warnings about padding refs
orderListMock.OrderListTable = forwardRef(() => <div>OrderList</div>);
const generateJsx = () => {
const pubKey = '0x123';
return (
<MockedProvider>
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
<OrderListManager partyId={pubKey} isReadOnly={false} {...props} />
<OrderListManager partyId={pubKey} isReadOnly={false} />
</VegaWalletContext.Provider>
</MockedProvider>
);
@@ -27,50 +32,10 @@ describe('OrderListManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(generateJsx());
expect(
await screen.getByRole('treegrid', {
name: (_, element) => element.classList.contains('ag-root'),
})
).toBeInTheDocument();
});
it('should display order info', async () => {
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
data: [{ id: '1' } as OrderFieldsFragment],
loading: false,
error: new Error('Query error'),
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
await act(async () => {
render(generateJsx());
});
render(generateJsx());
expect(
await screen.getByText('Something went wrong: Query error')
).toBeInTheDocument();
});
it('should display no rows overlay', async () => {
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
data: [],
loading: false,
error: undefined,
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
render(
generateJsx({ filter: Filter.Closed, noRowsMessage: 'no rows message' })
);
expect(screen.getByText('no rows message')).toBeInTheDocument();
expect(await screen.findByText('OrderList')).toBeInTheDocument();
});
});
@@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n';
import { useCallback, useRef, useState, useEffect } from 'react';
import { useCallback, useRef, useState } from 'react';
import type { AgGridReact } from 'ag-grid-react';
import { OrderListTable } from '../order-list';
import { OrderListTable } from '../order-list/order-list';
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
@@ -11,7 +11,6 @@ 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, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
export enum Filter {
'Open' = 'Open',
@@ -26,7 +25,6 @@ export interface OrderListManagerProps {
isReadOnly: boolean;
filter?: Filter;
gridProps?: ReturnType<typeof useDataGridEvents>;
noRowsMessage?: string;
}
export const OrderListManager = ({
@@ -36,7 +34,6 @@ export const OrderListManager = ({
isReadOnly,
filter,
gridProps,
noRowsMessage,
}: OrderListManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const [editOrder, setEditOrder] = useState<Order | null>(null);
@@ -47,19 +44,18 @@ export const OrderListManager = ({
? { partyId, filter: { liveOnly: true } }
: { partyId };
const { data, error, pageInfo, load } = useDataProvider({
const { data, error } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables,
update: ({ data }) => {
if (data && gridRef.current?.api) {
gridRef.current.api.setRowData(data);
return true;
}
return false;
},
});
useEffect(() => {
if (!data || !data.length) {
gridRef.current?.api?.showNoRowsOverlay();
} else {
gridRef.current?.api?.hideOverlay();
}
}, [data]);
const cancel = useCallback(
(order: Order) => {
if (!order.market) return;
@@ -73,78 +69,22 @@ 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>;
}
return (
<>
<div className="h-full relative">
<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>
<OrderListTable
rowData={data}
ref={gridRef}
filter={filter}
onCancel={cancel}
onEdit={setEditOrder}
onView={setViewOrder}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
isReadOnly={isReadOnly}
overlayNoRowsTemplate={error ? error.message : t('No orders')}
{...gridProps}
/>
</div>
{editOrder && (
<OrderEditDialog
@@ -1,32 +0,0 @@
import { act, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { OrderEditDialog } from './order-edit-dialog';
import { limitOrder } from '../mocks';
describe('OrderEditDialog', () => {
it('must be warned (pre-submit) if the input price has too many digits after the decimal place for the market', async () => {
// 7003-MORD-013
await act(async () => {
render(
<OrderEditDialog
order={limitOrder}
onChange={jest.fn()}
isOpen={true}
onSubmit={jest.fn()}
/>
);
});
const editOrder = await screen.findByTestId('edit-order');
const limitPrice = within(editOrder).getByLabelText('Price');
await userEvent.type(limitPrice, '0.111111');
const submitButton = within(editOrder).getByRole('button', {
name: 'Update',
});
await userEvent.click(submitButton);
const inputErrorText = within(editOrder).getByTestId('input-error-text');
expect(inputErrorText).toHaveTextContent(
'Price accepts up to 1 decimal places'
);
});
});
@@ -46,12 +46,6 @@ const generateJsx = (
describe('OrderListTable', () => {
it('should render correct columns', async () => {
// 7003-MORD-001
// 7003-MORD-002
// 7003-MORD-003
// 7003-MORD-004
// 7003-MORD-005
// 7003-MORD-006
await act(async () => {
render(generateJsx({ rowData: [marketOrder, limitOrder] }));
});
@@ -190,7 +184,6 @@ describe('OrderListTable', () => {
describe('amend cell', () => {
it('allows cancelling and editing for permitted orders', async () => {
// 7003-MORD-007
const mockEdit = jest.fn();
const mockCancel = jest.fn();
const order = generateOrder({
@@ -216,7 +209,6 @@ describe('OrderListTable', () => {
});
it('does not allow cancelling and editing for permitted orders if read only', async () => {
// 7003-MORD-008
const mockEdit = jest.fn();
const mockCancel = jest.fn();
const order = generateOrder({
@@ -241,7 +233,6 @@ describe('OrderListTable', () => {
});
it('shows if an order is a liquidity provision order and does not show order actions', async () => {
// 7003-MORD-008
const order = generateOrder({
type: Schema.OrderType.TYPE_LIMIT,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
@@ -260,7 +251,6 @@ describe('OrderListTable', () => {
});
it('shows if an order is a pegged order and does not show order actions', async () => {
// 7003-MORD-008
const order = generateOrder({
type: Schema.OrderType.TYPE_LIMIT,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
@@ -34,7 +34,7 @@ import type {
} from '@vegaprotocol/datagrid';
import type { AgGridReact } from 'ag-grid-react';
import type { Order } from '../order-data-provider';
import { Filter } from '../order-list-manager/order-list-manager';
import { Filter } from '../order-list-manager';
import type { ColDef } from 'ag-grid-community';
const defaultColDef = {
@@ -30,7 +30,6 @@ describe('StopOrdersManager', () => {
flush: jest.fn(),
reload: jest.fn(),
load: jest.fn(),
pageInfo: null,
});
await act(async () => {
render(generateJsx());
@@ -3,7 +3,7 @@ import { hasAmendableOrderProvider } from '../components/order-data-provider';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
export const useHasAmendableOrder = () => {
export const useHasAmendableOrder = (marketId?: string) => {
const { pubKey } = useVegaWallet();
const [hasAmendableOrder, setHasAmendableOrder] = useState(false);
const update = useCallback(({ data }: { data: boolean | null }) => {
@@ -15,6 +15,7 @@ export const useHasAmendableOrder = () => {
update,
variables: {
partyId: pubKey || '',
marketId,
},
skip: !pubKey,
});
@@ -1,5 +1,2 @@
export {
marketViewProposalsDataProvider,
proposalsDataProvider,
} from './proposals-data-provider';
export * from './proposals-data-provider';
export * from './__generated__/Proposals';
@@ -1,67 +0,0 @@
import { update } from './proposals-data-provider';
import type {
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables,
} from './__generated__/Proposals';
import * as Types from '@vegaprotocol/types';
describe('proposals data provider', () => {
const proposal = {
id: '1',
state: Types.ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarketState',
},
},
} as MarketViewProposalFieldsFragment;
const data = [proposal];
const delta = { ...proposal };
const reload = jest.fn();
const variables: MarketViewProposalsQueryVariables = {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
};
it('update existing data', () => {
expect(update(data, delta, reload, variables)?.[0]).toBe(delta);
});
it('removes existing data if delta do not match inState', () => {
expect(
update(
data,
{ ...delta, state: Types.ProposalState.STATE_PASSED },
reload,
variables
)?.length
).toBe(0);
});
it('do not add delta if it do not match inState', () => {
expect(
update(
data,
{ ...delta, id: '2', state: Types.ProposalState.STATE_PASSED },
reload,
variables
)?.length
).toBe(1);
});
it('do not add delta if it do not match proposalType', () => {
expect(
update(
data,
{
...delta,
id: '2',
terms: { ...delta.terms, change: { __typename: 'UpdateMarket' } },
},
reload,
variables
)?.length
).toBe(1);
});
});
@@ -1,7 +1,5 @@
import type { Update } from '@vegaprotocol/data-provider';
import { makeDataProvider } from '@vegaprotocol/data-provider';
import produce from 'immer';
import * as Types from '@vegaprotocol/types';
import type {
ProposalsListQuery,
ProposalsListQueryVariables,
@@ -10,7 +8,6 @@ import type {
MarketViewProposalFieldsFragment,
MarketViewProposalsQuery,
MarketViewProposalsQueryVariables,
MarketViewLiveProposalsSubscriptionVariables,
} from './__generated__/Proposals';
import {
MarketViewLiveProposalsDocument,
@@ -45,61 +42,21 @@ export const proposalsDataProvider = makeDataProvider<
errors.every((e) => e.message.match(/failed to get asset for ID/)),
});
const ProposalTypeMap: Record<
Types.ProposalType,
Types.ProposalChange['__typename']
> = {
[Types.ProposalType.TYPE_CANCEL_TRANSFER]: 'CancelTransfer',
[Types.ProposalType.TYPE_NETWORK_PARAMETERS]: 'UpdateNetworkParameter',
[Types.ProposalType.TYPE_NEW_ASSET]: 'NewAsset',
[Types.ProposalType.TYPE_NEW_FREE_FORM]: 'NewFreeform',
[Types.ProposalType.TYPE_NEW_MARKET]: 'NewMarket',
[Types.ProposalType.TYPE_NEW_SPOT_MARKET]: 'NewSpotMarket',
[Types.ProposalType.TYPE_NEW_TRANSFER]: 'NewTransfer',
[Types.ProposalType.TYPE_UPDATE_ASSET]: 'UpdateAsset',
[Types.ProposalType.TYPE_UPDATE_MARKET]: 'UpdateMarket',
[Types.ProposalType.TYPE_UPDATE_MARKET_STATE]: 'UpdateMarketState',
[Types.ProposalType.TYPE_UPDATE_REFERRAL_PROGRAM]: 'UpdateReferralProgram',
[Types.ProposalType.TYPE_UPDATE_SPOT_MARKET]: 'UpdateSpotMarket',
[Types.ProposalType.TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM]:
'UpdateVolumeDiscountProgram',
};
const matchFilter = (
data: MarketViewProposalFieldsFragment,
variables: MarketViewProposalsQueryVariables
) => {
return (
(!variables.inState || data.state === variables.inState) &&
(!variables.proposalType ||
data.terms.change.__typename === ProposalTypeMap[variables.proposalType])
);
};
export const update: Update<
MarketViewProposalFieldsFragment[] | null,
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables
> = (
const update = (
data: MarketViewProposalFieldsFragment[] | null,
delta: MarketViewProposalFieldsFragment,
reload,
variables
delta: MarketViewProposalFieldsFragment
) => {
const updateData = produce(data || [], (draft) => {
const { id } = delta;
const index = draft.findIndex((item) => item.id === id);
const match = matchFilter(delta, variables);
if (index === -1) {
if (match) {
draft.unshift(delta);
}
draft.unshift(delta);
} else {
if (match) {
draft[index] = delta;
} else {
draft.splice(index, 1);
}
const currNode = draft[index];
draft[index] = {
...currNode,
...delta,
};
}
});
return updateData;
@@ -109,15 +66,12 @@ const getMarketProposalsData = (
responseData: MarketViewProposalsQuery | null
) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || [];
const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {};
export const marketViewProposalsDataProvider = makeDataProvider<
MarketViewProposalsQuery,
MarketViewProposalFieldsFragment[],
MarketViewLiveProposalsSubscription,
MarketViewProposalFieldsFragment,
MarketViewProposalsQueryVariables,
MarketViewLiveProposalsSubscriptionVariables
MarketViewProposalsQueryVariables
>({
query: MarketViewProposalsDocument,
subscriptionQuery: MarketViewLiveProposalsDocument,
@@ -125,5 +79,4 @@ export const marketViewProposalsDataProvider = makeDataProvider<
getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) =>
subscriptionData.proposals,
getData: getMarketProposalsData,
getSubscriptionVariables: () => subscriptionVariables,
});
@@ -7,3 +7,4 @@ export * from './use-update-network-paramaters-toasts';
export * from './use-successor-market-proposal-details';
export * from './use-new-transfer-proposal-details';
export * from './use-cancel-transfer-proposal-details';
export * from './use-market-view-proposals';
@@ -0,0 +1,29 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketViewProposalsDataProvider } from '../proposals-data-provider';
import type * as Types from '@vegaprotocol/types';
export const useMarketViewProposals = ({
skip = false,
typename,
proposalType,
inState,
}: {
typename: Types.ProposalChange['__typename'];
skip?: boolean;
inState: Types.ProposalState;
proposalType?: Types.ProposalType;
}) => {
const variables = {
inState,
...(proposalType ? { proposalType } : null),
};
const { data } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables,
});
return (data || []).filter(
(item) => item.terms.change.__typename === typename
);
};
@@ -1,9 +1,9 @@
import { proposalsDataProvider } from '..';
import * as Schema from '@vegaprotocol/types';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useMemo } from 'react';
import first from 'lodash/first';
import { proposalsDataProvider } from '../proposals-data-provider';
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
import type { ProposalListFieldsFragment } from '..';
type UseUpdateProposalProps = {
id?: string;
@@ -58,7 +58,7 @@ type ThemeStore = {
setTheme: (theme?: Theme) => void;
};
const useThemeStore = create<ThemeStore>()((set) => ({
const useThemeStore = create<ThemeStore>((set) => ({
theme: getCurrentTheme(),
setTheme: (newTheme) => {
set((state) => {
+10 -54
View File
@@ -4,13 +4,10 @@ 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 = ({
@@ -19,60 +16,19 @@ export const TradesManager = ({
}: TradesContainerProps) => {
const update = useDealTicketFormValues((state) => state.updateAll);
const { data, error, load, pageInfo } = useDataProvider({
const { data, error } = 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 (
<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>
<TradesTable
rowData={data}
onClick={(price?: string) => {
update(marketId, { price });
}}
overlayNoRowsTemplate={error ? error.message : t('No trades')}
{...gridProps}
/>
);
};
@@ -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,22 +20,4 @@ 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 || (Array.isArray(data) && !data.length)) {
if (!data) {
return (
<div className="h-full flex items-center justify-center">
<div className="h-12 flex flex-col items-center">
+1 -1
View File
@@ -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.3.1';
export const DEFAULT_SNAP_VERSION = '0.2.0';
type GetSnapsResponse = Record<string, Snap>;