Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8461a0a0d5 | ||
|
|
80514a4adc | ||
|
|
1d39f81dfc | ||
|
|
e4f0cbc822 | ||
|
|
a9fd184647 | ||
|
|
f5868b6f1b | ||
|
|
e06ec8bb24 | ||
|
|
20e1522088 | ||
|
|
ae02b153f8 | ||
|
|
fee5f862a7 | ||
|
|
c25362b084 | ||
|
|
7c105d2c6b | ||
|
|
2dfcb02d1e | ||
|
|
a98ab775c3 | ||
|
|
097bf7c08c | ||
|
|
236e35a92b | ||
|
|
abf84b9d45 | ||
|
|
6d2a2ea0a0 | ||
|
|
6ac79e9f6d | ||
|
|
835ee64243 | ||
|
|
e7c3b5054c |
+1
-1
@@ -1,2 +1,2 @@
|
||||
* @vegaprotocol/frontend @vegaprotocol/frontend-qa
|
||||
* @vegaprotocol/frontend
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -133,7 +133,7 @@ export const ErrorBoundary = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const GHOST = (
|
||||
export const GHOST = (
|
||||
<svg
|
||||
width="56"
|
||||
height="85"
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
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,5 +1,6 @@
|
||||
export const Routes = {
|
||||
HOME: '/',
|
||||
RESTRICTED: '/restricted',
|
||||
TX: 'txs',
|
||||
BLOCKS: 'blocks',
|
||||
PARTIES: 'parties',
|
||||
|
||||
@@ -30,6 +30,7 @@ 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;
|
||||
@@ -356,6 +357,14 @@ export const routerConfig: Route[] = [
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.RESTRICTED,
|
||||
element: <RestrictedPage />,
|
||||
handle: {
|
||||
name: t('Restricted'),
|
||||
text: t('Restricted'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
|
||||
@@ -106,6 +106,7 @@ const Web3Container = ({
|
||||
useEthWithdrawApprovalsManager();
|
||||
return null;
|
||||
};
|
||||
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
@@ -239,6 +240,9 @@ 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({
|
||||
@@ -304,6 +308,14 @@ const AppContainer = () => {
|
||||
}
|
||||
}, [GIT_COMMIT_HASH, GIT_BRANCH, VEGA_ENV, telemetryOn]);
|
||||
|
||||
if (isRestricted) {
|
||||
return (
|
||||
<Router>
|
||||
<AppRouter />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ScrollToTop />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
|
||||
+80
-3
@@ -12,18 +12,19 @@ import {
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import { ProposalHeader, NewTransferSummary } 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'),
|
||||
@@ -31,6 +32,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
useNewTransferProposalDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
@@ -418,3 +420,78 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
+20
-5
@@ -237,7 +237,11 @@ export const ProposalHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const SuccessorCode = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
|
||||
@@ -254,7 +258,11 @@ const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
export const NewTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useNewTransferProposalDetails(proposalId);
|
||||
|
||||
@@ -265,14 +273,21 @@ const NewTransferSummary = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
{GovernanceTransferKindMapping[details.kind.__typename]}{' '}
|
||||
{t('transfer from')}{' '}
|
||||
<Lozenge>
|
||||
{truncateMiddle(details.source) || t(details.sourceType)}
|
||||
{details.source
|
||||
? truncateMiddle(details.source)
|
||||
: t(details.sourceType)}
|
||||
</Lozenge>{' '}
|
||||
{t('to')} <Lozenge>{truncateMiddle(details.destination)}</Lozenge>
|
||||
{t('to')}{' '}
|
||||
<Lozenge>
|
||||
{details.destination
|
||||
? truncateMiddle(details.destination)
|
||||
: t(details.destinationType)}
|
||||
</Lozenge>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CancelTransferSummary = ({
|
||||
export const CancelTransferSummary = ({
|
||||
proposalId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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;
|
||||
@@ -4,6 +4,7 @@ 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,8 +362,13 @@ const routerConfig = [
|
||||
element: <LazyDisclaimer name="Disclaimer" />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
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
|
||||
element: <NotFound name="NotFound" />,
|
||||
},
|
||||
...redirects,
|
||||
|
||||
@@ -7,6 +7,7 @@ const Routes = {
|
||||
PROPOSALS_REJECTED: '/proposals/rejected',
|
||||
PROTOCOL_UPGRADES: '/protocol-upgrades',
|
||||
NOT_PERMITTED: '/not-permitted',
|
||||
RESTRICTED: '/restricted',
|
||||
NOT_FOUND: '/not-found',
|
||||
CONTRACTS: '/contracts',
|
||||
TOKEN: '/token',
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
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,6 +86,7 @@ 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 marketId={marketId} />}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
>
|
||||
<TradingViews.orders.component filter={Filter.Open} />
|
||||
</Tab>
|
||||
@@ -136,7 +136,7 @@ const MainGrid = memo(
|
||||
<Tab
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu marketId={marketId} />}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
>
|
||||
<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 marketId={market?.id || ''} />
|
||||
<Menu />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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>;
|
||||
@@ -1,349 +0,0 @@
|
||||
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,7 +15,6 @@ 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,
|
||||
@@ -66,10 +65,7 @@ export const Portfolio = () => {
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel minSize={75}>
|
||||
<PortfolioGridChild>
|
||||
<Tabs storageKey="console-portfolio-top">
|
||||
<Tab id="account-history" name={t('Account history')}>
|
||||
<AccountHistoryContainer />
|
||||
</Tab>
|
||||
<Tabs storageKey="console-portfolio-top-1">
|
||||
<Tab
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { useUpdateProposal } from '@vegaprotocol/proposals';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export const LocalAssetDetailsDialog = () => {
|
||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||
const { data: proposal } = useUpdateProposal({
|
||||
id,
|
||||
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
|
||||
});
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
proposalId={proposal?.id ?? undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './asset-details-dialog';
|
||||
@@ -9,10 +9,36 @@ import { useMemo } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DateRange,
|
||||
calculateStartDate,
|
||||
} from '../../client-pages/portfolio/account-history-container';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
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 {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -30,12 +30,14 @@ 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([
|
||||
@@ -79,11 +81,12 @@ export const MarketLiquiditySupplied = ({
|
||||
});
|
||||
|
||||
const supplied = market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
new BigNumber(market?.suppliedStake)
|
||||
.multipliedBy(stakeToCcyVolume || 1)
|
||||
.toString(),
|
||||
assetDecimals
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
@@ -108,9 +111,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Supplied stake')}</span>
|
||||
<span>
|
||||
{market?.suppliedStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.suppliedStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.suppliedStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
@@ -119,9 +123,10 @@ export const MarketLiquiditySupplied = ({
|
||||
<span>{t('Target stake')}</span>
|
||||
<span>
|
||||
{market?.targetStake
|
||||
? addDecimalsFormatNumber(
|
||||
new BigNumber(market?.targetStake).toString(),
|
||||
assetDecimals
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
market?.targetStake,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
|
||||
@@ -21,6 +21,7 @@ const market = {
|
||||
} as unknown as Market;
|
||||
|
||||
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||
let mockDataMarketState: Market['state'] | null = null;
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||
@@ -43,6 +44,12 @@ jest.mock('@vegaprotocol/utils', () => ({
|
||||
let mockCandles = {};
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketState: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataMarketState,
|
||||
}
|
||||
: { data: undefined },
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
@@ -81,30 +88,6 @@ describe('MarketSuccessorBanner', () => {
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('successor market not in continuous mode', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
tradingMode: Types.MarketTradingMode.TRADING_MODE_NO_TRADING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('successor market is not active', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
state: Types.MarketState.STATE_PENDING,
|
||||
};
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(allUtils.getMarketExpiryDate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be displayed', () => {
|
||||
@@ -120,6 +103,17 @@ describe('MarketSuccessorBanner', () => {
|
||||
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||
});
|
||||
|
||||
it('no successor market data, market settled', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally successor volume', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
@@ -137,7 +131,9 @@ describe('MarketSuccessorBanner', () => {
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(screen.getByText('has 101.367 24h vol.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('has a 24h trading volume of 101.367')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally duration', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useMarketState,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
@@ -29,7 +30,9 @@ export const MarketSuccessorBanner = ({
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const { data: successorData } = useSuccessorMarket(market?.id);
|
||||
const { data: marketState } = useMarketState(market?.id);
|
||||
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
@@ -45,11 +48,6 @@ export const MarketSuccessorBanner = ({
|
||||
? intervalToDuration({ start: new Date(), end: expiry })
|
||||
: null;
|
||||
|
||||
const isInContinuesMode =
|
||||
successorData?.state === Types.MarketState.STATE_ACTIVE &&
|
||||
successorData?.tradingMode ===
|
||||
Types.MarketTradingMode.TRADING_MODE_CONTINUOUS;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorData?.id,
|
||||
});
|
||||
@@ -66,7 +64,7 @@ export const MarketSuccessorBanner = ({
|
||||
)
|
||||
: null;
|
||||
|
||||
if (isInContinuesMode && visible) {
|
||||
if (!loading && (isSettled || successorData) && visible) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
@@ -74,34 +72,47 @@ export const MarketSuccessorBanner = ({
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{t('This market has been succeeded')}
|
||||
</div>
|
||||
<div>
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}{' '}
|
||||
{t('The successor market')}{' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span> {t('has %s 24h vol.', [successorVolume])}</span>
|
||||
)}
|
||||
<div className="uppercase">
|
||||
{successorData
|
||||
? t('This market has been succeeded')
|
||||
: t('This market has been settled')}
|
||||
</div>
|
||||
{(duration || successorData) && (
|
||||
<div className="mt-1">
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in %s.', [
|
||||
formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
])}
|
||||
</span>
|
||||
)}
|
||||
{successorData && (
|
||||
<>
|
||||
{' '}
|
||||
{t('The successor market')}
|
||||
{!successorVolume ? ' is ' : ' '}
|
||||
<ExternalLink href={`/#/markets/${successorData?.id}`}>
|
||||
{successorData?.tradableInstrument.instrument.name}
|
||||
</ExternalLink>
|
||||
{successorVolume && (
|
||||
<span>
|
||||
{' '}
|
||||
{t('has a 24h trading volume of %s', [successorVolume])}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import type { NewMarketSuccessorFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
marketViewProposalsDataProvider,
|
||||
type NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
@@ -9,17 +12,20 @@ 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 proposals = useMarketViewProposals({
|
||||
const { data: proposals } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip: !marketId,
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
typename: 'NewMarket',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
});
|
||||
|
||||
const successors =
|
||||
|
||||
@@ -20,10 +20,13 @@ const marketMock = {
|
||||
},
|
||||
} as Market;
|
||||
|
||||
const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: { inState: Types.ProposalState.STATE_PASSED },
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -31,7 +34,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: 'first-id',
|
||||
id: '1',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -56,7 +59,7 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'second-id',
|
||||
id: '2',
|
||||
state: Types.ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: '2023-09-27T11:48:18Z',
|
||||
@@ -84,7 +87,99 @@ const proposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
},
|
||||
},
|
||||
};
|
||||
const mocks: MockedResponse[] = [proposalMock];
|
||||
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];
|
||||
|
||||
describe('MarketTerminationBanner', () => {
|
||||
beforeAll(() => {
|
||||
@@ -108,4 +203,28 @@ 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,12 +1,67 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { format, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMarketViewProposals } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { marketViewProposalsDataProvider } 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,
|
||||
@@ -15,57 +70,55 @@ export const MarketTerminationBanner = ({
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const skip = !market || !visible;
|
||||
const proposalsData = useMarketViewProposals({
|
||||
const { data: passedProposalsData } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip,
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
typename: 'UpdateMarketState',
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_PASSED,
|
||||
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
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 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 now = Date.now();
|
||||
const passedProposals = filterProposals(passedProposalsData, market.id, now);
|
||||
const openProposals = filterProposals(openProposalsData, market.id, now);
|
||||
|
||||
const enactmentDatetime = new Date(marketFound?.terms.enactmentDatetime);
|
||||
const name =
|
||||
marketFound?.terms.change.__typename === 'UpdateMarketState'
|
||||
? marketFound.terms.change.market.tradableInstrument.instrument.code
|
||||
: '';
|
||||
const name = market.tradableInstrument.instrument.code;
|
||||
if (!passedProposals.length && !openProposals.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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}`}
|
||||
>
|
||||
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 = (
|
||||
<>
|
||||
<div className="uppercase mb-1">
|
||||
{t('Trading on Market %s will stop on %s', [name, dayMonthDate])}
|
||||
{t('Trading on Market %s will stop on %s', [name, date])}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'You will no longer be able to hold a position on this market when it closes in %s.',
|
||||
[formattedDuration]
|
||||
[duration]
|
||||
)}{' '}
|
||||
{price &&
|
||||
assetSymbol &&
|
||||
@@ -74,8 +127,57 @@ export const MarketTerminationBanner = ({
|
||||
assetSymbol,
|
||||
])}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
</>
|
||||
);
|
||||
} 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={openProposals.length ? Intent.Warning : Intent.Info}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
data-testid={`termination-warning-banner-${market.id}`}
|
||||
>
|
||||
{content}
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,6 +56,7 @@ const MarketData = ({
|
||||
variables: {
|
||||
marketId: market.id,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
});
|
||||
|
||||
const marketData = data?.marketsData[0];
|
||||
@@ -70,6 +71,8 @@ const MarketData = ({
|
||||
|
||||
const marketTradingMode = marketData
|
||||
? marketData.marketTradingMode
|
||||
: market.data
|
||||
? market.data.marketTradingMode
|
||||
: market.tradingMode;
|
||||
|
||||
const mode = [
|
||||
@@ -95,7 +98,7 @@ const MarketData = ({
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<h3 className="flex items-baseline">
|
||||
<span className="text-sm lg:text-base text-ellipsis whitespace-nowrap overflow-hidden">
|
||||
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</span>
|
||||
{allProducts && productType && (
|
||||
|
||||
@@ -2,7 +2,10 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -36,6 +39,10 @@ describe('MarketSelector', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'a',
|
||||
@@ -56,7 +63,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'b',
|
||||
@@ -77,7 +87,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -91,7 +104,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'c',
|
||||
@@ -112,6 +128,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'cd',
|
||||
@@ -132,7 +152,10 @@ describe('MarketSelector', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const activeMarkets = markets.filter((m) => isMarketActive(m.state));
|
||||
const activeMarkets = markets.filter((m) =>
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
mockUseMarketList.mockReturnValue({
|
||||
data: markets,
|
||||
loading: false,
|
||||
|
||||
@@ -4,7 +4,10 @@ import { useMarketSelectorList } from './use-market-selector-list';
|
||||
import { isMarketActive } from '../../lib/utils';
|
||||
import { Product } from './product-selector';
|
||||
import { Sort } from './sort-dropdown';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import {
|
||||
createMarketFragment,
|
||||
createMarketsDataFragment,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import type { Filter } from './market-selector';
|
||||
@@ -31,22 +34,40 @@ describe('useMarketSelectorList', () => {
|
||||
|
||||
it('returns all markets active and suspended markets', () => {
|
||||
const markets = [
|
||||
createMarketFragment({ id: 'market-0' }),
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_SUSPENDED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_SUSPENDED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_CLOSED,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_CLOSED,
|
||||
}),
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-4',
|
||||
state: MarketState.STATE_PENDING,
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_PENDING,
|
||||
}),
|
||||
}),
|
||||
];
|
||||
mockUseMarketList.mockReturnValue({
|
||||
@@ -56,7 +77,8 @@ describe('useMarketSelectorList', () => {
|
||||
});
|
||||
const { result } = setup();
|
||||
const expectedFilteredMarkets = markets.filter((m) =>
|
||||
isMarketActive(m.state)
|
||||
// @ts-ignore candles get joined outside this type
|
||||
isMarketActive(m.data.marketState)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
data: markets,
|
||||
@@ -70,6 +92,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -90,6 +116,10 @@ describe('useMarketSelectorList', () => {
|
||||
// }),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -135,6 +165,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -148,6 +182,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -161,6 +199,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -174,6 +216,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
product: {
|
||||
@@ -238,6 +284,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'abc',
|
||||
@@ -247,6 +297,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'def',
|
||||
@@ -256,6 +310,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'defg',
|
||||
@@ -265,6 +323,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-3',
|
||||
// @ts-ignore candles get joined outside this type
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'ggg',
|
||||
@@ -333,11 +395,11 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -349,9 +411,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-1',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -363,9 +426,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-2',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -377,9 +441,10 @@ describe('useMarketSelectorList', () => {
|
||||
id: 'market-3',
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
// @ts-ignore data not on fragment
|
||||
data: {
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
markPrice: '1',
|
||||
},
|
||||
}),
|
||||
// @ts-ignore candles not on fragment
|
||||
candles: [
|
||||
{
|
||||
@@ -414,6 +479,10 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -426,6 +495,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -438,6 +511,10 @@ describe('useMarketSelectorList', () => {
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
// @ts-ignore actual fragment doesn't contain candles and is joined later
|
||||
candles: [
|
||||
{
|
||||
@@ -482,18 +559,30 @@ describe('useMarketSelectorList', () => {
|
||||
const markets = [
|
||||
createMarketFragment({
|
||||
id: 'market-0',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 3).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-1',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 1).toISOString(),
|
||||
},
|
||||
}),
|
||||
createMarketFragment({
|
||||
id: 'market-2',
|
||||
// @ts-ignore data not on fragment
|
||||
data: createMarketsDataFragment({
|
||||
marketState: MarketState.STATE_ACTIVE,
|
||||
}),
|
||||
marketTimestamps: {
|
||||
open: subDays(new Date(), 2).toISOString(),
|
||||
},
|
||||
|
||||
@@ -22,8 +22,12 @@ export const useMarketSelectorList = ({
|
||||
const markets = useMemo(() => {
|
||||
if (!data?.length) return [];
|
||||
const markets = data
|
||||
// only active
|
||||
.filter((m) => isMarketActive(m.state))
|
||||
// show only active markets, using m.data.marketState as this will be
|
||||
// data that will get refreshed when calling reload
|
||||
.filter((m) => {
|
||||
if (!m.data) return false;
|
||||
return isMarketActive(m.data.marketState);
|
||||
})
|
||||
// only selected product type
|
||||
.filter((m) => {
|
||||
if (
|
||||
|
||||
@@ -10,6 +10,19 @@ 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]: [
|
||||
@@ -43,6 +56,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
if (!pubKey) {
|
||||
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||
}
|
||||
const noRowsMessage = resolveNoRowsMessage(filter);
|
||||
|
||||
return (
|
||||
<OrderListManager
|
||||
@@ -56,6 +70,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
gridProps={gridStoreCallbacks}
|
||||
noRowsMessage={noRowsMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { VegaConnectDialog, ViewAsDialog } from '@vegaprotocol/wallet';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import {
|
||||
@@ -10,9 +6,9 @@ import {
|
||||
} from '@vegaprotocol/web3';
|
||||
import { WelcomeDialog } from '../components/welcome-dialog';
|
||||
import { RiskMessage } from '../components/welcome-dialog';
|
||||
import { LocalAssetDetailsDialog as AssetDetailsDialog } from '../components/asset-details-dialog';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<>
|
||||
<VegaConnectDialog
|
||||
@@ -20,12 +16,7 @@ const DialogsContainer = () => {
|
||||
riskMessage={<RiskMessage />}
|
||||
/>
|
||||
<ViewAsDialog connector={Connectors.view} />
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
<AssetDetailsDialog />
|
||||
<WelcomeDialog />
|
||||
<Web3ConnectUncontrolledDialog />
|
||||
<WithdrawalApprovalDialogContainer />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const SSRLoader = () => {
|
||||
const randomDelay = () => {
|
||||
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
|
||||
};
|
||||
import { pseudoRandom } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export const SSRLoader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -38,10 +38,9 @@ export const SSRLoader = () => {
|
||||
width: 10,
|
||||
height: 10,
|
||||
animation: 'flickering 0.4s linear alternate infinite',
|
||||
animationDelay: `-${randomDelay()}s`,
|
||||
animationDelay: `-${generate()}s`,
|
||||
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
|
||||
background: 'black',
|
||||
opacity: Math.random() > 0.5 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
|
||||
import { useProposalNotificationToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useUpdateNetworkParametersToasts();
|
||||
useProposalNotificationToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { create } from 'zustand';
|
||||
import { AssetDetailsTable } from './asset-details-table';
|
||||
import { AssetProposalNotification } from '@vegaprotocol/proposals';
|
||||
import { AssetProposalNotification } from './asset-proposal-notification';
|
||||
import { useAssetDataProvider } from './asset-data-provider';
|
||||
|
||||
export type AssetDetailsDialogStore = {
|
||||
@@ -47,6 +47,7 @@ export interface AssetDetailsDialogProps {
|
||||
open: boolean;
|
||||
onChange: (open: boolean) => void;
|
||||
asJson?: boolean;
|
||||
proposalId?: string;
|
||||
}
|
||||
|
||||
export const AssetDetailsDialog = ({
|
||||
@@ -55,6 +56,7 @@ export const AssetDetailsDialog = ({
|
||||
open,
|
||||
onChange,
|
||||
asJson = false,
|
||||
proposalId,
|
||||
}: AssetDetailsDialogProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
@@ -62,7 +64,7 @@ export const AssetDetailsDialog = ({
|
||||
|
||||
const content = asset ? (
|
||||
<div className="my-2">
|
||||
<AssetProposalNotification assetId={asset.id} />
|
||||
<AssetProposalNotification proposalId={proposalId} />
|
||||
{asJson ? (
|
||||
<div className="pr-8">
|
||||
<SyntaxHighlighter size="smaller" data={asset} />
|
||||
|
||||
@@ -57,7 +57,16 @@ export const rows: Rows = [
|
||||
key: AssetDetail.ID,
|
||||
label: t('ID'),
|
||||
tooltip: '',
|
||||
value: (asset) => truncateMiddle(asset.id),
|
||||
value: (asset) => (
|
||||
<>
|
||||
{truncateMiddle(asset.id)}{' '}
|
||||
<CopyWithTooltip text={asset.id}>
|
||||
<button title={t('Copy id to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.TYPE,
|
||||
|
||||
+4
-10
@@ -1,24 +1,18 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { ExternalLink, Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateProposal } from '../lib';
|
||||
|
||||
type AssetProposalNotificationProps = {
|
||||
assetId?: string;
|
||||
proposalId?: string;
|
||||
};
|
||||
export const AssetProposalNotification = ({
|
||||
assetId,
|
||||
proposalId,
|
||||
}: AssetProposalNotificationProps) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const { data: proposal } = useUpdateProposal({
|
||||
id: assetId,
|
||||
proposalType: Schema.ProposalType.TYPE_UPDATE_ASSET,
|
||||
});
|
||||
|
||||
if (proposal) {
|
||||
if (proposalId) {
|
||||
const proposalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', proposal.id || '')
|
||||
TOKEN_PROPOSAL.replace(':id', proposalId || '')
|
||||
);
|
||||
const message = (
|
||||
<>
|
||||
@@ -63,7 +63,9 @@ export const CandlesChartContainer = ({
|
||||
overlays,
|
||||
studies,
|
||||
notEnoughDataText: (
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">
|
||||
{t('No open orders')}
|
||||
</span>
|
||||
),
|
||||
initialNumCandlesToDisplay: candlesCount,
|
||||
studySize: STUDY_SIZE,
|
||||
|
||||
@@ -14,16 +14,17 @@ export const MarketProductPill = ({
|
||||
}: {
|
||||
productType?: ProductType;
|
||||
}) => {
|
||||
if (!productType) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
productType && (
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
)
|
||||
<Pill
|
||||
size="xxs"
|
||||
className="uppercase ml-0.5"
|
||||
title={ProductTypeMapping[productType]}
|
||||
>
|
||||
{ProductTypeShortName[productType]}
|
||||
</Pill>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
REDUCE_ONLY_TOOLTIP,
|
||||
stopSubmit,
|
||||
getNotionalSize,
|
||||
getAssetUnit,
|
||||
getBaseQuoteUnit,
|
||||
} 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 = getAssetUnit(
|
||||
const assetUnit = getBaseQuoteUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ const getDefaultValues = (
|
||||
...storedValues,
|
||||
});
|
||||
|
||||
export const getAssetUnit = (tags?: string[] | null) =>
|
||||
export const getBaseQuoteUnit = (tags?: string[] | null) =>
|
||||
tags
|
||||
?.find((tag) => tag.startsWith('base:') || tag.startsWith('ticker:'))
|
||||
?.replace(/^[^:]*:/, '');
|
||||
@@ -273,9 +273,7 @@ export const DealTicket = ({
|
||||
|
||||
const assetSymbol = getAsset(market).symbol;
|
||||
|
||||
const assetUnit = getAssetUnit(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
);
|
||||
const assetUnit = getQuoteName(market);
|
||||
|
||||
const summaryError = useMemo(() => {
|
||||
if (!pubKey) {
|
||||
|
||||
@@ -238,7 +238,7 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
volumeFormat={volumeFormat}
|
||||
priceFormat={priceFormat}
|
||||
notEnoughDataText={
|
||||
<span className="text-xs text-center">{t('No data')}</span>
|
||||
<span className="text-xs text-center">{t('No open orders')}</span>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -11,14 +11,8 @@ 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;
|
||||
|
||||
@@ -238,7 +238,7 @@ export const Orderbook = ({
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0">
|
||||
<Splash>{t('No data')}</Splash>
|
||||
<Splash>{t('No open orders')}</Splash>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -3,12 +3,12 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
export type MarketsDataFieldsFragment = { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } };
|
||||
|
||||
export type MarketsDataQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
export type MarketsDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', bestBidPrice: string, bestOfferPrice: string, markPrice: string, trigger: Types.AuctionTrigger, staticMidPrice: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, indicativeVolume: string, indicativePrice: string, bestStaticBidPrice: string, bestStaticOfferPrice: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, market: { __typename?: 'Market', id: string } } | null } }> } | null };
|
||||
|
||||
export const MarketsDataFieldsFragmentDoc = gql`
|
||||
fragment MarketsDataFields on MarketData {
|
||||
@@ -20,6 +20,7 @@ export const MarketsDataFieldsFragmentDoc = gql`
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -126,6 +126,16 @@ export const marketTradingModeProvider = makeDerivedDataProvider<
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketTradingMode
|
||||
);
|
||||
|
||||
export const marketStateProvider = makeDerivedDataProvider<
|
||||
MarketDataFieldsFragment['marketState'] | undefined,
|
||||
never,
|
||||
MarketDataQueryVariables
|
||||
>(
|
||||
[marketDataProvider],
|
||||
(parts, variables, prevData) =>
|
||||
(parts[0] as ReturnType<typeof getData>)?.marketState
|
||||
);
|
||||
|
||||
export const fundingRateProvider = makeDerivedDataProvider<
|
||||
string,
|
||||
never,
|
||||
@@ -176,3 +186,11 @@ export const useMarketTradingMode = (marketId?: string, skip?: boolean) => {
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMarketState = (marketId?: string, skip?: boolean) => {
|
||||
return useDataProvider({
|
||||
dataProvider: marketStateProvider,
|
||||
variables: { marketId: marketId || '' },
|
||||
skip: skip || !marketId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ fragment MarketsDataFields on MarketData {
|
||||
markPrice
|
||||
trigger
|
||||
staticMidPrice
|
||||
marketState
|
||||
marketTradingMode
|
||||
indicativeVolume
|
||||
indicativePrice
|
||||
|
||||
@@ -34,6 +34,7 @@ export const createMarketsDataFragment = (
|
||||
__typename: 'Market',
|
||||
},
|
||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||
staticMidPrice: '0',
|
||||
indicativePrice: '0',
|
||||
bestStaticBidPrice: '0',
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useVegaTransactionStore } from '@vegaprotocol/web3';
|
||||
import { useHasAmendableOrder } from '../../order-hooks';
|
||||
|
||||
export const OpenOrdersMenu = ({ marketId }: { marketId: string }) => {
|
||||
export const OpenOrdersMenu = () => {
|
||||
const { isReadOnly } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((state) => state.create);
|
||||
const hasAmendableOrder = useHasAmendableOrder(marketId);
|
||||
const hasAmendableOrder = useHasAmendableOrder();
|
||||
|
||||
if (isReadOnly) {
|
||||
return null;
|
||||
|
||||
@@ -239,11 +239,11 @@ export const hasActiveOrderProvider = makeDerivedDataProvider<
|
||||
export const hasAmendableOrderProvider = makeDerivedDataProvider<
|
||||
boolean,
|
||||
never,
|
||||
{ partyId: string; marketId?: string }
|
||||
{ partyId: string }
|
||||
>([activeOrdersProvider], (parts) => {
|
||||
const activeOrders = parts[0] as ReturnType<typeof getData>;
|
||||
const hasAmendableOrder = activeOrders.some(
|
||||
(order) => !(order.liquidityProvision || order.peggedOrder)
|
||||
(order) => !order.liquidityProvision
|
||||
);
|
||||
return hasAmendableOrder;
|
||||
});
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { render, screen, act } from '@testing-library/react';
|
||||
import { OrderListManager } from './order-list-manager';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { OrderListManagerProps } from './order-list-manager';
|
||||
import { OrderListManager, Filter } 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';
|
||||
|
||||
// @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 generateJsx = (props: Partial<OrderListManagerProps> | null = null) => {
|
||||
const pubKey = '0x123';
|
||||
return (
|
||||
<MockedProvider>
|
||||
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} />
|
||||
<OrderListManager partyId={pubKey} isReadOnly={false} {...props} />
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
);
|
||||
@@ -33,9 +28,46 @@ describe('OrderListManager', () => {
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
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(),
|
||||
});
|
||||
expect(await screen.findByText('OrderList')).toBeInTheDocument();
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
render(
|
||||
generateJsx({ filter: Filter.Closed, noRowsMessage: 'no rows message' })
|
||||
);
|
||||
|
||||
expect(screen.getByText('no rows message')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { OrderListTable } from '../order-list/order-list';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { ordersWithMarketProvider } from '../order-data-provider/order-data-provider';
|
||||
@@ -11,6 +12,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -25,6 +27,7 @@ export interface OrderListManagerProps {
|
||||
isReadOnly: boolean;
|
||||
filter?: Filter;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
noRowsMessage?: string;
|
||||
}
|
||||
|
||||
export const OrderListManager = ({
|
||||
@@ -34,6 +37,7 @@ export const OrderListManager = ({
|
||||
isReadOnly,
|
||||
filter,
|
||||
gridProps,
|
||||
noRowsMessage,
|
||||
}: OrderListManagerProps) => {
|
||||
const gridRef = useRef<AgGridReact | null>(null);
|
||||
const [editOrder, setEditOrder] = useState<Order | null>(null);
|
||||
@@ -56,6 +60,29 @@ export const OrderListManager = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
} else {
|
||||
gridRef.current?.api?.hideOverlay();
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const cancel = useCallback(
|
||||
(order: Order) => {
|
||||
if (!order.market) return;
|
||||
@@ -69,6 +96,10 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
@@ -82,8 +113,9 @@ export const OrderListManager = ({
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No orders')}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
</div>
|
||||
{editOrder && (
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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,6 +46,12 @@ 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] }));
|
||||
});
|
||||
@@ -184,6 +190,7 @@ 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({
|
||||
@@ -209,6 +216,7 @@ 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({
|
||||
@@ -233,6 +241,7 @@ 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,
|
||||
@@ -251,6 +260,7 @@ 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';
|
||||
import { Filter } from '../order-list-manager/order-list-manager';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
|
||||
const defaultColDef = {
|
||||
|
||||
@@ -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 = (marketId?: string) => {
|
||||
export const useHasAmendableOrder = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const [hasAmendableOrder, setHasAmendableOrder] = useState(false);
|
||||
const update = useCallback(({ data }: { data: boolean | null }) => {
|
||||
@@ -15,7 +15,6 @@ export const useHasAmendableOrder = (marketId?: string) => {
|
||||
update,
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
marketId,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './asset-proposal-notification';
|
||||
export * from './market-proposal-notification';
|
||||
export * from './proposals-list';
|
||||
export * from './protocol-upgrade-countdown';
|
||||
|
||||
@@ -319,6 +319,7 @@ fragment NewAssetFields on NewAsset {
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
__typename
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
@@ -488,3 +489,39 @@ subscription MarketViewLiveProposals {
|
||||
...MarketViewProposalFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment NotificationProposalsFields on Proposal {
|
||||
id
|
||||
state
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
__typename
|
||||
... on UpdateMarketState {
|
||||
...UpdateMarketStateFields
|
||||
}
|
||||
... on NewMarket {
|
||||
...NewMarketSuccessorFields
|
||||
}
|
||||
... on UpdateMarket {
|
||||
...UpdateMarketFields
|
||||
}
|
||||
... on NewAsset {
|
||||
...NewAssetFields
|
||||
}
|
||||
... on UpdateAsset {
|
||||
...UpdateAssetFields
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
...UpdateNetworkParameterFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription NotificationProposals {
|
||||
proposals {
|
||||
...NotificationProposalsFields
|
||||
}
|
||||
}
|
||||
|
||||
+77
-4
File diff suppressed because one or more lines are too long
@@ -1,2 +1,5 @@
|
||||
export * from './proposals-data-provider';
|
||||
export {
|
||||
marketViewProposalsDataProvider,
|
||||
proposalsDataProvider,
|
||||
} from './proposals-data-provider';
|
||||
export * from './__generated__/Proposals';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
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,5 +1,7 @@
|
||||
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,
|
||||
@@ -8,6 +10,7 @@ import type {
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables,
|
||||
} from './__generated__/Proposals';
|
||||
import {
|
||||
MarketViewLiveProposalsDocument,
|
||||
@@ -42,21 +45,61 @@ export const proposalsDataProvider = makeDataProvider<
|
||||
errors.every((e) => e.message.match(/failed to get asset for ID/)),
|
||||
});
|
||||
|
||||
const update = (
|
||||
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
|
||||
> = (
|
||||
data: MarketViewProposalFieldsFragment[] | null,
|
||||
delta: MarketViewProposalFieldsFragment
|
||||
delta: MarketViewProposalFieldsFragment,
|
||||
reload,
|
||||
variables
|
||||
) => {
|
||||
const updateData = produce(data || [], (draft) => {
|
||||
const { id } = delta;
|
||||
const index = draft.findIndex((item) => item.id === id);
|
||||
const match = matchFilter(delta, variables);
|
||||
if (index === -1) {
|
||||
draft.unshift(delta);
|
||||
if (match) {
|
||||
draft.unshift(delta);
|
||||
}
|
||||
} else {
|
||||
const currNode = draft[index];
|
||||
draft[index] = {
|
||||
...currNode,
|
||||
...delta,
|
||||
};
|
||||
if (match) {
|
||||
draft[index] = delta;
|
||||
} else {
|
||||
draft.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
return updateData;
|
||||
@@ -66,12 +109,15 @@ const getMarketProposalsData = (
|
||||
responseData: MarketViewProposalsQuery | null
|
||||
) => removePaginationWrapper(responseData?.proposalsConnection?.edges) || [];
|
||||
|
||||
const subscriptionVariables: MarketViewLiveProposalsSubscriptionVariables = {};
|
||||
|
||||
export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
MarketViewProposalsQuery,
|
||||
MarketViewProposalFieldsFragment[],
|
||||
MarketViewLiveProposalsSubscription,
|
||||
MarketViewProposalFieldsFragment,
|
||||
MarketViewProposalsQueryVariables
|
||||
MarketViewProposalsQueryVariables,
|
||||
MarketViewLiveProposalsSubscriptionVariables
|
||||
>({
|
||||
query: MarketViewProposalsDocument,
|
||||
subscriptionQuery: MarketViewLiveProposalsDocument,
|
||||
@@ -79,4 +125,5 @@ export const marketViewProposalsDataProvider = makeDataProvider<
|
||||
getDelta: (subscriptionData: MarketViewLiveProposalsSubscription) =>
|
||||
subscriptionData.proposals,
|
||||
getData: getMarketProposalsData,
|
||||
getSubscriptionVariables: () => subscriptionVariables,
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ export * from './use-proposal-event';
|
||||
export * from './use-vega-transaction';
|
||||
export * from './use-proposal-submit';
|
||||
export * from './use-update-proposal';
|
||||
export * from './use-update-network-paramaters-toasts';
|
||||
export * from './use-proposal-notification-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';
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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
|
||||
);
|
||||
};
|
||||
+10
-10
@@ -5,21 +5,21 @@ import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
PROPOSAL_STATES_TO_TOAST,
|
||||
useUpdateNetworkParametersToasts,
|
||||
} from './use-update-network-paramaters-toasts';
|
||||
useProposalNotificationToasts,
|
||||
} from './use-proposal-notification-toasts';
|
||||
import type {
|
||||
UpdateNetworkParameterProposalFragment,
|
||||
OnUpdateNetworkParametersSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
import { NotificationProposalsDocument } from '../proposals-data-provider/__generated__/Proposals';
|
||||
|
||||
const render = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
|
||||
return renderHook(() => useProposalNotificationToasts(), { wrapper });
|
||||
};
|
||||
|
||||
const generateUpdateNetworkParametersProposal = (
|
||||
@@ -51,7 +51,7 @@ const clear = () => {
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useUpdateNetworkParametersToasts', () => {
|
||||
describe('useProposalNotificationToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
query: NotificationProposalsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -89,7 +89,7 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
query: NotificationProposalsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -117,7 +117,7 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
query: NotificationProposalsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -142,7 +142,7 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
{
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
__typename: 'NewSpotMarket',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -150,7 +150,7 @@ describe('useUpdateNetworkParametersToasts', () => {
|
||||
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
query: NotificationProposalsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
@@ -0,0 +1,200 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ProposalStateMapping, MarketUpdateType } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { NotificationProposalsFieldsFragment } from '../proposals-data-provider';
|
||||
import { useNotificationProposalsSubscription } from '../proposals-data-provider';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
|
||||
const PROPOSAL_TYPES_TO_TOAST = [
|
||||
'UpdateNetworkParameter',
|
||||
'NewAsset',
|
||||
'NewMarket',
|
||||
'UpdateAsset',
|
||||
'UpdateMarket',
|
||||
'UpdateMarketState',
|
||||
];
|
||||
|
||||
type Proposal = NotificationProposalsFieldsFragment;
|
||||
|
||||
const NotificationProposalToastContent = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { data: assets } = useAssetsDataProvider();
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change;
|
||||
let pretitle = 'New';
|
||||
let content: ReactNode = null;
|
||||
switch (change.__typename) {
|
||||
case 'UpdateNetworkParameter':
|
||||
pretitle = t('Network change');
|
||||
content = (
|
||||
<p className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 'NewMarket':
|
||||
pretitle = t('New market');
|
||||
content = (
|
||||
<p>
|
||||
{t('New market ')}
|
||||
<span className="break-all">{change.instrument.name}</span>
|
||||
{change.successorConfiguration?.parentMarketId && (
|
||||
<span>
|
||||
{' '}
|
||||
{t('successor of %', [
|
||||
change.successorConfiguration.parentMarketId,
|
||||
])}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 'UpdateMarket':
|
||||
pretitle = t('Update market');
|
||||
content = (
|
||||
<p>
|
||||
{t('Market ')}
|
||||
<span className="break-all">
|
||||
{change.updateMarketConfiguration.instrument.code}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 'UpdateMarketState':
|
||||
if (
|
||||
change.updateType ===
|
||||
MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE
|
||||
) {
|
||||
pretitle = t('Market termination');
|
||||
} else if (
|
||||
change.updateType === MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND
|
||||
) {
|
||||
pretitle = t('Market suspension');
|
||||
} else {
|
||||
pretitle = t('Market resume');
|
||||
}
|
||||
content = (
|
||||
<p>
|
||||
{t('Market ')}
|
||||
<span className="break-all">
|
||||
{change.market.tradableInstrument.instrument.name}
|
||||
</span>
|
||||
{change.price && (
|
||||
<span> {t('Final price will be %s', [change.price])}</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 'NewAsset':
|
||||
pretitle = t('New asset');
|
||||
content = (
|
||||
<p>
|
||||
<span className="break-all">
|
||||
{change.name} ({change.symbol})
|
||||
</span>
|
||||
{', '}
|
||||
{t('type %s', [
|
||||
change.source.__typename === 'ERC20' ? 'ERC20' : 'Builtin asset',
|
||||
])}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
pretitle = t('Update asset');
|
||||
content = (
|
||||
<p>
|
||||
<span className="break-all">
|
||||
{assets?.find((asset) => asset.id === change.assetId)?.name ??
|
||||
change.assetId}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
const title = t('%s proposal %s', [
|
||||
pretitle,
|
||||
ProposalStateMapping[proposal.state].toLowerCase(),
|
||||
]);
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading>{title}</ToastHeading>
|
||||
{content}
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProposalNotificationToasts = () => {
|
||||
const setToast = useToasts((store) => store.setToast);
|
||||
const remove = useToasts((store) => store.remove);
|
||||
const hasToast = useToasts((store) => store.hasToast);
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `proposal-notification-${proposal.id}`;
|
||||
return {
|
||||
id,
|
||||
intent: Intent.Warning,
|
||||
content: <NotificationProposalToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
const displayToast = (toast: Toast) => {
|
||||
if (!hasToast(toast.id)) {
|
||||
setToast(toast);
|
||||
}
|
||||
};
|
||||
|
||||
return useNotificationProposalsSubscription({
|
||||
onData: ({ data }) => {
|
||||
// note proposals is poorly named, it is actually a single proposal
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal) return;
|
||||
if (!PROPOSAL_TYPES_TO_TOAST.includes(proposal.terms.change.__typename)) {
|
||||
return;
|
||||
}
|
||||
// if one of the following states show a toast
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
displayToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
|
||||
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
const CLOSE_AFTER = 0;
|
||||
type Proposal = UpdateNetworkParameterProposalFragment;
|
||||
|
||||
const UpdateNetworkParameterToastContent = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change as UpdateNetworkParameter;
|
||||
const title = t('Network change proposal %s').replace(
|
||||
'%s',
|
||||
ProposalStateMapping[proposal.state].toLowerCase()
|
||||
);
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading>{title}</ToastHeading>
|
||||
<p className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateNetworkParametersToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `update-network-param-proposal-${proposal.id}`;
|
||||
return {
|
||||
id: `update-network-param-proposal-${proposal.id}`,
|
||||
intent: Intent.Warning,
|
||||
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
return useOnUpdateNetworkParametersSubscription({
|
||||
onData: ({ data }) => {
|
||||
// note proposals is poorly named, it is actually a single proposal
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal) return;
|
||||
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
|
||||
|
||||
// if one of the following states show a toast
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
setToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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 type { ProposalListFieldsFragment } from '..';
|
||||
import { proposalsDataProvider } from '../proposals-data-provider';
|
||||
import type { ProposalListFieldsFragment } from '../proposals-data-provider';
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { render, act, screen } from '@testing-library/react';
|
||||
import { AsyncRenderer } from './async-renderer';
|
||||
|
||||
describe('AsyncRenderer', () => {
|
||||
const reload = jest.fn();
|
||||
it('timeout error should render button', async () => {
|
||||
const reload = jest.fn();
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
@@ -20,4 +20,22 @@ describe('AsyncRenderer', () => {
|
||||
});
|
||||
expect(reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors should be handled properly', async () => {
|
||||
const message = 'Node has been collapsed';
|
||||
await act(() => {
|
||||
render(
|
||||
<AsyncRenderer
|
||||
reload={reload}
|
||||
error={new Error(message)}
|
||||
loading={false}
|
||||
data={[]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(`Something went wrong: ${message}`)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AsyncRenderer<T = object>({
|
||||
reload,
|
||||
}: AsyncRendererProps<T>) {
|
||||
if (error) {
|
||||
if (!data) {
|
||||
if (!data || (Array.isArray(data) && !data.length)) {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="h-12 flex flex-col items-center">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo } from 'react';
|
||||
import styles from './loader.module.scss';
|
||||
|
||||
const pseudoRandom = (seed: number) => {
|
||||
export const pseudoRandom = (seed: number) => {
|
||||
let value = seed;
|
||||
return () => {
|
||||
value = (value * 16807) % 2147483647;
|
||||
@@ -10,6 +9,8 @@ const pseudoRandom = (seed: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
const generate = pseudoRandom(1);
|
||||
|
||||
export interface LoaderProps {
|
||||
size?: 'small' | 'large';
|
||||
forceTheme?: 'dark' | 'light';
|
||||
@@ -27,8 +28,6 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
|
||||
size === 'small' ? 'w-[15px] h-[15px]' : 'w-[50px] h-[50px]';
|
||||
const items = size === 'small' ? 9 : 25;
|
||||
|
||||
const generate = useMemo(() => pseudoRandom(1), []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center" data-testid="loader">
|
||||
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||
|
||||
@@ -41,7 +41,7 @@ const ethereumRequest = <T>(args: RequestArguments): Promise<T> => {
|
||||
|
||||
export const LOCAL_SNAP_ID = 'local:http://localhost:8080';
|
||||
export const DEFAULT_SNAP_ID = 'npm:@vegaprotocol/snap';
|
||||
export const DEFAULT_SNAP_VERSION = '0.2.0';
|
||||
export const DEFAULT_SNAP_VERSION = '0.3.1';
|
||||
|
||||
type GetSnapsResponse = Record<string, Snap>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user