Compare commits

..
Author SHA1 Message Date
Mikołaj Młodzikowski 89031bf826 feat(ci): append ipfs related information to release body (#3818) 2023-05-18 14:51:53 +02:00
dexturr 61501d4d14 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-18 12:10:20 +00:00
Madalina Raicu f8f3ce7df9 fix: no oracle for this market banner 2023-05-18 13:00:17 +01:00
Maciek f7aab5a78a feat(ui-toolkit,trading): allow toast box configuration (#3811) 2023-05-18 12:53:36 +01:00
Bartłomiej Głownia 2904975fd3 feat(trading): highlight negative oracle statuses in market lists (#3816) 2023-05-18 13:22:54 +02:00
Mikołaj Młodzikowski 4f55c946f0 feat(ci): add pipeline for rollback console deployments (#3817) 2023-05-18 10:11:41 +00:00
Matthew Russell de637fee13 chore(trading): show markets in opening auction in market selector (#3814) 2023-05-18 09:49:44 +01:00
dexturr 69c6db8ec2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-18 06:09:07 +00:00
Matthew Russell 8dc465c1d1 feat(trading): additional row actions for tables (#3600) 2023-05-17 21:05:53 -07:00
dexturr 0be934edca chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-18 00:16:39 +00:00
Matthew Russell 653688c1dd fix(governance): clear wallet state if user changes to a non existent party (#3813) 2023-05-17 19:25:14 +00:00
dexturr 34400be18a chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 18:09:58 +00:00
Sam Keen d4a0a4a42c fix(governance): updated page titles (#3810) 2023-05-17 16:16:19 +00:00
Joe Tsang fabf643ce7 chore(ci): fix nightly test runner (#3804) 2023-05-17 16:21:10 +01:00
Sam Keen 45e210092c fix(governance): ensure no negative overstaked penalty (#3794) 2023-05-17 14:41:55 +00:00
Ciaran McGhie 1dc3b3fd05 fix(ui-toolkit): add keys to healthbar segments (#3797) 2023-05-17 15:14:11 +01:00
Sam Keen 0a80e3e39d fix(governance): avoid unknown pubkey causing validator page error (#3755) 2023-05-17 13:21:00 +00:00
Sam Keen 7b8d9dc94b feat(governance): validator tables refocused on stake (#3780) 2023-05-17 14:14:23 +01:00
Daniel c0320c2468 feat(ci): update DNS for mainnet console when new version is released (#3783) 2023-05-17 14:45:06 +02:00
Sam Keen 11173d6614 fix(governance): point to correct data for displayed LP rewards (#3800) 2023-05-17 12:20:51 +00:00
dexturr e3324331e2 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 12:11:33 +00:00
Maciek 0a6e3cb6f5 chore(trading): refactor docs links resolver (#3782) 2023-05-17 12:10:31 +02:00
Sam Keen 68ede90609 feat(3779): proposal terms in table, full proposal json, description … (#3793) 2023-05-17 08:23:25 +00:00
daro-maj 09778a5d3f test(trading): pegged orders e2e tests (#3787) 2023-05-17 09:54:19 +02:00
MaciekandMatthew Russell 59993e3ea6 chore(trading): suppress sentry logs when eth fails (#3726)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-05-16 23:31:53 -07:00
dexturr 54161a6dba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 06:07:24 +00:00
Matthew Russell 26f524ea07 feat(trading): sidebar market list adjustments (#3792) 2023-05-16 18:45:17 -07:00
dexturr db13e6f21d chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-17 00:17:38 +00:00
dexturr 4a679813ba chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 18:07:35 +00:00
Matthew Russell a4279d5b5e feat(trading): sidebar market list (#3776) 2023-05-16 16:57:36 +00:00
m.ray 1a274a67c3 feat(trading): oracle banner check termination and settlement data oracles (#3707) 2023-05-16 15:12:22 +02:00
dexturr 1e9ff31fd7 chore: update tranches
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-05-16 12:08:54 +00:00
362 changed files with 7138 additions and 4151 deletions
@@ -0,0 +1,74 @@
name: After Release
on:
release:
types: [published]
jobs:
after-release:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: resolve ipfs hashes for release
run: |
echo "Name: ${{ github.event.release.name }} Description: ${{ github.event.release.body }}"
until docker pull vegaprotocol/trading:${{ github.event.release.name }}; do
echo "Image not pushed yet, waiting 60 seconds"
sleep 60
done
docker run --rm vegaprotocol/trading:${{ github.event.release.name }} cat /ipfs-hash > ipfs-hash
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
echo IPFS_V0=$(cat ipfs-hash) >> $GITHUB_ENV
echo IPFS_V1=$(ipfs cid format -v 1 -b base32 $(cat ipfs-hash)) >> $GITHUB_ENV
- name: Edit Release
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
body: |
${{ github.event.release.body }}
---
# Deployments
* https://explorer.vega.xyz
* https://governance.vega.xyz
# IPFS releases
Tye IPFS hash of this release of the Trading app is:
CIDv0: ${{ env.IPFS_V0 }}
CIDv1: ${{ env.IPFS_V1 }}
You can always access the latest IPFS release by visiting [vega.trading](https://vega.trading).
You can also access Trading directly from an IPFS gateway.
BEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).
Your settings are not remembered across different URLs.
IPFS gateways:
https://${{ env.IPFS_V1 }}.dweb.link/
https://${{ env.IPFS_V1 }}.cf-ipfs.com/
ipfs://${{ env.IPFS_V0 }}/
+2 -2
View File
@@ -20,7 +20,7 @@ jobs:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 40
timeout-minutes: 60
steps:
# Checks if skip cache was requested
- name: Set skip-nx-cache flag
@@ -98,4 +98,4 @@ jobs:
if: ${{ failure() }}
with:
name: test-report-${{ matrix.project }}
path: frontend-monorepo/apps/trading-e2e/cypress/reports
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports
+25 -32
View File
@@ -170,12 +170,6 @@ jobs:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Add ipfs hash to release
uses: softprops/action-gh-release@v1
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
with:
files: ${{ matrix.app }}-ipfs-hash
- name: Trigger fleek deployment
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
run: |
@@ -191,37 +185,36 @@ jobs:
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Checkout vega.xyz
- name: Update vega.trading DNS to redirect to the new console
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
uses: actions/checkout@v3
with:
repository: vegaprotocol/vega.xyz
path: './vega-xyz'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update hash on interstitial page
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
cd vega-xyz
./interstital-allow-update.sh ${new_hash}
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
git add interstitial-allow.json netlify.toml
commit_msg="feat(ci): Update CID for console release @ ${{ github.ref }}"
git commit -m "${commit_msg}"
git push origin main
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
# branch_name=${{ github.ref_name }}-hash-update
# git checkout -b "${branch_name}"
# git add interstitial-allow.json netlify.toml
# git push -u origin "${branch_name}"
# pr_url="$(gh pr create --title "${commit_msg}" --body 'update ipfs hash for console @ ${{ github.ref }}')"
# echo $pr_url
# gh pr merge --admin --auto $pr_url
# Generate console URL
new_console_url_type=ipfs
# new_console_url_type=ipns
new_console_url_domain=cf-ipfs.com
# new_console_url_domain=dweb.link
new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=vega.trading
dnsimple_record_id=44409591
# see: https://dnsimple.com/a/84895/domains/vega.trading/records/44409591/edit
curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
-d "{
\"content\": \"${new_console_url}\"
}" \
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
+77
View File
@@ -0,0 +1,77 @@
name: 'Rollback console'
on:
workflow_dispatch:
inputs:
version:
description: 'Version that should be set on rollback'
required: true
type: string
jobs:
rollback:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Retag mainnet
run: |
docker pull vegaprotocol/trading:${{ inputs.version }}
docker tag vegaprotocol/trading:${{ inputs.version }} vegaprotocol/trading:mainnet
docker push vegaprotocol/trading:mainnet
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
- name: Trigger fleek deployment
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Update vega.trading DNS to redirect to the new console
run: |
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
# Generate console URL
new_console_url_type=ipfs
# new_console_url_type=ipns
new_console_url_domain=cf-ipfs.com
# new_console_url_domain=dweb.link
new_console_url="https://${new_cid}.${new_console_url_type}.${new_console_url_domain}/"
echo "new_console_url=${new_console_url}"
# Update record in DNSimple
# docs: https://developer.dnsimple.com/v2/zones/records/#updateZoneRecord
dnsimple_account_id=84895
dnsimple_zone_name=vega.trading
dnsimple_record_id=44409591
# see: https://dnsimple.com/a/84895/domains/vega.trading/records/44409591/edit
curl -H 'Authorization: Bearer ${{ secrets.DNSIMPLE_API_TOKEN }}' \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
-X PATCH \
-d "{
\"content\": \"${new_console_url}\"
}" \
https://api.dnsimple.com/v2/${dnsimple_account_id}/zones/${dnsimple_zone_name}/records/${dnsimple_record_id}
+1 -1
View File
@@ -49,4 +49,4 @@ cypress.env.json
.next
#cypress
/apps/trading-e2e/cypress/reports/
/apps/**/cypress/reports/
+2 -1
View File
@@ -1,10 +1,11 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
projectId: 'et4snf',
reporter: '../../node_modules/cypress-mochawesome-reporter',
e2e: {
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('@cypress/grep/src/plugin')(config);
return config;
},
+1
View File
@@ -15,5 +15,6 @@
import '@vegaprotocol/cypress';
import './common.functions.js';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
registerCypressGrep();
@@ -3,6 +3,7 @@ import { Header } from './header';
import { MemoryRouter } from 'react-router-dom';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => (
<div data-testid="network-switcher">NetworkSwitcher</div>
),
@@ -1,8 +1,8 @@
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import {
LiquidityInfoPanel,
LiquidityMonitoringParametersInfoPanel,
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityPriceRangeInfoPanel,
@@ -12,8 +12,8 @@ import {
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
} from '@vegaprotocol/markets';
import { MarketInfoTable } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
@@ -1,4 +1,4 @@
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import compact from 'lodash/compact';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets';
import { PageTitle } from '../../components/page-helpers/page-title';
export const MarketPage = () => {
@@ -1,6 +1,6 @@
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { marketsProvider } from '@vegaprotocol/market-list';
import { marketsProvider } from '@vegaprotocol/markets';
import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
+3 -5
View File
@@ -32,9 +32,9 @@
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
}
.vega-ag-grid .ag-row {
border-width: 1px 0;
border-bottom: 1px solid transparent;
.vega-ag-grid .ag-row,
.vega-ag-grid .ag-cell {
border-width: 0;
}
/* Light variables */
@@ -46,7 +46,6 @@
--ag-header-column-separator-color: theme(colors.neutral[300]);
--ag-row-border-color: theme(colors.white);
--ag-row-hover-color: theme(colors.neutral[100]);
--ag-font-size: 12px;
}
/* Dark variables */
@@ -58,7 +57,6 @@
--ag-header-column-separator-color: theme(colors.neutral[600]);
--ag-row-border-color: theme(colors.black);
--ag-row-hover-color: theme(colors.neutral[800]);
--ag-font-size: 12px;
}
.voteicon svg {
+2 -1
View File
@@ -1,10 +1,11 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
projectId: 'et4snf',
reporter: '../../node_modules/cypress-mochawesome-reporter',
e2e: {
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('@cypress/grep/src/plugin')(config);
return config;
},
@@ -63,7 +63,6 @@ context(
});
it('should have option to go to last and newest page', function () {
waitForBeginningOfEpoch();
cy.getByTestId('goto-last-page').click();
cy.getByTestId('epoch-total-rewards-table')
.last()
+1
View File
@@ -8,6 +8,7 @@ import './wallet-eth.functions.ts';
import './wallet-teardown.functions.ts';
import './wallet-vega.functions.ts';
import './proposal.functions.ts';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
+17 -4
View File
@@ -52,6 +52,7 @@ import {
} from './components/telemetry-dialog/telemetry-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next';
import { isPartyNotFoundError } from './lib/party';
const cache: InMemoryCacheConfig = {
typePolicies: {
@@ -211,24 +212,36 @@ const AppContainer = () => {
enabled: true,
environment: VEGA_ENV,
release: GIT_COMMIT_HASH,
beforeSend(event) {
beforeSend(event, hint) {
const error = hint?.originalException;
const errorIsString = typeof error === 'string';
const errorIsObject = error instanceof Error;
const requestUrl = event.request?.url;
const transaction = event.transaction;
if (
(errorIsString && isPartyNotFoundError({ message: error })) ||
(errorIsObject && isPartyNotFoundError(error))
) {
// This error is caused by a pubkey making an API request before
// it has interacted with the chain. This isn't needed in Sentry.
return null;
}
const updatedRequest =
requestUrl && requestUrl.includes('/test?')
requestUrl && requestUrl.includes('/claim?')
? { ...event.request, url: removeQueryParams(requestUrl) }
: event.request;
const updatedTransaction =
transaction && transaction.includes('/test?')
transaction && transaction.includes('/claim?')
? removeQueryParams(transaction)
: transaction;
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
if (
breadcrumb.type === 'navigation' &&
breadcrumb.data?.to?.includes('/test?')
breadcrumb.data?.to?.includes('/claim?')
) {
return {
...breadcrumb,
@@ -1,6 +1,6 @@
import { Link } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
export const DownloadWalletPrompt = () => {
const { t } = useTranslation();
@@ -26,6 +26,7 @@ import type {
WalletDelegationFieldsFragment,
} from './__generated__/Delegations';
import { DelegationsDocument } from './__generated__/Delegations';
import { isPartyNotFoundError } from '../../lib/party';
export const usePollForDelegations = () => {
const { token: vegaToken } = useContracts();
@@ -75,6 +76,7 @@ export const usePollForDelegations = () => {
})
.then((res) => {
if (!mounted) return;
const canonisedDelegations = removePaginationWrapper(
res.data.party?.delegationsConnection?.edges
);
@@ -203,6 +205,15 @@ export const usePollForDelegations = () => {
setDelegatedNodes(delegatedAmounts);
})
.catch((err: Error) => {
// if party isn't found, dont log to Sentry, just clear state as, user
// will not have any delagations or accounts
if (isPartyNotFoundError(err)) {
setDelegations([]);
setAccounts([]);
setDelegatedNodes([]);
setCurrentStakeAvailable(new BigNumber(0));
return;
}
Sentry.captureException(err);
// If query fails stop interval. Its almost certain that the query
// will just continue to fail
@@ -594,7 +594,9 @@
"numberOfAgainstVotes": "Number of votes against",
"yesPercentage": "Yes percentage",
"noPercentage": "No percentage",
"proposalTerms": "Proposal terms",
"proposalJson": "Full proposal JSON",
"proposalDetails": "Proposal details",
"proposalDescription": "Description",
"currentlySetTo": "Currently expected to ",
"currently": "currently",
"finalOutcomeMayDiffer": "Final outcome may differ",
+8
View File
@@ -0,0 +1,8 @@
export const PARTY_NOT_FOUND = 'failed to get party for ID';
export const isPartyNotFoundError = (error: { message: string }) => {
if (error.message.includes(PARTY_NOT_FOUND)) {
return true;
}
return false;
};
+2 -1
View File
@@ -12,7 +12,8 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import {
@@ -0,0 +1 @@
export { ProposalDescription } from './proposal-description';
@@ -0,0 +1,51 @@
import ReactMarkdown from 'react-markdown';
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
export const ProposalDescription = ({
description,
}: {
description: string;
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
const showDescriptionIconClasses = classnames('mb-4', {
'rotate-180': showDescription,
});
return (
<section data-testid="proposal-description">
<button
onClick={() => setShowDescription(!showDescription)}
data-testid="proposal-description-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDescription')} />
<div className={showDescriptionIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDescription && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
</RoundedWrapper>
)}
</section>
);
};
@@ -206,28 +206,6 @@ describe('Proposal header', () => {
).not.toBeInTheDocument();
});
it('Renders Freeform proposal - long rationale (105 chars) - details', () => {
renderComponent(
generateProposal({
id: 'long',
rationale: {
title: '0x0',
description:
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
},
terms: {
change: {
__typename: 'NewFreeform',
},
},
}),
false
);
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
/Class aptent/
);
});
// Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => {
renderComponent(
@@ -5,7 +5,6 @@ import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import ReactMarkdown from 'react-markdown';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
@@ -23,12 +22,7 @@ export const ProposalHeader = ({
let details: ReactNode;
let proposalType = '';
let title = proposal?.rationale.title.trim();
let description = proposal?.rationale.description.trim();
if (title?.length === 0 && description && description.length > 0) {
title = description;
description = '';
}
const title = proposal?.rationale.title.trim();
const titleContent = shorten(title ?? '', 100);
@@ -145,23 +139,6 @@ export const ProposalHeader = ({
{details}
</div>
)}
{description && !isListItem && (
<div data-testid="proposal-description">
{/*<div className="uppercase mr-2">{t('ProposalDescription')}:</div>*/}
<SubHeading title={t('ProposalDescription')} />
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
)}
</>
);
};
@@ -0,0 +1 @@
export { ProposalJson } from './proposal-json';
@@ -1,15 +1,15 @@
import classnames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import type { PartialDeep } from 'type-fest';
import type * as Schema from '@vegaprotocol/types';
import { useState } from 'react';
import classnames from 'classnames';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const ProposalTermsJson = ({
terms,
export const ProposalJson = ({
proposal,
}: {
terms: PartialDeep<Schema.ProposalTerms>;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
@@ -18,20 +18,20 @@ export const ProposalTermsJson = ({
});
return (
<section>
<section data-testid="proposal-json">
<button
onClick={() => setShowDetails(!showDetails)}
data-testid="proposal-terms-toggle"
data-testid="proposal-json-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalTerms')} />
<SubHeading title={t('proposalJson')} />
<div className={showDetailsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showDetails && <SyntaxHighlighter data={terms} />}
{showDetails && <SyntaxHighlighter data={proposal} />}
</section>
);
};
@@ -1 +0,0 @@
export { ProposalTermsJson } from './proposal-terms-json';
@@ -0,0 +1 @@
export { ProposalTerms } from './proposal-terms';
@@ -0,0 +1,142 @@
import {
RoundedWrapper,
KeyValueTable,
KeyValueTableRow,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { BigNumber } from '../../../../lib/bignumber';
import { SubHeading } from '../../../../components/heading';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import classnames from 'classnames';
interface ProposalTermsProps {
data: Record<string, unknown>;
}
const getParsedValue = (value: unknown) => {
if (typeof value === 'string') {
try {
// Check if value is a number string - if so use bignumber to maintain precision
if (/^\d+(\.\d+)?$/.test(value)) {
return new BigNumber(value).toString();
} else {
// This would convert, for example, a JSON object ('{"key":"value"}')
// into an actual JS object ({key: "value"})
return JSON.parse(value);
}
} catch (error) {
return value;
}
} else {
return value;
}
};
const RenderKeyValue = ({
title,
value,
}: {
title: string;
value: string | number;
}) => (
<KeyValueTable>
<KeyValueTableRow>
{title}
{value}
</KeyValueTableRow>
</KeyValueTable>
);
const RenderArray = ({ title, array }: { title: string; array: unknown[] }) => {
if (array.every((item) => typeof item === 'string')) {
return <RenderKeyValue title={title} value={array.join(', ')} />;
} else {
return (
<div>
<div className="mb-2">{title}</div>
{array.map((item, index) => (
<div key={index}>
<ProposalTermsRenderer data={item as Record<string, unknown>} />
</div>
))}
</div>
);
}
};
// Working with 'unknown' type as a proposal's terms can be in many shapes
const RenderTerm = ({ title, value }: { title: string; value: unknown }) => {
const parsedValue = getParsedValue(value);
if (parsedValue === null || typeof parsedValue === 'boolean') {
return <RenderKeyValue title={title} value={String(parsedValue)} />;
} else if (
typeof parsedValue === 'string' ||
typeof parsedValue === 'number'
) {
return <RenderKeyValue title={title} value={parsedValue} />;
} else if (typeof parsedValue === 'object') {
if (Array.isArray(parsedValue)) {
return <RenderArray title={title} array={parsedValue} />;
} else {
return (
<div>
<div className="my-2">{title}</div>
<ProposalTermsRenderer
data={parsedValue as Record<string, unknown>}
/>
</div>
);
}
} else if (parsedValue === undefined) {
return <span>{'undefined'}</span>;
} else {
return <span>{String(parsedValue)}</span>;
}
};
const ProposalTermsRenderer = ({ data }: ProposalTermsProps) => {
return (
<RoundedWrapper paddingBottom={true}>
{Object.keys(data)
.filter(
(key) =>
!['__typename', 'closingDatetime', 'enactmentDatetime'].includes(
key
)
)
.map((key, index) => (
<div key={index}>
<RenderTerm title={key} value={data[key]} />
</div>
))}
</RoundedWrapper>
);
};
export const ProposalTerms = ({ data }: ProposalTermsProps) => {
const { t } = useTranslation();
const [showTerms, setShowTerms] = useState(false);
const showTermsIconClasses = classnames('mb-4', {
'rotate-180': showTerms,
});
return (
<section data-testid="proposal-terms">
<button
onClick={() => setShowTerms(!showTerms)}
data-testid="proposal-terms-toggle"
>
<div className="flex items-center gap-3">
<SubHeading title={t('proposalDetails')} />
<div className={showTermsIconClasses}>
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
{showTerms && <ProposalTermsRenderer data={data} />}
</section>
);
};
@@ -25,8 +25,11 @@ jest.mock('../proposal-detail-header/proposal-header', () => ({
jest.mock('../proposal-change-table', () => ({
ProposalChangeTable: () => <div data-testid="proposal-change-table"></div>,
}));
jest.mock('../proposal-terms-json', () => ({
ProposalTermsJson: () => <div data-testid="proposal-terms-json"></div>,
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-terms/proposal-terms', () => ({
ProposalTerms: () => <div data-testid="proposal-terms"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
@@ -49,7 +52,8 @@ it('renders each section', async () => {
render(<Proposal proposal={proposal as ProposalQuery['proposal']} />);
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-terms')).toBeInTheDocument();
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
@@ -6,8 +6,10 @@ import { AsyncRenderer, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalTermsJson } from '../proposal-terms-json';
import { ProposalJson } from '../proposal-json';
import { ProposalTerms } from '../proposal-terms';
import { ProposalVotesTable } from '../proposal-votes-table';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
@@ -20,7 +22,7 @@ export enum ProposalType {
PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER',
PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM',
}
interface ProposalProps {
export interface ProposalProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}
@@ -78,9 +80,11 @@ export const Proposal = ({ proposal }: ProposalProps) => {
<AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal">
<ProposalHeader proposal={proposal} isListItem={false} />
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
@@ -90,7 +94,20 @@ export const Proposal = ({ proposal }: ProposalProps) => {
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-12">
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
<ProposalTerms data={proposal.terms} />
</div>
<div className="mb-6">
<ProposalJson proposal={proposal} />
</div>
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
@@ -102,10 +119,10 @@ export const Proposal = ({ proposal }: ProposalProps) => {
/>
</RoundedWrapper>
</div>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
<ProposalTermsJson terms={proposal.terms} />
</section>
</AsyncRenderer>
);
@@ -8,12 +8,11 @@ import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { createDocsLinks, ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
@@ -36,7 +35,6 @@ export const ProposalsList = ({
protocolUpgradeProposals,
lastBlockHeight,
}: ProposalsListProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const sortedProposals = proposals.reduce(
@@ -83,9 +81,9 @@ export const ProposalsList = ({
marginBottom={false}
title={t('pageTitleProposals')}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<div className="xs:justify-self-end" data-testid="new-proposal-link">
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}>
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
@@ -2,9 +2,10 @@ import { render, screen } from '@testing-library/react';
import { ProposalFormTerms } from './proposal-form-terms';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const renderComponent = () => {
@@ -5,8 +5,7 @@ import {
ExternalLink,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/utils';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import type { UseFormRegisterReturn } from 'react-hook-form';
interface ProposalFormTermsProps {
@@ -22,23 +21,20 @@ export const ProposalFormTerms = ({
labelOverride,
docsLink,
}: ProposalFormTermsProps) => {
const { VEGA_DOCS_URL } = useEnvironment();
const { t } = useTranslation();
return (
<FormGroup
label={labelOverride || t('ProposalTerms')}
labelFor="proposal-terms"
>
{docsLink && VEGA_DOCS_URL && (
{docsLink && DocsLinks && (
<div className="mt-[-4px] mb-2 text-sm font-light">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${docsLink}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
target="_blank"
>
{`${createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}${docsLink}`}
{`${DocsLinks.PROPOSALS_GUIDE}${docsLink}`}
</ExternalLink>
</div>
)}
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -5,7 +5,7 @@ import {
useProposalSubmit,
deadlineToRoundedHours,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import {
ProposalFormDescription,
ProposalFormSubheader,
@@ -17,7 +17,6 @@ import {
import { ProposalMinRequirements } from '../../components/shared';
import { AsyncRenderer, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { Heading } from '../../../../components/heading';
import { createDocsLinks } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -42,7 +41,7 @@ export const ProposeFreeform = () => {
NetworkParams.governance_proposal_freeform_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -111,17 +110,13 @@ export const ProposeFreeform = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -1,10 +1,7 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from 'react-hook-form';
import {
createDocsLinks,
suitableForSyntaxHighlighter,
} from '@vegaprotocol/utils';
import { suitableForSyntaxHighlighter } from '@vegaprotocol/utils';
import { useNetworkParams } from '@vegaprotocol/network-parameters';
import {
getClosingTimestamp,
@@ -12,7 +9,7 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import {
ProposalFormDescription,
ProposalFormSubheader,
@@ -84,7 +81,7 @@ export const ProposeNetworkParameter = () => {
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams();
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -175,17 +172,13 @@ export const ProposeNetworkParameter = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const newAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -7,8 +7,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -53,7 +53,7 @@ export const ProposeNewAsset = () => {
NetworkParams.governance_proposal_asset_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -142,17 +142,13 @@ export const ProposeNewAsset = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const newMarketNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -6,8 +6,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -51,7 +51,7 @@ export const ProposeNewMarket = () => {
NetworkParams.governance_proposal_market_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -132,17 +132,13 @@ export const ProposeNewMarket = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -2,12 +2,11 @@ import Routes from '../../routes';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { Heading } from '../../../components/heading';
import { createDocsLinks } from '@vegaprotocol/utils';
export const Propose = () => {
const { VEGA_DOCS_URL, VEGA_EXPLORER_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
return (
@@ -15,14 +14,11 @@ export const Propose = () => {
<section className="pb-6">
<Heading title={t('NewProposal')} />
<div className="text-sm">
{VEGA_DOCS_URL && (
{DocsLinks && (
<p>
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
target="_blank"
>
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
{DocsLinks.PROPOSALS_GUIDE}
</ExternalLink>
</p>
)}
@@ -1,6 +1,6 @@
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useEnvironment } from '@vegaprotocol/environment';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { Heading } from '../../../../components/heading';
import {
AsyncRenderer,
@@ -9,7 +9,7 @@ import {
InputError,
TextArea,
} from '@vegaprotocol/ui-toolkit';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -39,7 +39,7 @@ export const ProposeRaw = () => {
NetworkParams.governance_proposal_freeform_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -79,14 +79,11 @@ export const ProposeRaw = () => {
spamProtectionMin={params.spam_protection_proposal_min_tokens}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
target="_blank"
>
{createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE}
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE} target="_blank">
{DocsLinks.PROPOSALS_GUIDE}
</ExternalLink>
</p>
)}
@@ -10,9 +10,10 @@ import type { MockedResponse } from '@apollo/client/testing';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
jest.mock('@vegaprotocol/environment', () => ({
useEnvironment: () => ({
VEGA_DOCS_URL: 'https://docs.vega.xyz',
}),
...jest.requireActual('@vegaprotocol/environment'),
DocsLinks: {
PROPOSALS_GUIDE: 'https://docs.vega.xyz/tutorials/proposals',
},
}));
const updateAssetNetworkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
@@ -6,8 +6,8 @@ import {
useProposalSubmit,
doesValueEquateToParam,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -51,7 +51,7 @@ export const ProposeUpdateAsset = () => {
NetworkParams.governance_proposal_updateAsset_minProposerBalance,
NetworkParams.spam_protection_proposal_min_tokens,
]);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -132,17 +132,13 @@ export const ProposeUpdateAsset = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -7,8 +7,8 @@ import {
getEnactmentTimestamp,
useProposalSubmit,
} from '@vegaprotocol/proposals';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, validateJson } from '@vegaprotocol/utils';
import { useEnvironment, DocsLinks } from '@vegaprotocol/environment';
import { validateJson } from '@vegaprotocol/utils';
import {
NetworkParams,
useNetworkParams,
@@ -98,7 +98,7 @@ export const ProposeUpdateMarket = () => {
const [selectedMarket, setSelectedMarket] = useState<string | undefined>(
undefined
);
const { VEGA_EXPLORER_URL, VEGA_DOCS_URL } = useEnvironment();
const { VEGA_EXPLORER_URL } = useEnvironment();
const { t } = useTranslation();
const {
register,
@@ -181,17 +181,13 @@ export const ProposeUpdateMarket = () => {
userAction={ProposalUserAction.CREATE}
/>
{VEGA_DOCS_URL && (
{DocsLinks && (
<p className="text-sm" data-testid="proposal-docs-link">
<span className="mr-1">{t('ProposalTermsText')}</span>
<ExternalLink
href={`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}
href={`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}
target="_blank"
>{`${
createDocsLinks(VEGA_DOCS_URL).PROPOSALS_GUIDE
}${DOCS_LINK}`}</ExternalLink>
>{`${DocsLinks.PROPOSALS_GUIDE}${DOCS_LINK}`}</ExternalLink>
</p>
)}
@@ -25,7 +25,7 @@ const mockData = {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_LIQUIDITY: {
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: {
amount: '0',
percentageOfTotal: '0',
},
@@ -55,7 +55,9 @@ describe('EpochIndividualRewardsTable', () => {
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
@@ -24,7 +24,7 @@ describe('generateEpochIndividualRewardsList', () => {
};
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
@@ -34,7 +34,7 @@ describe('generateEpochIndividualRewardsList', () => {
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
@@ -44,7 +44,7 @@ describe('generateEpochIndividualRewardsList', () => {
};
const reward5: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '150',
percentageOfTotal: '0.15',
receivedAt: new Date(),
@@ -105,7 +105,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
@@ -172,7 +172,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
@@ -202,7 +202,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
@@ -237,7 +237,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
@@ -285,7 +285,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '150',
percentageOfTotal: '0.15',
},
@@ -320,7 +320,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '200',
percentageOfTotal: '0.2',
},
@@ -350,7 +350,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
@@ -395,7 +395,7 @@ describe('generateEpochIndividualRewardsList', () => {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
amount: '100',
percentageOfTotal: '0.1',
},
@@ -29,7 +29,7 @@ const rewardsList = [
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
{
@@ -78,7 +78,9 @@ describe('EpochTotalRewardsTable', () => {
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
@@ -162,9 +162,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
@@ -227,7 +228,7 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '5',
},
},
@@ -287,9 +288,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
@@ -351,9 +353,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '5',
},
],
@@ -416,7 +419,7 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '6',
},
},
@@ -424,7 +427,7 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '27',
},
},
@@ -497,9 +500,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '33',
},
],
@@ -561,9 +565,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
@@ -637,9 +642,10 @@ describe('generateEpochAssetRewardsList', () => {
},
],
[
AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
rewardType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
amount: '0',
},
],
@@ -11,7 +11,6 @@ import {
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { createDocsLinks } from '@vegaprotocol/utils';
import {
useNetworkParams,
NetworkParams,
@@ -21,7 +20,7 @@ import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading, SubHeading } from '../../../components/heading';
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
@@ -29,7 +28,6 @@ type RewardsView = 'total' | 'individual';
export const RewardsPage = () => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
const { pubKey, pubKeys } = useVegaWallet();
const [toggleRewardsView, setToggleRewardsView] =
useState<RewardsView>('total');
@@ -68,9 +66,9 @@ export const RewardsPage = () => {
<Heading title={t('pageTitleRewards')} />
<p className="mb-12">
{t('rewardsIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).REWARDS_GUIDE}
href={DocsLinks.REWARDS_GUIDE}
target="_blank"
data-testid="rewards-guide-link"
className="text-white"
@@ -26,7 +26,7 @@ export const RowAccountTypes = {
columnTitle: 'rewardsColPriceMakingHeader',
description: 'rewardsColPriceMakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
[AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES]: {
columnTitle: 'rewardsColLiquidityProvisionHeader',
description: 'rewardsColLiquidityProvisionTooltip',
},
+7 -4
View File
@@ -229,7 +229,7 @@ const redirects = [
const routerConfig = [
{
path: Routes.HOME,
element: <LazyHome name="Token" />,
element: <LazyHome name="Home" />,
},
{
path: Routes.PROPOSALS,
@@ -291,7 +291,7 @@ const routerConfig = [
// Not lazy as loaded when a user first hits the site
children: [
{
element: <Home name="Home" />,
element: <Home name="Token" />,
index: true,
},
{
@@ -325,8 +325,11 @@ const routerConfig = [
},
],
},
{ path: 'associate', element: <LazyStakingAssociate /> },
{ path: 'disassociate', element: <LazyStakingDisassociate /> },
{ path: 'associate', element: <LazyStakingAssociate name="Associate" /> },
{
path: 'disassociate',
element: <LazyStakingDisassociate name="Disassociate" />,
},
],
},
{
@@ -1,12 +1,14 @@
import { useEthereumConfig } from '@vegaprotocol/web3';
import { StakingWalletsContainer } from './components/staking-wallets-container/staking-wallets-container';
import { StakingWalletsContainer } from './components/staking-wallets-container';
import { AssociatePage } from './associate-page';
import { AssociatePageNoVega } from './associate-page-no-vega';
import { Heading } from '../../../components/heading';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import type { RouteChildProps } from '../../index';
export const AssociateContainer = () => {
export const AssociateContainer = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const { config } = useEthereumConfig();
@@ -39,7 +39,7 @@ jest.mock('@vegaprotocol/wallet', () => ({
}));
const renderComponent = () => {
return render(<Disassociate />);
return render(<Disassociate name="Disassociate" />);
};
describe('Disassociate', () => {
@@ -3,10 +3,12 @@ import { useWeb3React } from '@web3-react/core';
import { EthConnectPrompt } from '../../../components/eth-connect-prompt';
import { DisassociatePage } from './components/disassociate-page';
import { Heading } from '../../../components/heading';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import type { RouteChildProps } from '../../index';
export const DisassociateContainer = () => {
export const DisassociateContainer = ({ name }: RouteChildProps) => {
useDocumentTitle(name);
const { t } = useTranslation();
const { account } = useWeb3React();
const { pubKey } = useVegaWallet();
@@ -1,14 +1,12 @@
import { useTranslation } from 'react-i18next';
import { EpochData } from './epoch-data';
import { useEnvironment } from '@vegaprotocol/environment';
import { DocsLinks } from '@vegaprotocol/environment';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { createDocsLinks } from '@vegaprotocol/utils';
import { Heading } from '../../../components/heading';
import React from 'react';
export const Staking = () => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
return (
<>
@@ -16,9 +14,9 @@ export const Staking = () => {
<section>
<p className="mb-12">
{t('stakingIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="staking-guide-link"
className="text-white"
@@ -131,20 +131,25 @@ export const ConsensusValidatorsTable = ({
const gridRef = useRef<AgGridReact | null>(null);
const thirdOfTotalStake = useMemo(
() => new BigNumber(totalStake).dividedBy(3),
[totalStake]
);
const nodes = useMemo(() => {
if (!data) return [];
let canonisedNodes = data
.sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
return bVotingPower.minus(aVotingPower).toNumber();
const aStakedTotal = new BigNumber(a.stakedTotal);
const bStakedTotal = new BigNumber(b.stakedTotal);
return bStakedTotal.minus(aStakedTotal).toNumber();
})
.map((node, index) => {
const votingPowerRanking = index + 1;
const stakedTotalRanking = index + 1;
return {
...node,
votingPowerRanking,
stakedTotalRanking,
};
})
.map(
@@ -157,7 +162,7 @@ export const ConsensusValidatorsTable = ({
stakedTotal,
rankingScore: { stakeScore, votingPower },
pendingStake,
votingPowerRanking,
stakedTotalRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
@@ -175,7 +180,7 @@ export const ConsensusValidatorsTable = ({
return {
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
[ValidatorFields.VALIDATOR]: {
avatarUrl: logo,
name,
@@ -236,16 +241,12 @@ export const ConsensusValidatorsTable = ({
return canonisedNodes;
}
// The point of identifying and hiding the group that could halt the network
// is that we assume the top 1/3 of stake is held by considerably less than
// 1/3 of the validators and we really want people not to stake any more to
// that group, because we want to make it require as many difference
// validators to collude as possible to halt the network, so we hide them.
const { topThird, remaining } = canonisedNodes.reduce(
(acc, node) => {
if (acc.cumulativeScore < 100 / 3) {
acc.cumulativeScore += parseFloat(
node[ValidatorFields.NORMALISED_VOTING_POWER]
if (acc.cumulativeScore.isLessThan(thirdOfTotalStake)) {
const prev = acc.cumulativeScore;
acc.cumulativeScore = prev.plus(
new BigNumber(node[ValidatorFields.STAKE])
);
acc.topThird.push(node);
return acc;
@@ -253,61 +254,70 @@ export const ConsensusValidatorsTable = ({
acc.remaining.push(node);
return acc;
},
{ topThird: [], remaining: [], cumulativeScore: 0 } as {
{ topThird: [], remaining: [], cumulativeScore: new BigNumber(0) } as {
topThird: CanonisedConsensusNodeProps[];
remaining: CanonisedConsensusNodeProps[];
cumulativeScore: number;
cumulativeScore: BigNumber;
}
);
// We need to combine the top third of validators into a single node, this
// way the combined values can be passed to AG grid so that the combined cell's
// values are correct for ordering.
const combinedTopThird = topThird.reduce((acc, node) => {
const {
[ValidatorFields.STAKE]: stake,
[ValidatorFields.STAKE_SHARE]: stakeShare,
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
} = node;
const combinedTopThird = topThird.reduce(
(acc, node) => {
const {
[ValidatorFields.STAKE]: stake,
[ValidatorFields.STAKE_SHARE]: stakeShare,
[ValidatorFields.PENDING_STAKE]: pendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: normalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: totalPenalties,
} = node;
const {
[ValidatorFields.STAKE]: accStake,
[ValidatorFields.STAKE_SHARE]: accStakeShare,
[ValidatorFields.PENDING_STAKE]: accPendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
} = acc;
const {
[ValidatorFields.STAKE]: accStake,
[ValidatorFields.STAKE_SHARE]: accStakeShare,
[ValidatorFields.PENDING_STAKE]: accPendingStake,
[ValidatorFields.NORMALISED_VOTING_POWER]: accNormalisedVotingPower,
[ValidatorFields.TOTAL_PENALTIES]: accTotalPenalties,
} = acc;
return {
...acc,
[ValidatorFields.STAKE]: toBigNum(accStake, decimals)
.plus(toBigNum(stake, decimals))
.toString(),
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
new BigNumber(parseFloat(accStakeShare)).plus(
new BigNumber(parseFloat(stakeShare))
return {
...acc,
[ValidatorFields.STAKE]: new BigNumber(accStake)
.plus(new BigNumber(stake))
.toString(),
[ValidatorFields.STAKE_SHARE]: formatNumberPercentage(
new BigNumber(parseFloat(accStakeShare)).plus(
new BigNumber(parseFloat(stakeShare))
),
2
),
2
),
[ValidatorFields.PENDING_STAKE]: toBigNum(accPendingStake, decimals)
.plus(toBigNum(pendingStake, decimals))
.toString(),
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
new BigNumber(parseFloat(normalisedVotingPower))
[ValidatorFields.PENDING_STAKE]: toBigNum(accPendingStake, decimals)
.plus(toBigNum(pendingStake, decimals))
.toString(),
[ValidatorFields.NORMALISED_VOTING_POWER]: formatNumberPercentage(
new BigNumber(parseFloat(accNormalisedVotingPower)).plus(
new BigNumber(parseFloat(normalisedVotingPower))
),
2
),
2
),
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
new BigNumber(parseFloat(accTotalPenalties)).plus(
new BigNumber(parseFloat(totalPenalties))
[ValidatorFields.TOTAL_PENALTIES]: formatNumberPercentage(
new BigNumber(parseFloat(accTotalPenalties)).plus(
new BigNumber(parseFloat(totalPenalties))
),
2
),
2
),
};
});
};
},
{
[ValidatorFields.STAKE]: '0',
[ValidatorFields.STAKE_SHARE]: '0',
[ValidatorFields.PENDING_STAKE]: '0',
[ValidatorFields.NORMALISED_VOTING_POWER]: '0',
[ValidatorFields.TOTAL_PENALTIES]: '0',
}
);
return [
{
@@ -322,6 +332,7 @@ export const ConsensusValidatorsTable = ({
decimals,
hideTopThird,
previousEpochData,
thirdOfTotalStake,
totalStake,
validatorsView,
]);
@@ -352,6 +363,7 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.PENDING_STAKE,
@@ -373,7 +385,6 @@ export const ConsensusValidatorsTable = ({
headerTooltip: t('NormalisedVotingPowerDescription').toString(),
cellRenderer: VotingPowerRenderer,
width: 200,
sort: 'desc',
},
{
field: ValidatorFields.TOTAL_PENALTIES,
@@ -57,16 +57,16 @@ export const StandbyPendingValidatorsTable = ({
return data
.sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
return bVotingPower.minus(aVotingPower).toNumber();
const aStakedTotal = new BigNumber(a.stakedTotal);
const bStakedTotal = new BigNumber(b.stakedTotal);
return bStakedTotal.minus(aStakedTotal).toNumber();
})
.map((node, index) => {
const votingPowerRanking = index + 1;
const stakedTotalRanking = index + 1;
return {
...node,
votingPowerRanking,
stakedTotalRanking,
};
})
.map(
@@ -79,7 +79,7 @@ export const StandbyPendingValidatorsTable = ({
stakedTotal,
rankingScore: { stakeScore },
pendingStake,
votingPowerRanking,
stakedTotalRanking,
stakedByUser,
pendingUserStake,
userStakeShare,
@@ -125,7 +125,7 @@ export const StandbyPendingValidatorsTable = ({
return {
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
[ValidatorFields.RANKING_INDEX]: stakedTotalRanking,
[ValidatorFields.VALIDATOR]: {
avatarUrl,
name,
@@ -166,7 +166,7 @@ export const StandbyPendingValidatorsTable = ({
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? formatNumberPercentage(new BigNumber(userStakeShare))
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
: undefined,
};
}
@@ -210,6 +210,7 @@ export const StandbyPendingValidatorsTable = ({
headerTooltip: t('StakeDescription').toString(),
cellRenderer: TotalStakeRenderer,
width: 120,
sort: 'desc',
},
{
field: ValidatorFields.PENDING_STAKE,
@@ -233,7 +234,6 @@ export const StandbyPendingValidatorsTable = ({
}),
cellRenderer: StakeNeededForPromotionRenderer,
width: 210,
sort: 'asc',
},
{
field: ValidatorFields.TOTAL_PENALTIES,
@@ -2,12 +2,8 @@ import { useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import BigNumber from 'bignumber.js';
import * as Schema from '@vegaprotocol/types';
import {
createDocsLinks,
removePaginationWrapper,
toBigNum,
} from '@vegaprotocol/utils';
import { useEnvironment } from '@vegaprotocol/environment';
import { removePaginationWrapper, toBigNum } from '@vegaprotocol/utils';
import { DocsLinks } from '@vegaprotocol/environment';
import { Link as UTLink, Toggle } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '../../../../lib/format-number';
@@ -42,7 +38,6 @@ export const ValidatorTables = ({
previousEpochData,
}: ValidatorsTableProps) => {
const { t } = useTranslation();
const { VEGA_DOCS_URL } = useEnvironment();
const {
appState: { decimals },
} = useAppState();
@@ -213,12 +208,12 @@ export const ValidatorTables = ({
<>
<SubHeading title={t('status-pending')} />
<p>
{VEGA_DOCS_URL && (
{DocsLinks && (
<>
<span>{t('pendingDescription1')} </span>
<span>
<UTLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="validator-forum-link"
>
@@ -1,4 +1,3 @@
import { useEffect, useState } from 'react';
import { ENV } from '../../../config';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
@@ -10,6 +9,7 @@ import { usePreviousEpochQuery } from '../__generated__/PreviousEpoch';
import type { ReactElement } from 'react';
import type { StakingQuery } from '../__generated__/Staking';
import type { PreviousEpochQuery } from '../__generated__/PreviousEpoch';
import { isPartyNotFoundError } from '../../../lib/party';
// TODO should only request a single node. When migrating from deprecated APIs we should address this.
@@ -39,21 +39,9 @@ export const NodeContainer = ({
}
: undefined,
},
errorPolicy: 'all',
});
const [isRefetching, setIsRefetching] = useState(false);
useEffect(() => {
if (error && error.message.includes('failed to get party for ID')) {
setIsRefetching(true);
// The API errors if there is a pubkey, but it hasn't interacted with the
// chain before. In that case, retry the query with empty pubKey
refetch({
partyId: '',
}).finally(() => setIsRefetching(false));
}
}, [error, refetch, delegationsPagination]);
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
@@ -63,7 +51,7 @@ export const NodeContainer = ({
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
if (error && !isRefetching) {
if (error && !isPartyNotFoundError(error)) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>
@@ -75,7 +63,7 @@ export const NodeContainer = ({
);
}
if (loading || isRefetching) {
if (loading) {
return (
<Splash>
<SplashLoader />
@@ -1,7 +1,11 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useEnvironment } from '@vegaprotocol/environment';
import { createDocsLinks, ExternalLinks, toBigNum } from '@vegaprotocol/utils';
import {
useEnvironment,
DocsLinks,
ExternalLinks,
} from '@vegaprotocol/environment';
import { toBigNum } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types';
import {
Link as UTLink,
@@ -64,7 +68,7 @@ export const ValidatorTable = ({
stakedTotal,
previousEpochData,
}: ValidatorTableProps) => {
const { ETHERSCAN_URL, VEGA_DOCS_URL } = useEnvironment();
const { ETHERSCAN_URL } = useEnvironment();
const { t } = useTranslation();
const {
appState: { decimals },
@@ -90,9 +94,9 @@ export const ValidatorTable = ({
<>
<p className="mb-12">
{t('validatorFormIntro')}{' '}
{VEGA_DOCS_URL && (
{DocsLinks && (
<ExternalLink
href={createDocsLinks(VEGA_DOCS_URL).STAKING_GUIDE}
href={DocsLinks.STAKING_GUIDE}
target="_blank"
data-testid="validator-table-staking-guide-link"
className="text-white"
+8 -3
View File
@@ -60,9 +60,14 @@ export const getOverstakingPenalty = (
}
return formatNumberPercentage(
new BigNumber(1)
.minus(new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore)))
.times(100),
BigNumber.max(
new BigNumber(1)
.minus(
new BigNumber(validatorScore).dividedBy(new BigNumber(stakeScore))
)
.times(100),
new BigNumber(0)
),
2
);
};
+2 -1
View File
@@ -3,7 +3,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Heading, SubHeading } from '../../components/heading';
import { ExternalLinks, toBigNum } from '@vegaprotocol/utils';
import { ExternalLinks } from '@vegaprotocol/environment';
import { toBigNum } from '@vegaprotocol/utils';
import { useAppState } from '../../contexts/app-state/app-state-context';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { RouteChildProps } from '..';
+3 -4
View File
@@ -40,9 +40,9 @@
line-height: calc(min(var(--ag-line-height, 26px), 26px) - 4px);
}
.vega-ag-grid .ag-row {
border-width: 1px 0;
border-bottom: 1px solid transparent;
.vega-ag-grid .ag-row,
.vega-ag-grid .ag-cell {
border-width: 0;
}
/* Dark variables */
@@ -54,7 +54,6 @@
--ag-header-column-separator-color: theme(colors.neutral[600]);
--ag-row-border-color: theme(colors.black);
--ag-row-hover-color: theme(colors.neutral[800]);
--ag-font-size: 12px;
}
.validators-table .ag-theme-balham-dark .ag-body-horizontal-scroll {
@@ -10,8 +10,8 @@ import {
displayChange,
} from '@vegaprotocol/liquidity';
import type { Candle } from '@vegaprotocol/market-list';
import { marketCandlesProvider } from '@vegaprotocol/market-list';
import type { Candle } from '@vegaprotocol/markets';
import { marketCandlesProvider } from '@vegaprotocol/markets';
const THROTTLE_UPDATE_TIME = 500;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -20,7 +20,7 @@ module.exports = defineConfig({
chromeWebSecurity: false,
projectId: 'et4snf',
defaultCommandTimeout: 10000,
viewportWidth: 1440,
viewportWidth: 1800,
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
@@ -318,10 +318,11 @@ describe('Closed markets', { tags: '@smoke' }, () => {
.should('have.text', product.settlementAsset.symbol);
// 6001-MARK-020
cy.get(rowSelector)
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('[col-id="id"]')
.should('have.text', settledMarket.id);
.find('button svg')
.should('exist');
});
// test market list for market in terminated state
@@ -373,5 +374,33 @@ describe('Closed markets', { tags: '@smoke' }, () => {
'have.text',
`Asset details - ${settlementAsset.symbol}`
);
cy.get('[data-testid="dialog-close"]').click();
});
it('can open row actions', () => {
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
.should('have.text', 'Copy Market ID');
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/0/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
});
});
@@ -108,10 +108,9 @@ describe('deposit actions', { tags: '@smoke' }, () => {
it('Deposit to trade is visble', () => {
cy.getByTestId('Collateral').click();
cy.contains('[data-testid="deposit"]', 'Deposit to trade').should(
'be.visible'
);
cy.contains('[data-testid="deposit"]', 'Deposit to trade').click();
cy.contains('[data-testid="deposit"]', 'Deposit')
.should('be.visible')
.click();
cy.getByTestId('deposit-submit').should('be.visible');
});
});
@@ -199,7 +199,6 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
.within(() => {
cy.getByTestId('block-explorer-link').contains('Block explorer');
cy.getByTestId('github-link').contains('Oracle repository');
cy.getByTestId('verified-accounts').contains('0 proofs of ownership');
});
cy.getByTestId('dialog-close').click();
@@ -0,0 +1,105 @@
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
describe('markets selector', { tags: '@smoke' }, () => {
const list = 'market-selector-list';
const searchInput = 'search-term';
beforeEach(() => {
cy.window().then((window) => {
window.localStorage.setItem('marketId', 'market-1');
});
cy.mockTradingPage(
MarketState.STATE_ACTIVE,
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/');
cy.wait('@Markets');
cy.wait('@MarketsData');
cy.wait('@MarketsCandles');
});
it('can toggle the sidebar', () => {
cy.getByTestId('market-selector').should('be.visible');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('not.exist');
cy.getByTestId('sidebar-toggle').click();
cy.getByTestId('market-selector').should('be.visible');
});
// need function keyword as we need 'this' to access market data
it('displays data as expected', () => {
// TODO: load data from mocks in. Using alias and wrap intermittently fails
const data = [
{
code: 'AAPL.MF21',
name: 'Apple Monthly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'BTCUSD.MF21',
name: 'ACTIVE MARKET',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'ETHBTC.QM21',
name: 'ETHBTC Quarterly (30 Jun 2022)',
markPrice: '46,126.90058',
change: '+200.00%',
},
{
code: 'SOLUSD',
name: 'SUSPENDED MARKET',
markPrice: '84.41',
change: '+200.00%',
},
];
cy.getByTestId(list)
.find('a')
.each((item, i) => {
const market = data[i];
// 6001-MARK-021
expect(item.find('h3').text()).equals(market.code);
// 6001-MARK-022
expect(item.find('h4').text()).equals(market.name);
// 6001-MARK-024
expect(item.find('[data-testid="market-item-price"]').text()).equals(
market.markPrice
);
// 6001-MARK-023
expect(item.find('[data-testid="market-item-change"]').text()).equals(
market.change
);
// 6001-MARK-025
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
});
});
// 6001-MARK-27
it('can use the filter options', () => {
// product type
cy.getByTestId('product-Spot').click();
cy.getByTestId(list).contains('Spot markets coming soon.');
cy.getByTestId('product-Perpetual').click();
cy.getByTestId(list).contains('Perpetual markets coming soon.');
cy.getByTestId('product-Future').click();
cy.getByTestId(list).find('a').should('have.length', 4);
// 6001-MARK-29
cy.getByTestId(searchInput).clear().type('btc');
cy.getByTestId(list).find('a').should('have.length', 2);
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
cy.getByTestId(searchInput).clear();
cy.getByTestId(list).find('a').should('have.length', 4);
});
});
@@ -31,7 +31,7 @@ describe('Market trading page', () => {
// 7002-SORD-001
// 7002-SORD-002
it('must display market name', () => {
cy.getByTestId('popover-trigger').should('not.be.empty');
cy.getByTestId('header-title').should('not.be.empty');
});
it('must see market expiry', () => {
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -31,36 +30,6 @@ describe('markets table', { tags: '@smoke' }, () => {
cy.getByTestId('sparkline-svg').should('be.visible');
});
it('renders market list drop down', () => {
openMarketDropDown();
cy.getByTestId('price').invoke('text').should('not.be.empty');
cy.getByTestId('trading-mode-col').should('not.be.empty');
cy.getByTestId('taker-fee').should('contain.text', '%');
cy.getByTestId('market-volume').should('not.be.empty');
cy.getByTestId('market-name').should('not.be.empty');
cy.getByTestId('trading-mode-col')
.contains('Monitoring auction - liquidity')
.eq(0)
.realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should('have.class', '');
cy.get(
'[data-testid="market-trading-mode"] [data-testid="item-value"]'
).realHover();
cy.get('[data-testid="trading-mode-tooltip"] p').should(
'have.class',
'mb-4'
);
});
it('able to select market from dropdown', () => {
openMarketDropDown();
cy.getByTestId('market-link-market-0').first().should('be.visible').click();
cy.contains('ACTIVE MARKET').should('be.visible');
cy.url().should('include', '/markets/market-0');
cy.getByTestId('popover-trigger').should('not.be.empty');
});
it('able to open and sort full market list - market page', () => {
const ExpectedSortedMarkets = [
'AAPL.MF21',
@@ -253,15 +222,3 @@ describe('markets table', { tags: '@smoke' }, () => {
});
});
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).then((button) => {
if (button.is(':visible')) {
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
cy.getByTestId(dialogCloseBtn).click();
}
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
cy.getByTestId(popoverTrigger).click({ force: true });
cy.contains('Loading market data...').should('not.exist');
});
}
@@ -48,7 +48,7 @@ describe('accounts', { tags: '@smoke' }, () => {
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
.find('[col-id="accounts-actions"]')
.should('have.text', ' ');
.should('have.text', '');
cy.getByTestId('tab-accounts')
.get(tradingAccountRowId)
+2 -2
View File
@@ -1,7 +1,7 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import * as Schema from '@vegaprotocol/types';
import type { CyHttpMessages } from 'cypress/types/net-stubbing';
import type { Provider, Status } from '@vegaprotocol/market-info';
import type { Provider, Status } from '@vegaprotocol/markets';
import {
accountsQuery,
assetQuery,
@@ -32,7 +32,7 @@ import {
blockStatisticsQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/markets';
type MarketPageMockData = {
state: Schema.MarketState;
+1 -1
View File
@@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { marketsWithDataProvider } from '@vegaprotocol/market-list';
import { marketsWithDataProvider } from '@vegaprotocol/markets';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { Links, Routes } from '../../pages/client-router';
@@ -5,10 +5,9 @@ import {
lpAggregatedDataProvider,
useCheckLiquidityStatus,
} from '@vegaprotocol/liquidity';
import { tooltipMapping } from '@vegaprotocol/market-info';
import { tooltipMapping } from '@vegaprotocol/markets';
import {
addDecimalsFormatNumber,
createDocsLinks,
formatNumberPercentage,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
@@ -38,8 +37,8 @@ import type { LiquidityProvisionData, Filter } from '@vegaprotocol/liquidity';
import { Link, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarket, useStaticMarketData } from '@vegaprotocol/market-list';
import { useEnvironment } from '@vegaprotocol/environment';
import { useMarket, useStaticMarketData } from '@vegaprotocol/markets';
import { DocsLinks } from '@vegaprotocol/environment';
const enum LiquidityTabs {
Active = 'active',
@@ -147,7 +146,6 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
market?.tradableInstrument.instrument.product.settlementAsset.decimals || 0;
const symbol =
market?.tradableInstrument.instrument.product.settlementAsset.symbol;
const { VEGA_DOCS_URL } = useEnvironment();
const { params } = useNetworkParams([
NetworkParams.market_liquidity_stakeToCcyVolume,
@@ -216,10 +214,12 @@ const LiquidityViewHeader = memo(({ marketId }: { marketId?: string }) => {
<div className="break-word">{marketId}</div>
</HeaderStat>
<HeaderStat heading={t('Learn more')}>
{VEGA_DOCS_URL && (
<ExternalLink href={createDocsLinks(VEGA_DOCS_URL).LIQUIDITY}>
{DocsLinks ? (
<ExternalLink href={DocsLinks.LIQUIDITY}>
{t('Providing liquidity')}
</ExternalLink>
) : (
(null as React.ReactNode)
)}
</HeaderStat>
</Header>
@@ -0,0 +1,83 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AssetDropdown } from './asset-dropdown';
const createAssets = (count = 3) => {
return new Array(count).fill(null).map((_, i) => ({
id: i.toString(),
symbol: 'asset-1',
}));
};
describe('AssetDropdown', () => {
const assets = createAssets();
it('renders and selects chosen assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
expect(items).toHaveLength(assets.length);
expect(items.map((i) => i.textContent)).toEqual(
assets.map((a) => a.symbol)
);
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true);
});
it('unselects already selected assets', async () => {
const mockOnSelect = jest.fn();
render(
<AssetDropdown
checkedAssets={[assets[0].id]}
assets={assets}
onSelect={mockOnSelect}
onReset={jest.fn()}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
await userEvent.click(items[0]);
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, false);
});
it('can be reset clearing all assets', async () => {
const mockOnSelect = jest.fn();
const mockOnReset = jest.fn();
render(
<AssetDropdown
checkedAssets={assets.map((a) => a.id)}
assets={assets}
onSelect={mockOnSelect}
onReset={mockOnReset}
/>
);
await userEvent.click(screen.getByRole('button'));
const items = screen.getAllByRole('menuitemcheckbox');
// all should be checked
items.forEach((item) => {
expect(item).toBeChecked();
});
await userEvent.click(screen.getByText('Reset'));
expect(mockOnReset).toHaveBeenCalled();
});
it('doesnt render if no assets provided', async () => {
const { container } = render(
<AssetDropdown
checkedAssets={[]}
assets={[]}
onSelect={jest.fn()}
onReset={jest.fn()}
/>
);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,58 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from '@vegaprotocol/ui-toolkit';
export const AssetDropdown = ({
assets,
checkedAssets,
onSelect,
onReset,
}: {
assets: Array<{ id: string; symbol: string }> | undefined;
checkedAssets: string[];
onSelect: (id: string, checked: boolean) => void;
onReset: () => void;
}) => {
if (!assets?.length) {
return null;
}
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="asset-trigger">
<span className="px-1">$</span>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{assets?.map((a) => {
return (
<DropdownMenuCheckboxItem
key={a.id}
checked={checkedAssets.includes(a.id)}
onCheckedChange={(checked) => {
if (typeof checked === 'boolean') {
onSelect(a.id, checked);
}
}}
data-testid={`asset-id-${a.id}`}
>
{a.symbol}
<DropdownMenuItemIndicator />
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -2,119 +2,101 @@ import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { useEnvironment } from '@vegaprotocol/environment';
import { ButtonLink, Link } from '@vegaprotocol/ui-toolkit';
import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/market-list';
import type { Market } from '@vegaprotocol/markets';
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
ColumnKind,
SelectMarketPopover,
} from '../../components/select-market';
import type { OnCellClickHandler } from '../../components/select-market';
import { Header, HeaderStat } from '../../components/header';
import { NO_MARKET } from './constants';
import { HeaderStat } from '../../components/header';
import { MarketMarkPrice } from '../../components/market-mark-price';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/markets';
import { MarketState } from '../../components/market-state';
import { HeaderStatMarketTradingMode } from '../../components/market-trading-mode';
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
interface TradeMarketHeaderProps {
interface HeaderStatsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
}
export const TradeMarketHeader = ({
market,
onSelect,
}: TradeMarketHeaderProps) => {
export const HeaderStats = ({ market }: HeaderStatsProps) => {
const { VEGA_EXPLORER_URL } = useEnvironment();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
const onCellClick: OnCellClickHandler = (e, kind, value) => {
if (value && kind === ColumnKind.Asset) {
openAssetDetailsDialog(value, e.target as HTMLElement);
}
};
return (
<Header
title={
<SelectMarketPopover
marketCode={market?.tradableInstrument.instrument.code || NO_MARKET}
marketName={market?.tradableInstrument.instrument.name || NO_MARKET}
onSelect={onSelect}
onCellClick={onCellClick}
/>
}
>
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
<div className="flex flex-col justify-end lg:pt-4">
<div className="xl:flex xl:gap-4 items-end">
<div
data-testid="header-summary"
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
<HeaderStat
heading={t('Expiry')}
description={
market && (
<ExpiryTooltipContent
market={market}
explorerUrl={VEGA_EXPLORER_URL}
/>
)
}
testId="market-expiry"
>
<ExpiryLabel market={market} />
</HeaderStat>
<HeaderStat heading={t('Price')} testId="market-price">
<MarketMarkPrice
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat heading={t('Change (24h)')} testId="market-change">
<Last24hPriceChange
marketId={market?.id}
decimalPlaces={market?.decimalPlaces}
/>
</HeaderStat>
<HeaderStat
heading={t('Volume (24h)')}
testId="market-volume"
description={t(
'The total number of contracts traded in the last 24 hours.'
)}
>
<Last24hVolume
marketId={market?.id}
positionDecimalPlaces={market?.positionDecimalPlaces}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
marketId={market?.id}
initialTradingMode={market?.tradingMode}
/>
<MarketState market={market} />
{asset ? (
<HeaderStat
heading={t('Settlement asset')}
testId="market-settlement-asset"
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</Header>
<div>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</div>
</div>
</div>
);
};
@@ -0,0 +1,111 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketSelectorItem } from './market-selector-item';
import { MemoryRouter } from 'react-router-dom';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type {
MarketDataUpdateFieldsFragment,
MarketDataUpdateSubscription,
} from '@vegaprotocol/markets';
import { MarketDataUpdateDocument } from '@vegaprotocol/markets';
import {
AuctionTrigger,
MarketState,
MarketTradingMode,
} from '@vegaprotocol/types';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
describe('MarketSelectorItem', () => {
const market = createMarketFragment({
id: 'market-0',
decimalPlaces: 2,
// @ts-ignore fragment doesn't contain candles
candles: [{ close: '5' }, { close: '10' }],
});
const marketData: MarketDataUpdateFieldsFragment = {
__typename: 'ObservableMarketData',
marketId: market.id,
auctionEnd: null,
auctionStart: null,
bestBidPrice: '100',
bestBidVolume: '100',
bestOfferPrice: '100',
bestOfferVolume: '100',
bestStaticBidPrice: '100',
bestStaticBidVolume: '100',
bestStaticOfferPrice: '100',
bestStaticOfferVolume: '100',
indicativePrice: '100',
indicativeVolume: '100',
marketState: MarketState.STATE_ACTIVE,
marketTradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
marketValueProxy: '100',
markPrice: '50000',
midPrice: '100',
staticMidPrice: '100',
openInterest: '100',
suppliedStake: '1000000',
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
};
const mock: MockedResponse<MarketDataUpdateSubscription> = {
request: {
query: MarketDataUpdateDocument,
variables: {
marketId: market.id,
},
},
result: {
data: {
marketsData: [marketData],
},
},
};
const mockOnSelect = jest.fn();
const renderJsx = () => {
return render(
<MemoryRouter>
<MockedProvider mocks={[mock]}>
<MarketSelectorItem
market={market}
currentMarketId={market.id}
style={{}}
onSelect={mockOnSelect}
/>
</MockedProvider>
</MemoryRouter>
);
};
it('renders market information', async () => {
renderJsx();
const link = screen.getByRole('link');
// link renders and is styled
expect(link).toHaveAttribute('href', '/markets/' + market.id);
expect(link).toHaveClass('ring-1');
expect(screen.getByTestId('market-item-price')).toHaveTextContent('-');
// candles are loaded immediately
expect(screen.getByTestId('market-item-change')).toHaveTextContent(
'+100.00%'
);
await waitFor(() => {
expect(screen.getByTestId('market-item-price')).toHaveTextContent(
addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
);
});
await userEvent.click(link);
expect(mockOnSelect).toHaveBeenCalledWith(market.id);
});
});
@@ -0,0 +1,115 @@
import type { CSSProperties } from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';
import {
addDecimalsFormatNumber,
formatNumber,
priceChangePercentage,
} from '@vegaprotocol/utils';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import { useMarketDataUpdateSubscription } from '@vegaprotocol/markets';
import { Sparkline } from '@vegaprotocol/ui-toolkit';
export const MarketSelectorItem = ({
market,
style,
currentMarketId,
onSelect,
}: {
market: MarketMaybeWithDataAndCandles;
style: CSSProperties;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const wrapperClasses = classNames(
'block bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4',
'min-h-[120px]',
{
'ring-1 ring-vega-light-300 dark:ring-vega-dark-300':
currentMarketId === market.id,
}
);
return (
<div style={style} className="my-0.5 px-4">
<Link
to={`/markets/${market.id}`}
className={wrapperClasses}
onClick={() => {
onSelect && onSelect(market.id);
}}
>
<h3>{market.tradableInstrument.instrument.code}</h3>
<h4
title={market.tradableInstrument.instrument.name}
className="text-sm text-vega-light-300 dark:text-vega-dark-300 text-ellipsis whitespace-nowrap overflow-hidden"
>
{market.tradableInstrument.instrument.name}
</h4>
<MarketData market={market} />
</Link>
</div>
);
};
const MarketData = ({ market }: { market: MarketMaybeWithDataAndCandles }) => {
const { data } = useMarketDataUpdateSubscription({
variables: {
marketId: market.id,
},
});
const marketData = data?.marketsData[0];
// use market data price if available as this is comes from
// the subscription
const price = marketData
? addDecimalsFormatNumber(marketData.markPrice, market.decimalPlaces)
: market.data
? addDecimalsFormatNumber(market.data.markPrice, market.decimalPlaces)
: '-';
return (
<div className="flex flex-nowrap justify-between items-center mt-1">
<div className="w-1/2">
<div
className="text-ellipsis whitespace-nowrap overflow-hidden"
data-testid="market-item-price"
>
{price}
</div>
{market.candles && (
<PriceChange candles={market.candles.map((c) => c.close)} />
)}
</div>
<div className="w-1/2 max-w-[120px]">
{market.candles ? (
<Sparkline
width={120}
height={20}
data={market.candles.filter(Boolean).map((c) => Number(c.close))}
/>
) : (
'-'
)}
</div>
</div>
);
};
const PriceChange = ({ candles }: { candles: string[] }) => {
const priceChange = candles ? priceChangePercentage(candles) : undefined;
const priceChangeClasses = classNames('text-xs', {
'text-vega-pink': priceChange && priceChange < 0,
'text-vega-green': priceChange && priceChange > 0,
});
let prefix = '';
if (priceChange && priceChange > 0) {
prefix = '+';
}
const formattedChange = formatNumber(Number(priceChange), 2);
return (
<div className={priceChangeClasses} data-testid="market-item-change">
{priceChange ? `${prefix}${formattedChange}%` : '-'}
</div>
);
};
@@ -0,0 +1,288 @@
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 { MarketState } from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
import type { SortType } from './sort-dropdown';
import { SortTypeMapping } from './sort-dropdown';
import { Sort } from './sort-dropdown';
import { subDays } from 'date-fns';
import { isMarketActive } from './use-market-selector-list';
jest.mock('@vegaprotocol/markets');
const mockUseMarketList = useMarketList as jest.Mock;
// mock market list items to avoid subscriptions starting
jest.mock('./market-selector-item', () => ({
MarketSelectorItem: (props: { market: { id: string } }) => (
<div data-testid={props.market.id} />
),
}));
// without a real DOM autosize won't render with an actual height or width
jest.mock('react-virtualized-auto-sizer', () => {
// eslint-disable-next-line react/display-name
return ({
children,
}: {
children: (size: { width: number; height: number }) => ReactNode;
}) => <div>{children({ width: 300, height: 1000 })}</div>;
});
describe('MarketSelector', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'a',
name: 'a',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '200' }],
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
tradableInstrument: {
instrument: {
code: 'b',
name: 'b',
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '400' }],
marketTimestamps: {
open: subDays(new Date(), 2).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_ACTIVE,
tradableInstrument: {
instrument: {
code: 'c',
name: 'c',
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '1000' }],
marketTimestamps: {
open: subDays(new Date(), 0).toISOString(),
},
}),
createMarketFragment({
id: 'market-4',
tradableInstrument: {
instrument: {
code: 'cd',
name: 'cd',
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
// @ts-ignore candles get joined outside this type
candles: [{ close: '100' }, { close: '300' }],
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
];
const activeMarkets = markets.filter((m) => isMarketActive(m.state));
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
it('renders only active markets', () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.getByRole('link')).toHaveTextContent('All markets');
});
it('filters by product type', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('product-Spot'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Spot markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Perpetual'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(0);
expect(screen.getByTestId('no-items')).toHaveTextContent(
'Perpetual markets coming soon.'
);
await userEvent.click(screen.getByTestId('product-Future'));
expect(screen.queryAllByTestId(/market-\d/)).toHaveLength(
activeMarkets.length
);
expect(screen.queryByTestId('no-items')).not.toBeInTheDocument();
});
it('filters by search term', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.type(screen.getByTestId('search-term'), 'zzz');
expect(screen.getByTestId('no-items')).toHaveTextContent('No markets');
const input = screen.getByTestId('search-term');
await userEvent.clear(input);
await userEvent.type(input, 'a');
expect(input).toHaveValue('a');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'b');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-1')).toBeInTheDocument();
await userEvent.clear(input);
await userEvent.type(input, 'c');
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
expect(screen.getByTestId('market-4')).toBeInTheDocument();
});
it('filters by asset', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('asset-trigger'));
expect(screen.getAllByTestId(/asset-id/)).toHaveLength(3);
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(2);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
// reopen asset dropdown and add asset-1
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-1'));
// all markets with asset-0 or asset-1 shown (no market id as market is closed)
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(3);
expect(screen.getByTestId('market-0')).toBeInTheDocument();
expect(screen.getByTestId('market-1')).toBeInTheDocument();
expect(screen.getByTestId('market-3')).toBeInTheDocument();
// reopen and uncheck asset-0
await userEvent.click(screen.getByTestId('asset-trigger'));
await userEvent.click(screen.getByTestId('asset-id-asset-0'));
expect(screen.getAllByTestId(/market-\d/)).toHaveLength(1);
expect(screen.getByTestId('market-3')).toBeInTheDocument();
});
it('sorts by gained', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
const options = screen.getAllByTestId(/sort-item/);
expect(options.map((o) => o.textContent)).toEqual(
Object.entries(Sort)
.filter(([key]) => key !== Sort.None)
.map(([key]) => SortTypeMapping[key as SortType])
);
await userEvent.click(screen.getByTestId('sort-item-Gained'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[1].id, markets[4].id, markets[0].id]);
});
it('sorts by lost', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-Lost'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[0].id, markets[4].id, markets[1].id, markets[3].id]);
});
it('sorts by new', async () => {
render(
<MemoryRouter>
<MarketSelector currentMarketId="market-0" />
</MemoryRouter>
);
await userEvent.click(screen.getByTestId('sort-trigger'));
await userEvent.click(screen.getByTestId('sort-item-New'));
expect(
screen
.getAllByTestId(/market-\d/)
.map((el) => el.getAttribute('data-testid'))
).toEqual([markets[3].id, markets[0].id, markets[1].id, markets[4].id]);
});
});
@@ -0,0 +1,276 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
import {
Input,
TinyScroll,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { CSSProperties } from 'react';
import { useState } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useMarketSelectorList } from './use-market-selector-list';
import type { ProductType } from './product-selector';
import { Product, ProductSelector } from './product-selector';
import { AssetDropdown } from './asset-dropdown';
import type { SortType } from './sort-dropdown';
import { Sort, SortDropdown } from './sort-dropdown';
import { MarketSelectorItem } from './market-selector-item';
export type Filter = {
searchTerm: string;
product: ProductType;
sort: SortType;
assets: string[];
};
/**
* Fetches market data and filters it given a set of filter properties
* defined in Filter
*/
export const MarketSelector = ({
currentMarketId,
onSelect,
}: {
currentMarketId?: string;
onSelect?: (marketId: string) => void;
}) => {
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
const { markets, data, loading, error } = useMarketSelectorList(filter);
return (
<div
className="grid grid-rows-[min-content_1fr_min-content] h-full"
data-testid="market-selector"
>
<div className="px-4 pt-2 pb-4">
<ProductSelector
product={filter.product}
onSelect={(product) => {
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm flex gap-1 items-stretch">
<div className="flex-1">
<Input
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
value={filter.searchTerm}
type="text"
placeholder={t('Search')}
data-testid="search-term"
className="w-full"
appendElement={
filter.searchTerm.length ? (
<button
onClick={() =>
setFilter((curr) => ({ ...curr, searchTerm: '' }))
}
className="text-vega-light-200 dark:text-vega-dark-200"
>
<VegaIcon name={VegaIconNames.CROSS} />
</button>
) : (
<span />
)
}
/>
</div>
<AssetDropdown
assets={uniqBy(
data?.map(
(d) => d.tradableInstrument.instrument.product.settlementAsset
),
'id'
)}
checkedAssets={filter.assets}
onSelect={(id: string, checked) => {
setFilter((curr) => {
if (checked) {
if (curr.assets.includes(id)) {
return curr;
} else {
return { ...curr, assets: [...curr.assets, id] };
}
} else {
if (curr.assets.includes(id)) {
return {
...curr,
assets: curr.assets.filter((x) => x !== id),
};
}
}
return curr;
});
}}
onReset={() => setFilter((curr) => ({ ...curr, assets: [] }))}
/>
<SortDropdown
currentSort={filter.sort}
onSelect={(sort) => {
setFilter((curr) => {
if (curr.sort === sort) {
return { ...curr, sort: Sort.None };
}
return {
...curr,
sort,
};
});
}}
onReset={() => setFilter((curr) => ({ ...curr, sort: Sort.None }))}
/>
</div>
</div>
<div data-testid="market-selector-list">
<MarketList
data={markets}
loading={loading}
error={error}
searchTerm={filter.searchTerm}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={
filter.product === Product.Perpetual
? t('Perpetual markets coming soon.')
: filter.product === Product.Spot
? t('Spot markets coming soon.')
: t('No markets')
}
/>
</div>
<div className="px-4 py-2">
<span className="inline-block border-b border-black dark:border-white">
<Link to={'/markets/all'} className="flex items-center gap-x-2">
{t('All markets')}
<VegaIcon name={VegaIconNames.ARROW_RIGHT} />
</Link>
</span>
</div>
</div>
);
};
const MarketList = ({
data,
error,
loading,
currentMarketId,
onSelect,
noItems,
}: {
data: MarketMaybeWithDataAndCandles[];
error: Error | undefined;
loading: boolean;
searchTerm: string;
currentMarketId?: string;
onSelect?: (marketId: string) => void;
noItems: string;
}) => {
if (error) {
return <div>{error.message}</div>;
}
return (
<AutoSizer>
{({ width, height }) => (
<TinyScroll>
<List
data={data}
loading={loading}
width={width}
height={height}
currentMarketId={currentMarketId}
onSelect={onSelect}
noItems={noItems}
/>
</TinyScroll>
)}
</AutoSizer>
);
};
const List = ({
data,
loading,
width,
height,
onSelect,
noItems,
currentMarketId,
}: {
data: MarketMaybeWithDataAndCandles[];
loading: boolean;
width: number;
height: number;
noItems: string;
onSelect?: (marketId: string) => void;
currentMarketId?: string;
}) => {
const row = ({ index, style }: { index: number; style: CSSProperties }) => {
const market = data[index];
return (
<MarketSelectorItem
market={market}
currentMarketId={currentMarketId}
style={style}
onSelect={onSelect}
/>
);
};
if (!data || loading) {
return (
<div style={{ width, height }}>
<Skeleton />
<Skeleton />
</div>
);
}
if (!data.length) {
return (
<div style={{ width, height }} data-testid="no-items">
<div className="mb-2 px-4">
<div className="text-sm bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg px-4 py-2">
{noItems}
</div>
</div>
</div>
);
}
return (
<FixedSizeList
className="virtualized-list"
itemCount={data.length}
itemSize={130}
width={width}
height={height}
>
{row}
</FixedSizeList>
);
};
const Skeleton = () => {
return (
<div className="mb-2 px-2">
<div className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-lg p-4">
<div className="w-full h-3 bg-vega-light-200 dark:bg-vega-dark-200 mb-2" />
<div className="w-2/3 h-3 bg-vega-light-200 dark:bg-vega-dark-200" />
</div>
</div>
);
};
+4 -2
View File
@@ -7,9 +7,10 @@ import {
useThrottledDataProvider,
} from '@vegaprotocol/data-provider';
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
import { marketProvider, marketDataProvider } from '@vegaprotocol/markets';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid, TradePanels } from './trade-grid';
import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -100,6 +101,7 @@ export const MarketPage = () => {
/>
);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@@ -0,0 +1,50 @@
import classNames from 'classnames';
// Make sure these match the available __typename properties on product
export const Product = {
Spot: 'Spot',
Future: 'Future',
Perpetual: 'Perpetual',
} as const;
export type ProductType = keyof typeof Product;
const ProductTypeMapping: {
[key in ProductType]: string;
} = {
[Product.Spot]: 'Spot',
[Product.Future]: 'Futures',
[Product.Perpetual]: 'Perpetuals',
};
export const ProductSelector = ({
product,
onSelect,
}: {
product: ProductType;
onSelect: (product: ProductType) => void;
}) => {
return (
<div className="flex gap-3 mb-3">
{Object.keys(Product).map((t) => {
const classes = classNames('py-1 border-b-2', {
'border-vega-yellow text-black dark:text-white': t === product,
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
t !== product,
});
return (
<button
key={t}
onClick={() => {
onSelect(t as ProductType);
}}
className={classes}
data-testid={`product-${t}`}
>
{ProductTypeMapping[t as ProductType]}
</button>
);
})}
</div>
);
};
@@ -0,0 +1,76 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuItemIndicator,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
export const Sort = {
None: 'None',
Gained: 'Gained',
Lost: 'Lost',
New: 'New',
} as const;
export type SortType = keyof typeof Sort;
export const SortTypeMapping: {
[key in SortType]: string;
} = {
[Sort.None]: 'None',
[Sort.Gained]: 'Top gaining',
[Sort.Lost]: 'Top losing',
[Sort.New]: 'New markets',
};
export const SortDropdown = ({
currentSort,
onSelect,
onReset,
}: {
currentSort: SortType;
onSelect: (sort: SortType) => void;
onReset: () => void;
}) => {
return (
<DropdownMenu
trigger={
<DropdownMenuTrigger data-testid="sort-trigger">
<VegaIcon name={VegaIconNames.TREND_UP} />
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
<DropdownMenuRadioGroup
value={currentSort}
onValueChange={(value) => onSelect(value as SortType)}
>
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
<DropdownMenuSeparator />
{Object.keys(Sort)
.filter((s) => s !== Sort.None)
.map((key) => {
return (
<DropdownMenuRadioItem
inset
key={key}
value={key}
data-testid={`sort-item-${key}`}
>
{SortTypeMapping[key as SortType]}
<DropdownMenuItemIndicator />
</DropdownMenuRadioItem>
);
})}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};
+66 -195
View File
@@ -1,120 +1,37 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import { memo, useState } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { memo, useState } from 'react';
import type { ReactNode, ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { OracleBanner } from '@vegaprotocol/market-info';
import {
Tab,
LocalStoragePersistTabs as Tabs,
Splash,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { AccountsContainer } from '../../components/accounts-container';
import type { Market } from '@vegaprotocol/market-list';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { TradeMarketHeader } from './trade-market-header';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { t } from '@vegaprotocol/i18n';
import { OracleBanner } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { Filter } from '@vegaprotocol/orders';
import {
usePaneLayout,
useScreenDimensions,
} from '@vegaprotocol/react-helpers';
import {
Tab,
LocalStoragePersistTabs as Tabs,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import { VegaWalletContainer } from '../../components/vega-wallet-container';
import { HeaderTitle } from '../../components/header';
import {
ResizableGrid,
ResizableGridPanel,
} from '../../components/resizable-grid';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
type TradingView = keyof typeof TradingViews;
import { TradingViews } from './trade-views';
import { MarketSelector } from './market-selector';
import { HeaderStats } from './header-stats';
interface TradeGridProps {
market: Market | null;
@@ -410,18 +327,57 @@ const MainGrid = memo(
);
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({
market,
onSelect,
pinnedAsset,
}: TradeGridProps) => {
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_min-content_1fr]',
'grid-cols-[320px_1fr]'
);
const paneWrapperClasses = classNames('min-h-0', {
'col-span-2 col-start-1': !sidebarOpen,
});
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<div className={wrapperClasses}>
<div className="border-b border-r border-default">
<div className="flex gap-2 justify-between items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button
onClick={() => setSidebarOpen((x) => !x)}
className="p-2"
data-testid="sidebar-toggle"
>
<span
className={classNames('block', {
'rotate-90 translate-x-1': !sidebarOpen,
'-rotate-90 -translate-x-1': sidebarOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
</div>
<div className="border-b border-default min-w-0">
<HeaderStats market={market} />
</div>
<div className="col-span-2 bg-vega-green">
<OracleBanner marketId={market?.id || ''} />
</div>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
{sidebarOpen && (
<div className="border-r border-default min-h-0">
<div className="h-full pb-8">
<MarketSelector currentMarketId={market?.id} />
</div>
</div>
)}
<div className={paneWrapperClasses}>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
);
};
@@ -439,88 +395,3 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
</section>
);
};
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
</div>
);
};
@@ -0,0 +1,146 @@
import type { PinnedAsset } from '@vegaprotocol/accounts';
import type { Market } from '@vegaprotocol/markets';
import { OracleBanner } from '@vegaprotocol/markets';
import {
useMarketClickHandler,
useMarketLiquidityClickHandler,
} from '../../lib/hooks/use-market-click-handler';
import type { TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { memo, useState } from 'react';
import {
Icon,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { NO_MARKET } from './constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import { HeaderStats } from './header-stats';
import * as DialogPrimitives from '@radix-ui/react-dialog';
import { HeaderTitle } from '../../components/header';
import { MarketSelector } from './market-selector';
interface TradePanelsProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const [drawerOpen, setDrawerOpen] = useState(false);
const onMarketClick = useMarketClickHandler(true);
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = memo<{
marketId: string;
onSelect: (marketId: string, metaKey?: boolean) => void;
onMarketClick?: (marketId: string) => void;
onOrderTypeClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view].component);
if (!Component) {
throw new Error(`No component for view: ${view}`);
}
if (!market) return <Splash>{NO_MARKET}</Splash>;
return (
<Component
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
/>
);
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="border-b border-default min-w-0">
<div className="flex gap-4 items-center px-4 py-2">
<HeaderTitle
primaryContent={market?.tradableInstrument.instrument.code}
secondaryContent={market?.tradableInstrument.instrument.name}
/>
<button onClick={() => setDrawerOpen((x) => !x)} className="p-2">
<span
className={classNames('block', {
'rotate-90 translate-x-1': !drawerOpen,
'-rotate-90 -translate-x-1': drawerOpen,
})}
>
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
</span>
</button>
</div>
<HeaderStats market={market} />
</div>
<div>
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews).map((key) => {
const isActive = view === key;
const className = classNames('p-4 min-w-[100px] capitalize', {
'text-black dark:text-vega-yellow': isActive,
'bg-neutral-200 dark:bg-neutral-800': isActive,
});
return (
<button
data-testid={key}
onClick={() => setView(key as TradingView)}
className={className}
key={key}
>
{TradingViews[key as keyof typeof TradingViews].label}
</button>
);
})}
</div>
<DialogPrimitives.Root open={drawerOpen} onOpenChange={setDrawerOpen}>
<DialogPrimitives.Portal>
<DialogPrimitives.Overlay />
<DialogPrimitives.Content
className={classNames(
'fixed h-full max-w-[500px] w-[90vw] z-10 top-0 left-0 transition-transform',
'bg-white dark:bg-black',
'border-r border-default'
)}
>
<DialogPrimitives.Close className="absolute top-0 right-0 p-2">
<Icon name="cross" />
</DialogPrimitives.Close>
{drawerOpen && (
<MarketSelector onSelect={() => setDrawerOpen(false)} />
)}
</DialogPrimitives.Content>
</DialogPrimitives.Portal>
</DialogPrimitives.Root>
</div>
);
};
@@ -0,0 +1,90 @@
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { MarketInfoAccordionContainer } from '@vegaprotocol/markets';
import { OrderbookContainer } from '@vegaprotocol/market-depth';
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
import type { OrderListContainerProps } from '@vegaprotocol/orders';
import { FillsContainer } from '@vegaprotocol/fills';
import { PositionsContainer } from '@vegaprotocol/positions';
import { TradesContainer } from '@vegaprotocol/trades';
import type { ComponentProps } from 'react';
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { AccountsContainer } from '../../components/accounts-container';
import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
type MarketDependantView =
| typeof CandlesChartContainer
| typeof DepthChartContainer
| typeof DealTicketContainer
| typeof MarketInfoAccordionContainer
| typeof OrderbookContainer
| typeof TradesContainer;
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
const requiresMarket = (View: MarketDependantView) => {
const WrappedComponent = (props: MarketDependantViewProps) =>
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
return WrappedComponent;
};
export type TradingView = keyof typeof TradingViews;
export const TradingViews = {
candles: {
label: 'Candles',
component: requiresMarket(CandlesChartContainer),
},
depth: {
label: 'Depth',
component: requiresMarket(DepthChartContainer),
},
liquidity: {
label: 'Liquidity',
component: requiresMarket(LiquidityContainer),
},
ticket: {
label: 'Ticket',
component: requiresMarket(DealTicketContainer),
},
info: {
label: 'Info',
component: requiresMarket(MarketInfoAccordionContainer),
},
orderbook: {
label: 'Orderbook',
component: requiresMarket(OrderbookContainer),
},
trades: {
label: 'Trades',
component: requiresMarket(TradesContainer),
},
positions: { label: 'Positions', component: PositionsContainer },
activeOrders: {
label: 'Active',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Open} />
),
},
closedOrders: {
label: 'Closed',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Closed} />
),
},
rejectedOrders: {
label: 'Rejected',
component: (props: OrderListContainerProps) => (
<OrderListContainer {...props} filter={Filter.Rejected} />
),
},
orders: {
label: 'All',
component: OrderListContainer,
},
collateral: { label: 'Collateral', component: AccountsContainer },
fills: { label: 'Fills', component: FillsContainer },
};
@@ -0,0 +1,480 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import {
isMarketActive,
useMarketSelectorList,
} from './use-market-selector-list';
import { Product } from './product-selector';
import { Sort } from './sort-dropdown';
import { createMarketFragment } from '@vegaprotocol/mock';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/markets');
const mockUseMarketList = useMarketList as jest.Mock;
describe('useMarketSelectorList', () => {
const setup = (initialArgs?: Partial<Filter>) => {
const defaultArgs: Filter = {
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
};
return renderHook((args) => useMarketSelectorList(args), {
initialProps: merge(defaultArgs, initialArgs),
});
};
it('returns all markets active and suspended markets', () => {
const markets = [
createMarketFragment({ id: 'market-0' }),
createMarketFragment({
id: 'market-1',
state: MarketState.STATE_SUSPENDED,
}),
createMarketFragment({
id: 'market-2',
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-3',
state: MarketState.STATE_CLOSED,
}),
createMarketFragment({
id: 'market-4',
state: MarketState.STATE_PENDING,
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) =>
isMarketActive(m.state)
);
expect(result.current).toEqual({
data: markets,
markets: expectedFilteredMarkets,
loading: false,
error: undefined,
});
});
it('filters by product', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
__typename: 'Future',
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
__typename: 'Spot' as 'Future', // spot isn't in schema yet
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
__typename: 'Perpetual' as 'Future', // spot isn't in schema yet
},
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup();
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: '',
product: Product.Spot as 'Future',
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1]]);
rerender({
searchTerm: '',
product: Product.Perpetual as 'Future',
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
});
it('filters by asset', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-0',
},
},
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-1',
},
},
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
product: {
settlementAsset: {
id: 'asset-2',
},
},
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0'],
});
expect(result.current.markets).toEqual([markets[0], markets[1]]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1'],
});
expect(result.current.markets).toEqual([
markets[0],
markets[1],
markets[2],
]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-0', 'asset-1', 'asset-2'],
});
// all assets selected
expect(result.current.markets).toEqual(markets);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: ['asset-invalid'],
});
expect(result.current.markets).toEqual([]);
});
it('filters by search term', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'abc',
name: 'aaa',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'def',
name: 'ggg',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'defg',
name: 'gggh',
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
code: 'ggg',
name: 'foo',
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: 'abc',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'def',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[1], markets[2]]);
rerender({
searchTerm: 'defg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[2]]);
rerender({
searchTerm: 'zzz',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([]);
// by name
rerender({
searchTerm: 'aaa',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([markets[0]]);
rerender({
searchTerm: 'ggg',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[3],
]);
});
it('sorts albphabetically', () => {
const markets = [
createMarketFragment({
id: 'market-0',
tradableInstrument: {
instrument: {
code: 'd',
},
},
}),
createMarketFragment({
id: 'market-1',
tradableInstrument: {
instrument: {
code: 'b',
},
},
}),
createMarketFragment({
id: 'market-2',
tradableInstrument: {
instrument: {
code: 'a',
},
},
}),
createMarketFragment({
id: 'market-3',
tradableInstrument: {
instrument: {
code: 'c',
},
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.None,
assets: [],
});
expect(result.current.markets).toEqual([
markets[2],
markets[1],
markets[3],
markets[0],
]);
});
it('sorts by gained', () => {
const markets = [
createMarketFragment({
id: 'market-0',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '200',
},
],
}),
createMarketFragment({
id: 'market-1',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '1000',
},
],
}),
createMarketFragment({
id: 'market-2',
// @ts-ignore actual fragment doesnt contain candles and is joined later
candles: [
{
close: '100',
},
{
close: '400',
},
],
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result, rerender } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.Gained,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[0],
]);
rerender({
searchTerm: '',
product: Product.Future,
sort: Sort.Lost as 'Gained',
assets: [],
});
expect(result.current.markets).toEqual([
markets[0],
markets[2],
markets[1],
]);
});
it('sorts by open timestamp', () => {
const markets = [
createMarketFragment({
id: 'market-0',
marketTimestamps: {
open: subDays(new Date(), 3).toISOString(),
},
}),
createMarketFragment({
id: 'market-1',
marketTimestamps: {
open: subDays(new Date(), 1).toISOString(),
},
}),
createMarketFragment({
id: 'market-2',
marketTimestamps: {
open: subDays(new Date(), 2).toISOString(),
},
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup({
searchTerm: '',
product: Product.Future,
sort: Sort.New,
assets: [],
});
expect(result.current.markets).toEqual([
markets[1],
markets[2],
markets[0],
]);
});
});
@@ -0,0 +1,86 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/markets';
import { priceChangePercentage } from '@vegaprotocol/utils';
import type { Filter } from './market-selector';
import { Sort } from './sort-dropdown';
export const useMarketSelectorList = ({
product,
assets,
sort,
searchTerm,
}: Filter) => {
const { data, loading, error } = useMarketList();
const markets = useMemo(() => {
if (!data?.length) return [];
const markets = data
// only active
.filter((m) => isMarketActive(m.state))
// only selected product type
.filter((m) => {
if (m.tradableInstrument.instrument.product.__typename === product) {
return true;
}
return false;
})
.filter((m) => {
if (assets.length === 0) return true;
return assets.includes(
m.tradableInstrument.instrument.product.settlementAsset.id
);
})
// filter based on search term
.filter((m) => {
const code = m.tradableInstrument.instrument.code.toLowerCase();
const name = m.tradableInstrument.instrument.name.toLowerCase();
if (
code.includes(searchTerm.toLowerCase()) ||
name.includes(searchTerm.toLowerCase())
) {
return true;
}
return false;
});
if (sort === Sort.None) {
return orderBy(markets, ['tradableInstrument.instrument.code'], ['asc']);
}
if (sort === Sort.Gained || sort === Sort.Lost) {
const dir = sort === Sort.Gained ? 'desc' : 'asc';
return orderBy(
markets,
[
(m) => {
if (!m.candles?.length) return 0;
return Number(priceChangePercentage(m.candles.map((c) => c.close)));
},
],
[dir]
);
}
if (sort === Sort.New) {
return orderBy(
markets,
[(m) => new Date(m.marketTimestamps.open).getTime()],
['desc']
);
}
return markets;
}, [data, product, searchTerm, assets, sort]);
return { markets, data, loading, error };
};
export const isMarketActive = (state: MarketState) => {
return [
MarketState.STATE_ACTIVE,
MarketState.STATE_SUSPENDED,
MarketState.STATE_PENDING,
].includes(state);
};
@@ -6,8 +6,16 @@ import { MarketState } from '@vegaprotocol/types';
import { subDays } from 'date-fns';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
import type {
OracleSpecDataConnectionQuery,
MarketsDataQuery,
MarketsQuery,
} from '@vegaprotocol/markets';
import {
OracleSpecDataConnectionDocument,
MarketsDataDocument,
MarketsDocument,
} from '@vegaprotocol/markets';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type {
@@ -16,9 +24,6 @@ import type {
} from '@vegaprotocol/positions';
import { PositionsDocument } from '@vegaprotocol/positions';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import type { MarketsDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
import { MarketsDataDocument } from '@vegaprotocol/market-list';
import { MarketsDocument } from '@vegaprotocol/market-list';
import {
createMarketFragment,
marketsQuery,
@@ -214,7 +219,7 @@ describe('Closed', () => {
'Settlement price',
'Realised PNL',
'Settlement asset',
'Market ID',
'', // actions row
];
expect(headers).toHaveLength(expectedHeaders.length);
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
@@ -236,7 +241,7 @@ describe('Closed', () => {
addDecimalsFormatNumber(property.value, market.decimalPlaces),
addDecimalsFormatNumber(position.realisedPNL, market.decimalPlaces),
market.tradableInstrument.instrument.product.settlementAsset.symbol,
market.id,
'', // actions row
];
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
@@ -328,8 +333,13 @@ describe('Closed', () => {
// check that only included ids are shown
const cells = screen
.getAllByRole('gridcell')
.filter((cell) => cell.getAttribute('col-id') === 'id')
.map((cell) => cell.textContent?.trim());
.filter((cell) => cell.getAttribute('col-id') === 'code')
.map((cell) => {
const marketId = within(cell)
.getByTestId('market-code')
.getAttribute('data-market-id');
return marketId;
});
expect(cells).toEqual(expectedRows.map((m) => m.node.id));
});
});
+27 -6
View File
@@ -4,7 +4,7 @@ import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import { AgGridLazy as AgGrid, COL_DEFS } from '@vegaprotocol/datagrid';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/i18n';
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
@@ -13,8 +13,11 @@ import {
getMarketExpiryDate,
} from '@vegaprotocol/utils';
import { usePositionsQuery } from '@vegaprotocol/positions';
import type { MarketMaybeWithData } from '@vegaprotocol/market-list';
import { closedMarketsWithDataProvider } from '@vegaprotocol/market-list';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import {
MarketTableActions,
closedMarketsWithDataProvider,
} from '@vegaprotocol/markets';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { ColDef } from 'ag-grid-community';
@@ -121,6 +124,16 @@ const ClosedMarketsDataGrid = ({ rowData }: { rowData: Row[] }) => {
{
headerName: t('Market'),
field: 'code',
cellRenderer: ({
value,
data,
}: VegaICellRendererParams<Row, 'code'>) => {
return (
<span data-testid="market-code" data-market-id={data?.id}>
{value}
</span>
);
},
},
{
headerName: t('Description'),
@@ -262,9 +275,17 @@ const ClosedMarketsDataGrid = ({ rowData }: { rowData: Row[] }) => {
),
},
{
headerName: t('Market ID'),
field: 'id',
flex: 1,
colId: 'market-actions',
...COL_DEFS.actions,
cellRenderer: ({ data }: VegaICellRendererParams<Row>) => {
if (!data) return null;
return (
<MarketTableActions
marketId={data.id}
assetId={data.settlementAsset.id}
/>
);
},
},
];
return cols;
@@ -1,4 +1,4 @@
import { MarketsContainer } from '@vegaprotocol/market-list';
import { MarketsContainer } from '@vegaprotocol/markets';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
export const Markets = () => {

Some files were not shown because too many files have changed in this diff Show More