Compare commits

..
Author SHA1 Message Date
Madalina Raicu ced248345d fix: release script for mainnet 2023-05-09 16:55:36 +01:00
272 changed files with 3517 additions and 6270 deletions
+3 -3
View File
@@ -5,8 +5,6 @@ on:
branches:
- release/*
- develop
tags:
- v*
pull_request:
types:
- opened
@@ -99,13 +97,15 @@ jobs:
- name: See affected apps
run: |
echo ">>>> debug"
echo "NX Version: $nx_version"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo ">>>> eof debug"
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
echo -n "Affected projects: $affected"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
branch_slug="$(echo ${{ github.head_ref || github.ref_name }} | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
projects_e2e=""
preview_governance="not deployed"
preview_trading="not deployed"
+1
View File
@@ -33,4 +33,5 @@ jobs:
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
######
- name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
+35 -91
View File
@@ -30,21 +30,13 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
- name: Log in to the Container registry
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
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node
uses: actions/setup-node@v3
with:
@@ -62,24 +54,30 @@ jobs:
- name: Define variables
run: |
envName=''
dockerfile="dist.Dockerfile"
if [[ "${{ github.event_name }}" = "push" ]]; then
domain="vega.rocks"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
domain="vega.community"
if [[ "${{ matrix.app }}" = "trading" ]]; then
dockerfile="ipfs.Dockerfile"
fi
fi
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
elif [[ "${{ matrix.app}}" = "trading" ]] && [[ ${{ startsWith(github.ref, 'refs/tags/v') && 'true' || 'false' }} = "true" ]]; then
envName="mainnet"
fi
bucketName="${{ matrix.app }}.${envName}.${domain}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
fi
nodeVersion=$(cat .nvmrc | head -n 1)
echo ENV_NAME=${envName} >> $GITHUB_ENV
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
- name: Build local dist
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
@@ -100,60 +98,62 @@ jobs:
- name: Build and export to local Docker
id: docker_build
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
uses: docker/build-push-action@v3
with:
context: .
file: docker/node-outside-docker.Dockerfile
file: ${{ env.DOCKERFILE }}
load: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' }}
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
fi
- name: Publish dist as docker image (ghcr)
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
- name: Copy dist to local filesystem
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
run: |
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
docker cp dist:/usr/share/nginx/html dist
echo "check local dist files"
tree dist/html
mv dist/html dist-result
- name: Publish dist as docker image
uses: docker/build-push-action@v3
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
if: ${{ github.event_name == 'pull_request' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
file: ${{ env.DOCKERFILE }}
push: true
build-args: |
APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub)
uses: docker/build-push-action@v3
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.ref_name }}
vegaprotocol/${{ matrix.app }}:mainnet
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') }}
if: ${{ github.event_name == 'push' }}
with:
args: --acl private --follow-symlinks --delete
env:
@@ -169,59 +169,3 @@ jobs:
with:
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: |
# 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: Checkout vega.xyz
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
# 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
-1
View File
@@ -2,7 +2,6 @@
# compiled output
/dist
/dist-result
/tmp
/out-tsc
/tools/executors/**/*.js
-20
View File
@@ -1,20 +0,0 @@
.PHONY: latest-release
latest-release:
gh release list | head -n 1 | awk '{print $1}'
.PHONY: show-latest-release
show-latest-release:
gh release view `gh release list | head -n 1 | awk '{print $1}'`
.PHONY: recalculate-ipfs
recalculate-ipfs:
echo "ipfs hash inside the image"
docker run --rm ${TAG} cat /ipfs-hash
echo "recalculating ipfs hash"
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
.PHONY: eject-ipfs-hash
unpack:
docker create --name=dist ${TAG}
docker cp dist:/usr/share/nginx/html dist
docker rm dist
+12 -55
View File
@@ -103,68 +103,25 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console
# Docker & Vegacapsule
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
## Docker
In order to run a container on port 3000:
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
```
In order to run a container:
```bash
docker run -p 3000:80 [TAG]
```
## Build instructions
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
### nx build outside the docker
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
```bash
./docker/prepare-dist.sh
```
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
```
### nx build inside the docker
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build) example.
```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
```
### Computing ipfs-hash of the build
At the moment this feature is important only for `trading` (console) releases.
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
```bash
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
```
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
### Verifying ipfs-hash of existing current application version
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
4. Download docker image with the desired release `docker pull $TAG`.
5. Recalculate hash: `make recalculate-ipfs`
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
## Config
-1
View File
@@ -13,7 +13,6 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://stagnet1.governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
# App flags
NX_EXPLORER_ASSETS=1
-1
View File
@@ -2,7 +2,6 @@
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest/
NX_ETHERSCAN_URL=https://etherscan.io
+1 -2
View File
@@ -1,2 +1 @@
# .env is stagnet1, so there are no overrides required
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz","STAGNET3":"https://stagnet3.explorer.vega.xyz"}'
# .env is stagnet1, so there are no overrides required
+2 -24
View File
@@ -1,18 +1,9 @@
import {
AppFailure,
NetworkLoader,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
const splashLoading = (
<Splash>
@@ -21,23 +12,10 @@ const splashLoading = (
);
function App() {
const { VEGA_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
(store) => [store.dialogOpen, store.setDialogOpen]
);
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<RouterProvider router={router} fallbackElement={splashLoading} />
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
<RouterProvider router={router} fallbackElement={splashLoading} />
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -1,19 +1,13 @@
import {
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react';
import { useMemo, useState } from 'react';
import { ENV } from '../../config/env';
export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const setNodeSwitcherOpen = useNodeSwitcherStore(
(store) => store.setDialogOpen
);
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
@@ -21,40 +15,46 @@ export const Footer = () => {
);
return (
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div>
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div>
</footer>
</footer>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</>
);
};
@@ -1,5 +1,3 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([
[51, 'Transaction failed validation'],
@@ -30,11 +28,7 @@ export const ChainResponseCode = ({
}: ChainResponseCodeProps) => {
const isSuccess = successCodes.has(code);
const icon = isSuccess ? (
<Icon name="tick-circle" className="fill-vega-green-550" />
) : (
<Icon name="cross" className="fill-vega-pink-550" />
);
const icon = isSuccess ? '✅' : '❌';
const label = ErrorCodes.get(code) || 'Unknown response code';
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
@@ -42,7 +36,7 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
<div title={`Response code: ${code} - ${label}`} className="inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
@@ -4,7 +4,6 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import ProposalLink from '../../links/proposal-link/proposal-link';
import { VoteIcon } from '../../vote-icon/vote-icon';
interface TxProposalVoteProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -31,22 +30,27 @@ export const TxProposalVote = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const vote = txData.command.voteSubmission.value === 'VALUE_YES';
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal details')}</TableCell>
<TableCell>
<ProposalLink id={txData.command.voteSubmission.proposalId} />
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Vote')}</TableCell>
<TableCell>
<VoteIcon vote={vote} />
</TableCell>
<TableCell>{vote}</TableCell>
</TableRow>
</TableWithTbody>
);
@@ -1,6 +1,5 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../types/explorer';
import { VoteIcon } from '../vote-icon/vote-icon';
interface TxOrderTypeProps {
orderType: string;
@@ -138,15 +137,12 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) {
type = getLabelForChainEvent(command.chainEvent);
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
} else if (type === 'Validator Heartbeat') {
colours =
'text-white dark-text-white bg-vega-light-200 dark:bg-vega-dark-100';
} else if (type === 'Proposal' || type === 'Governance Proposal') {
if (command && !!command.proposalSubmission) {
type = getLabelForProposal(command.proposalSubmission);
@@ -154,16 +150,6 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
colours = 'text-black bg-vega-yellow';
}
if (type === 'Vote on Proposal') {
return (
<VoteIcon
vote={command?.voteSubmission?.value === 'VALUE_YES'}
yesText="Proposal vote"
noText="Proposal vote"
/>
);
}
if (type === 'Vote on Proposal' || type === 'Vote Submission') {
colours = 'text-black bg-vega-yellow';
}
@@ -98,6 +98,6 @@ describe('Txs infinite list item', () => {
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
});
});
@@ -31,7 +31,7 @@ export const TxsInfiniteListItem = ({
return (
<div
data-testid="transaction-row"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
>
<div
className="text-sm col-span-10 md:col-span-3 leading-none"
@@ -83,7 +83,7 @@ export const TxsInfiniteListItem = ({
data-testid="tx-success"
>
<span className="md:hidden uppercase text-vega-dark-300">
Success&nbsp;
Success:&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
@@ -1,37 +0,0 @@
import { render } from '@testing-library/react';
import { VoteIcon } from './vote-icon';
describe('Vote TX icon', () => {
it('should use the text For by default for yes votes', () => {
const yes = render(<VoteIcon vote={true} />);
expect(yes.getByTestId('label')).toHaveTextContent('For');
});
it('should use the yesText for yes votes if specified', () => {
const yes = render(<VoteIcon vote={true} yesText="Test" />);
expect(yes.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the tick icon for yes votes', () => {
const no = render(<VoteIcon vote={true} />);
expect(no.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
it('should use the text Against by default for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByTestId('label')).toHaveTextContent('Against');
});
it('should use the noText for no votes if specified', () => {
const no = render(<VoteIcon vote={false} noText="Test" />);
expect(no.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the delete icon for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByRole('img')).toHaveAttribute('aria-label', 'delete icon');
});
});
@@ -1,40 +0,0 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
export interface VoteIconProps {
// True is a yes vote, false is undefined or no vorte
vote: boolean;
// Defaults to 'For', but can be any text
yesText?: string;
// Defaults to 'Against', but can be any text
noText?: string;
}
/**
* Displays a lozenge with an icon representing the way a user voted for a proposal.
* The yes and no text can be overridden
*
* @returns
*/
export function VoteIcon({
vote,
yesText = 'For',
noText = 'Against',
}: VoteIconProps) {
const label = vote ? yesText : noText;
const bg = vote ? 'bg-vega-green-550' : 'bg-vega-pink-550';
const icon: IconName = vote ? 'tick-circle' : 'delete';
const fill = vote ? 'vega-green-300' : 'vega-pink-300';
const text = vote ? 'vega-green-200' : 'vega-pink-200';
return (
<div
className={`voteicon inline-block my-1 py-1 px-2 py rounded-md text-white leading-one sm align-top ${bg}`}
>
<Icon name={icon} size={3} className={`mr-2 p-0 fill-${fill}`} />
<span className={`text-base text-${text}`} data-testid="label">
{label}
</span>
</div>
);
}
-4
View File
@@ -60,7 +60,3 @@
--ag-row-hover-color: theme(colors.neutral[800]);
--ag-font-size: 12px;
}
.voteicon svg {
vertical-align: baseline;
}
@@ -7,7 +7,6 @@ import {
waitForSpinner,
navigateTo,
navigation,
turnTelemetryOff,
} from '../../support/common.functions';
import {
clickOnValidatorFromList,
@@ -68,7 +67,6 @@ context(
'teardown wallet & drill into a specific validator',
function () {
cy.clearLocalStorage();
turnTelemetryOff();
cy.reload();
waitForSpinner();
cy.connectVegaWallet();
@@ -254,10 +252,10 @@ context(
waitForBeginningOfEpoch();
cy.getByTestId(stakeValidatorListStakePercentage).should(
'have.text',
'50.02%'
'100%'
);
navigateTo(navigation.validators);
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
}
);
@@ -109,7 +109,7 @@ context(
);
cy.getByTestId('protocol-upgrade-proposal-status').should(
'have.text',
'Approved by validators '
'Approved '
);
});
});
@@ -136,7 +136,7 @@ context(
);
cy.getByTestId('protocol-upgrade-state').should(
'have.text',
'Approved by validators'
'Approved'
);
cy.getByTestId('protocol-upgrade-release-tag').should(
'have.text',
@@ -29,7 +29,7 @@ const performancePenaltyToolTip = '[data-testid="performance-penalty-tooltip"]';
const overstakedPenaltyToolTip = '[data-testid="overstaked-penalty-tooltip"]';
const totalPenaltyToolTip = '[data-testid="total-penalty-tooltip"]';
const epochCountDown = '[data-testid="epoch-countdown"]';
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\d+)?$/;
const stakeNumberRegex = /^\d*\.?\d*$/;
context('Validators Page - verify elements on page', function () {
before('navigate to validators page', function () {
@@ -84,13 +84,13 @@ context('Validators Page - verify elements on page', function () {
cy.get(stakedByOperatorToolTip)
.invoke('text')
.should('contain', 'Staked by operator: 3,000.00');
.should('contain', 'Staked by operator: 0.00');
cy.get(stakedByDelegatesToolTip)
.invoke('text')
.should('contain', 'Staked by delegates: 0.00');
cy.get(totalStakedToolTip)
.invoke('text')
.should('contain', 'Total stake: 3,000.00');
.should('contain', 'Total stake: 0.00');
});
it('Should be able to see validator normalised voting power', function () {
@@ -106,10 +106,10 @@ context('Validators Page - verify elements on page', function () {
cy.get(unnormalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Unnormalised voting power: 20.00%');
.should('contain', 'Unnormalised voting power: 0.00%');
cy.get(normalisedVotingPowerToolTip)
.invoke('text')
.should('contain', 'Normalised voting power: 50.00%');
.should('contain', 'Normalised voting power: 0.10%');
});
// 2002-SINC-018
@@ -126,13 +126,13 @@ context('Validators Page - verify elements on page', function () {
cy.get(performancePenaltyToolTip)
.invoke('text')
.should('contain', 'Performance penalty: 0.00%');
.should('contain', 'Performance penalty: 100.00%');
cy.get(overstakedPenaltyToolTip)
.invoke('text')
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
.should('contain', 'Overstaked penalty:'); // value not asserted due to #2886
cy.get(totalPenaltyToolTip)
.invoke('text')
.should('contain', 'Total penalties: 60.00%');
.should('contain', 'Total penalties: 0.00%');
});
it('Should be able to see validator pending stake', function () {
-2
View File
@@ -33,6 +33,4 @@ before(() => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
});
// Self stake validators so they are displayed
cy.validatorsSelfDelegate();
});
@@ -3,6 +3,7 @@ import {
StakingBridge,
Token,
TokenVesting,
TokenFaucetable,
CollateralBridge,
} from '@vegaprotocol/smart-contracts';
import { ethers, Wallet } from 'ethers';
@@ -40,7 +41,7 @@ export async function depositAsset(
decimalPlaces: number
) {
// Approve asset
const faucet = new Token(assetEthAddress, signer);
const faucet = new TokenFaucetable(assetEthAddress, signer);
cy.wrap(
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
{
@@ -61,7 +62,7 @@ export async function depositAsset(
}
export async function faucetAsset(assetEthAddress: string) {
const faucet = new Token(assetEthAddress, signer);
const faucet = new TokenFaucetable(assetEthAddress, signer);
await promiseWithTimeout(faucet.faucet(), 10 * 60 * 1000, 'faucet asset');
}
+27 -90
View File
@@ -2,6 +2,7 @@ import './i18n';
import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { BrowserRouter as Router, useLocation } from 'react-router-dom';
import { AppLoader } from './app-loader';
import { NetworkInfo } from '@vegaprotocol/network-info';
@@ -36,10 +37,6 @@ import {
useEnvironment,
NetworkLoader,
useInitializeEnv,
NodeGuard,
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client';
@@ -51,7 +48,6 @@ import {
TELEMETRY_ON,
} from './components/telemetry-dialog/telemetry-dialog';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
import { useTranslation } from 'react-i18next';
const cache: InMemoryCacheConfig = {
typePolicies: {
@@ -183,83 +179,32 @@ const ScrollToTop = () => {
return null;
};
const removeQueryParams = (url: string) => {
return url.split('?')[0];
};
const AppContainer = () => {
const { config, loading, error } = useEthereumConfig();
const {
VEGA_ENV,
VEGA_URL,
GIT_COMMIT_HASH,
GIT_BRANCH,
ETHEREUM_PROVIDER_URL,
} = useEnvironment();
const { VEGA_ENV, GIT_COMMIT_HASH, GIT_BRANCH, ETHEREUM_PROVIDER_URL } =
useEnvironment();
const [telemetryOn] = useLocalStorage(TELEMETRY_ON);
const { t } = useTranslation();
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen,
store.setDialogOpen,
]);
useEffect(() => {
if (ENV.dsn && telemetryOn === 'true') {
if (ENV.dsn && telemetryOn) {
Sentry.init({
dsn: ENV.dsn,
integrations: [new Integrations.BrowserTracing()],
tracesSampleRate: 0.1,
enabled: true,
environment: VEGA_ENV,
release: GIT_COMMIT_HASH,
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 && error.includes('failed to get party ID')) ||
(errorIsObject &&
error?.message?.includes('failed to get party ID'))
) {
// 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;
beforeSend(event) {
if (event.request?.url?.includes('/claim?')) {
return {
...event,
request: {
...event.request,
url: event.request?.url.split('?')[0],
},
};
}
const updatedRequest =
requestUrl && requestUrl.includes('/claim?')
? { ...event.request, url: removeQueryParams(requestUrl) }
: event.request;
const updatedTransaction =
transaction && transaction.includes('/claim?')
? removeQueryParams(transaction)
: transaction;
const updatedBreadcrumbs = event.breadcrumbs?.map((breadcrumb) => {
if (
breadcrumb.type === 'navigation' &&
breadcrumb.data?.to?.includes('/claim?')
) {
return {
...breadcrumb,
data: {
...breadcrumb.data,
to: removeQueryParams(breadcrumb.data.to),
},
};
}
return breadcrumb;
});
return {
...event,
request: updatedRequest,
transaction: updatedTransaction,
breadcrumbs: updatedBreadcrumbs ?? event.breadcrumbs,
};
return event;
},
});
Sentry.setTag('branch', GIT_BRANCH);
@@ -274,29 +219,21 @@ const AppContainer = () => {
<ScrollToTop />
<AppStateProvider>
<div className="grid min-h-full text-white">
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
<AppFailure title={t('NodeUnsuitable', { url: VEGA_URL })} />
<AsyncRenderer<EthereumConfig | null>
loading={loading}
data={config}
error={error}
render={(cnf) =>
cnf && (
<Web3Container
chainId={Number(cnf.chain_id)}
providerUrl={ETHEREUM_PROVIDER_URL}
/>
)
}
>
<AsyncRenderer<EthereumConfig | null>
loading={loading}
data={config}
error={error}
render={(cnf) =>
cnf && (
<Web3Container
chainId={Number(cnf.chain_id)}
providerUrl={ETHEREUM_PROVIDER_URL}
/>
)
}
/>
</NodeGuard>
/>
</div>
</AppStateProvider>
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
</Router>
);
};
@@ -1,14 +1,23 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
export const EthConnectPrompt = () => {
const { t } = useTranslation();
const { open } = useWeb3ConnectStore();
const { appDispatch } = useAppState();
return (
<Button
variant="default"
onClick={open}
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
fill={true}
data-testid="connect-to-eth-btn"
>
@@ -2,7 +2,10 @@ import { Button } from '@vegaprotocol/ui-toolkit';
import { useWeb3React } from '@web3-react/core';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
export const EthWalletContainer = ({
children,
@@ -11,12 +14,21 @@ export const EthWalletContainer = ({
}) => {
const { account } = useWeb3React();
const { t } = useTranslation();
const { open } = useWeb3ConnectStore();
const { appDispatch } = useAppState();
if (!account) {
return (
<div className="w-full text-center">
<Button onClick={open}>{t('connectEthWallet')}</Button>
<Button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
>
{t('connectEthWallet')}
</Button>
</div>
);
}
@@ -27,11 +27,7 @@ import {
import { Loader } from '@vegaprotocol/ui-toolkit';
import colors from 'tailwindcss/colors';
import { useBalances } from '../../lib/balances/balances-store';
import {
useEthereumConfig,
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
import { getChainName } from '@vegaprotocol/web3';
const removeLeadingAddressSymbol = (key: string) => {
@@ -193,7 +189,6 @@ export const EthWallet = () => {
const pendingTxs = usePendingTransactions();
const disconnect = useWeb3Disconnect(connector);
const { config } = useEthereumConfig();
const { open } = useWeb3ConnectStore();
return (
<WalletCard>
@@ -238,7 +233,12 @@ export const EthWallet = () => {
) : (
<Button
fill={true}
onClick={open}
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
data-testid="connect-to-eth-wallet-button"
>
{t('connectEthWalletToAssociate')}
@@ -25,26 +25,20 @@ export function Web3Connector({
connectors,
chainId,
}: Web3ConnectorProps) {
const { open, close, isOpen } = useWeb3ConnectStore();
const { appState, appDispatch } = useAppState();
const setDialogOpen = useCallback(
(isOpen: boolean) => {
if (isOpen) {
open();
} else {
close();
}
appDispatch({ type: AppStateActionType.SET_ETH_WALLET_OVERLAY, isOpen });
},
[open, close]
[appDispatch]
);
const appChainId = Number(chainId);
return (
<>
<Web3Content appChainId={appChainId}>{children}</Web3Content>
<Web3ConnectDialog
connectors={connectors}
dialogOpen={isOpen}
dialogOpen={appState.ethConnectOverlay}
setDialogOpen={setDialogOpen}
desiredChainId={appChainId}
/>
@@ -34,6 +34,9 @@ export interface AppState {
/** Whether or not the manage VEGA wallet overlay is open */
vegaWalletManageOverlay: boolean;
/** Whether or not the connect to Ethereum wallet overlay is open */
ethConnectOverlay: boolean;
/** Whether or not the transaction modal is open */
transactionOverlay: boolean;
/**
@@ -54,6 +57,7 @@ export enum AppStateActionType {
REFRESH_BALANCES,
SET_VEGA_WALLET_OVERLAY,
SET_VEGA_WALLET_MANAGE_OVERLAY,
SET_ETH_WALLET_OVERLAY,
SET_DRAWER,
REFRESH_ASSOCIATED_BALANCES,
SET_ASSOCIATION_BREAKDOWN,
@@ -77,6 +81,10 @@ export type AppStateAction =
type: AppStateActionType.SET_VEGA_WALLET_MANAGE_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_ETH_WALLET_OVERLAY;
isOpen: boolean;
}
| {
type: AppStateActionType.SET_DRAWER;
isOpen: boolean;
@@ -16,6 +16,7 @@ const initialAppState: AppState = {
totalSupply: new BigNumber(0),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -44,6 +45,12 @@ function appStateReducer(state: AppState, action: AppStateAction): AppState {
vegaWalletOverlay: action.isOpen ? false : state.vegaWalletOverlay,
};
}
case AppStateActionType.SET_ETH_WALLET_OVERLAY: {
return {
...state,
ethConnectOverlay: action.isOpen,
};
}
case AppStateActionType.SET_DRAWER: {
return {
...state,
@@ -202,7 +202,6 @@
"tokenVotes": "Token votes",
"liquidityVotes": "Liquidity votes",
"castYourVote": "Cast your vote",
"yourVote": "Your vote",
"for": "For",
"against": "Against",
"majorityRequired": "Majority Required",
@@ -788,9 +787,9 @@
"homeVegaTokenButtonText": "Manage tokens",
"downloadProposalJson": "Download proposal as JSON",
"networkUpgrade": "Network Upgrade",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved by validators",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Waiting for validator votes",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Declined by validators",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED": "Unspecified",
"vegaRelease{release}": "Vega Release {{release}}",
"upgradeBlockHeight": "Upgrade block height",
@@ -812,6 +811,5 @@
"OptOutOfTelemetry": "You can opt out any time via settings",
"NoThanks": "No thanks",
"ShareData": "Share data",
"ContinueSharingData": "Continue sharing data",
"NodeUnsuitable": "Node: {{url}} is unsuitable"
"ContinueSharingData": "Continue sharing data"
}
@@ -50,22 +50,7 @@ export const useTranches = create<TranchesStore>()((set) => ({
?.map((t) => {
const tranche_progress =
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
let lockedDecimal;
if (t.duration !== 0) {
if (tranche_progress < 0) {
lockedDecimal = 1;
} else {
lockedDecimal = 1 - tranche_progress;
}
} else {
if (now < t.cliff_start) {
lockedDecimal = 1;
} else {
lockedDecimal = 0;
}
}
const clampedLockedDecimal = Math.max(0, Math.min(1, lockedDecimal));
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
return {
tranche_id: t.tranche_id,
tranche_start: secondsToDate(t.cliff_start),
@@ -75,7 +60,7 @@ export const useTranches = create<TranchesStore>()((set) => ({
toBigNum(t.current_balance, decimals)
),
locked_amount: toBigNum(t.initial_balance, decimals).times(
clampedLockedDecimal
lockedDecimal
),
users: t.users,
};
@@ -53,6 +53,7 @@ const mockAppState: AppState = {
totalSupply: new BigNumber(65000000),
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -24,6 +24,7 @@ const openProposalClosesNextMonth = generateProposal({
},
terms: {
closingDatetime: nextMonth.toString(),
enactmentDatetime: nextMonth.toString(),
},
});
@@ -35,6 +36,7 @@ const openProposalClosesNextWeek = generateProposal({
},
terms: {
closingDatetime: nextWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
});
@@ -43,6 +45,7 @@ const enactedProposalClosedLastWeek = generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: lastWeek.toString(),
},
});
@@ -51,6 +54,7 @@ const failedProposalClosedLastMonth = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: lastMonth.toString(),
enactmentDatetime: lastMonth.toString(),
},
});
@@ -32,9 +32,7 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
it('should render the state', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent(
'Waiting for validator votes'
);
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
});
it('should render the vega release tag', () => {
@@ -26,34 +26,28 @@ describe('ProtocolUpgradeProposalsListItem', () => {
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
icon: 'protocol-upgrade-proposal-status-icon-rejected',
text: 'Declined by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
icon: 'protocol-upgrade-proposal-status-icon-pending',
text: 'Waiting for validator votes',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
icon: 'protocol-upgrade-proposal-status-icon-approved',
text: 'Approved by validators',
},
{
status:
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
text: 'Unspecified',
},
];
statuses.forEach(({ status, icon, text }) => {
statuses.forEach(({ status, icon }) => {
renderComponent({ ...proposal, status });
const statusIcon = screen.getByTestId(icon);
const textContent = screen.getByText(text);
expect(statusIcon).toBeInTheDocument();
expect(textContent).toBeInTheDocument();
});
});
@@ -111,9 +111,7 @@ describe('Vote buttons', () => {
</AppStateProvider>
);
expect(
screen.getByText(
'You need some VEGA tokens to participate in governance.'
)
screen.getByText('You need some VEGA tokens to participate in governance')
).toBeTruthy();
});
@@ -2,12 +2,7 @@ import { format } from 'date-fns';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import {
AsyncRenderer,
Button,
ButtonLink,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { AsyncRenderer, Button, ButtonLink } from '@vegaprotocol/ui-toolkit';
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
import { ProposalState, VoteValue } from '@vegaprotocol/types';
import {
@@ -166,12 +161,7 @@ export const VoteButtons = ({
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
<>
{currentStakeAvailable.isLessThanOrEqualTo(0) && (
<>
<p data-testid="no-stake-available">{t('noGovernanceTokens')}.</p>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</>
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
)}
<div className="flex gap-4" data-testid="vote-buttons">
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { RoundedWrapper, Icon } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
@@ -202,12 +202,7 @@ export const VoteDetails = ({
)}
<section className="mt-10">
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
<SubHeading title={t('castYourVote')} />
{pubKey ? (
proposal && (
<VoteButtonsContainer
@@ -229,9 +224,6 @@ export const VoteDetails = ({
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</RoundedWrapper>
@@ -16,6 +16,7 @@ const mockAppState: AppState = {
totalSupply: mockTotalSupply,
vegaWalletOverlay: false,
vegaWalletManageOverlay: false,
ethConnectOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
@@ -20,11 +20,8 @@ import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
arr,
[
(p) => new Date(p?.terms?.closingDatetime).getTime(),
(p) => new Date(p?.datetime).getTime(),
],
['asc', 'asc']
[(p) => new Date(p?.terms?.closingDatetime).getTime(), (p) => p.id],
['desc', 'desc']
);
const orderByUpgradeBlockHeight = (
@@ -54,16 +54,22 @@ export const TrancheItem = ({
{formatNumber(total, 2)}
</span>
</div>
<div className="grid grid-cols-2 my-2">
<div>
<span>{t('Starts unlocking')}:</span>{' '}
<span>{format(tranche.tranche_start, DATE_FORMAT_LONG)}</span>
</div>
<div className="justify-self-end">
<span>{t('Fully unlocked')}:</span>{' '}
<span>{format(tranche.tranche_end, DATE_FORMAT_LONG)}</span>
</div>
</div>
<table className="w-full">
<tbody>
<tr>
<td>{t('Starts unlocking')}</td>
<td className="text-right">
{format(tranche.tranche_start, DATE_FORMAT_LONG)}
</td>
</tr>
<tr>
<td>{t('Fully unlocked')}</td>
<td className="text-right">
{format(tranche.tranche_end, DATE_FORMAT_LONG)}
</td>
</tr>
</tbody>
</table>
<LockedProgress
locked={locked}
unlocked={unlocked}
@@ -79,24 +79,7 @@ export const generateEpochIndividualRewardsList = ({
epoch?.rewards.push(asset);
}
if (!asset.rewardTypes[rewardType]) {
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
} else {
const previousAmount = asset.rewardTypes[rewardType]?.amount;
const previousPercentageOfTotal =
asset.rewardTypes[rewardType]?.percentageOfTotal;
asset.rewardTypes[rewardType] = {
amount: previousAmount
? new BigNumber(previousAmount).plus(amount).toString()
: amount,
percentageOfTotal: previousPercentageOfTotal
? new BigNumber(previousPercentageOfTotal)
.plus(percentageOfTotal)
.toString()
: percentageOfTotal,
};
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
@@ -34,7 +34,6 @@ import {
formatNumberPercentage,
toBigNum,
} from '@vegaprotocol/utils';
import { VALIDATOR_LOGO_MAP } from './logo-map';
interface CanonisedConsensusNodeProps {
id: string;
@@ -162,11 +161,6 @@ export const ConsensusValidatorsTable = ({
pendingUserStake,
userStakeShare,
}) => {
const logo = VALIDATOR_LOGO_MAP[id]
? VALIDATOR_LOGO_MAP[id]
: avatarUrl
? avatarUrl
: null;
const {
rawValidatorScore: previousEpochValidatorScore,
performanceScore: previousEpochPerformanceScore,
@@ -177,7 +171,7 @@ export const ConsensusValidatorsTable = ({
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
[ValidatorFields.VALIDATOR]: {
avatarUrl: logo,
avatarUrl,
name,
},
[ValidatorFields.STAKE]: stakedTotal,
@@ -216,7 +210,7 @@ export const ConsensusValidatorsTable = ({
: undefined,
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
? formatNumberPercentage(new BigNumber(userStakeShare))
: undefined,
};
}
@@ -1,14 +0,0 @@
export const VALIDATOR_LOGO_MAP: { [key: string]: string } = {
'55504e9bfd914a7bbefa342c82f59a2f4dee344e5b6863a14c02a812f4fbde32':
'https://pbs.twimg.com/profile_images/1586047492629712897/ZVMWBE94_400x400.jpg',
efbdf943443bd7595e83b0d7e88f37b7932d487d1b94aab3d004997273bb43fc:
'https://pbs.twimg.com/profile_images/1026823609979949057/3e-LCHHm_400x400.jpg',
'126751c5830b50d39eb85412fb2964f46338cce6946ff455b73f1b1be3f5e8cc':
'https://pbs.twimg.com/profile_images/1228627868542029824/9aoaLiIx_400x400.jpg',
'43697a3e911d8b70c0ce672adde17a5c38ca8f6a0486bf85ed0546e1b9a82887':
'https://pbs.twimg.com/profile_images/1352167987478843392/XzX82gIb_400x400.jpg',
ac735acc9ab11cf1d8c59c2df2107e00092b4ac96451cb137a1629af5b66242a:
'https://pbs.twimg.com/profile_images/1159254945885016064/vhhp1wL4_400x400.jpg',
'74023df02b8afc9eaf3e3e2e8b07eab1d2122ac3e74b1b0222daf4af565ad3dd':
'https://cdn4.telegram-cdn.org/file/n_OJ6upnglNG95E7u5FyfLmqqBUhf25LqtyZD0UM27gMM_WWi8rywrp9JaAA83quP7vx-WU9Q86quvIRdk9wuUrC2tdD08cYxah4dXHa4OgphFg7Vm5UYFjuGxuHn916OIlzIuVOjJWK_nricGdlPcZQK_T4kBJ_5v4pp94j76_DQ1eSy8DjUxnchZxqpGFv-lGN8U4SjRMK0RC_6DusNaRYEn9Ni2ZQHig7nMiqud-nkK5RgUrClfXFwyhoMFVfhHpGUzwpSv1pSEVLXVh7E1mVmWYrW9hzdQGGrouWCGRqtf_hRa9enS2fv0P9mAB23BcURWuLOVtMdJTF4wfc-w.jpg',
};
@@ -16,7 +16,6 @@ import type { PreviousEpochQuery } from '../../__generated__/PreviousEpoch';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import type { StakingDelegationFieldsFragment } from '../../__generated__/Staking';
import type { ValidatorsView } from './validator-tables';
import { VALIDATOR_LOGO_MAP } from './logo-map';
export enum ValidatorFields {
RANKING_INDEX = 'rankingIndex',
@@ -103,18 +102,13 @@ interface ValidatorRendererProps {
export const ValidatorRenderer = ({ data }: ValidatorRendererProps) => {
const { t } = useTranslation();
const { avatarUrl, name } = data.validator;
const logo = VALIDATOR_LOGO_MAP[data.id]
? VALIDATOR_LOGO_MAP[data.id]
: avatarUrl
? avatarUrl
: null;
return (
<div className="w-[238px] grid grid-cols-[1fr_auto] gap-2 items-center">
<span className="flex overflow-hidden">
{logo && (
{avatarUrl && (
<img
className="h-6 w-6 rounded-full mr-2"
src={logo}
src={avatarUrl}
alt={`Avatar icon for ${name}`}
onError={(e) => (e.currentTarget.style.display = 'none')}
/>
@@ -78,38 +78,37 @@ export const ValidatorTables = ({
const nextDelegation = delegations?.find(
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
);
if (validator.stakedByOperator !== '0') {
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
}
switch (validator.rankingScore?.status) {
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_TENDERMINT:
acc.consensusValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_ERSATZ:
acc.standbyValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
break;
case Schema.ValidatorStatus.VALIDATOR_NODE_STATUS_PENDING:
acc.pendingValidators.push(
addUserDataToValidator(
validator,
currentDelegation,
nextDelegation,
currentUserStakeAvailable
)
);
}
return acc;
},
@@ -28,7 +28,6 @@ export const NodeContainer = ({
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
const { data, loading, error, refetch } = useStakingQuery({
variables: {
partyId: pubKey || '',
@@ -38,9 +37,7 @@ export const NodeContainer = ({
}
: undefined,
},
errorPolicy: 'all',
});
const { data: previousEpochData } = usePreviousEpochQuery({
variables: {
epochId: (Number(data?.epoch.id) - 1).toString(),
@@ -50,7 +47,7 @@ export const NodeContainer = ({
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
if (error && !error.message.includes('failed to get party for ID')) {
if (error) {
return (
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
<pre>
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: 1800,
viewportWidth: 1440,
viewportHeight: 900,
responseTimeout: 50000,
requestTimeout: 20000,
@@ -12,7 +12,6 @@ import {
createMarketsDataFragment,
assetQuery,
networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock';
import {
addDecimalsFormatNumber,
@@ -159,7 +158,6 @@ describe('Closed markets', { tags: '@smoke' }, () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(
req,
@@ -6,10 +6,6 @@ const row = 'key-value-table-row';
const marketTitle = 'accordion-title';
const externalLink = 'external-link';
const accordionContent = 'accordion-content';
const providerName = 'provider-name';
const oracleBannerStatus = 'oracle-banner-status';
const oracleBannerDialogTrigger = 'oracle-banner-dialog-trigger';
const oracleFullProfile = 'oracle-full-profile';
describe('market info is displayed', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -32,11 +28,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
it('show oracle banner', () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
cy.getByTestId(oracleBannerDialogTrigger)
.should('contain.text', 'Show more')
.click();
cy.getByTestId(oracleFullProfile).should('exist');
cy.getByTestId('oracle-status').should('contain.text', 'COMPROMISED');
});
it('current fees displayed', () => {
@@ -189,19 +181,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
cy.getByTestId(marketTitle).contains('Oracle').click();
cy.getByTestId(accordionContent)
.getByTestId(providerName)
.getByTestId('provider-name')
.and('contain', 'Another oracle');
cy.getByTestId(providerName).should('be.visible').click();
cy.getByTestId('dialog-content')
.eq(1)
.within(() => {
cy.getByTestId('block-explorer-link').contains('Block explorer');
cy.getByTestId('github-link').contains('Oracle repository');
});
cy.getByTestId('dialog-close').click();
cy.getByTestId(accordionContent)
.getByTestId('verified-proofs')
.and('contain', '1');
@@ -1,105 +0,0 @@
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('header-title').should('not.be.empty');
cy.getByTestId('popover-trigger').should('not.be.empty');
});
it('must see market expiry', () => {
@@ -4,6 +4,7 @@ import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/utils';
const dialogCloseBtn = 'dialog-close';
const popoverTrigger = 'popover-trigger';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
@@ -30,6 +31,36 @@ 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',
@@ -222,3 +253,15 @@ 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');
});
}
@@ -16,10 +16,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
describe('limit order', () => {
before(() => {
cy.getByTestId(toggleLimit).click();
@@ -102,7 +98,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
'have.text',
'Total margin available'
);
cy.get('.text-neutral-500').should('have.text', '100,000.01 tDAI');
cy.get('.text-neutral-500').should('have.text', '~100,000.01 tDAI');
});
});
});
@@ -1,6 +1,10 @@
import * as Schema from '@vegaprotocol/types';
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
import {
accountsQuery,
amendGeneralAccountBalance,
estimateOrderQuery,
} from '@vegaprotocol/mock';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
@@ -40,10 +44,13 @@ describe(
cy.setVegaWallet();
cy.mockTradingPage();
const accounts = accountsQuery();
amendGeneralAccountBalance(accounts, 'market-0', '1');
amendGeneralAccountBalance(accounts, 'market-0', '100000000');
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Accounts', accounts);
});
cy.mockGQL((req) => {
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
});
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
@@ -59,7 +66,7 @@ describe(
);
cy.getByTestId('dealticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
'You may not have enough margin available to open this position. 2,354.72283 tDAI is currently required. You have only 1,000.01 tDAI available.'
);
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
cy.getByTestId('dialog-content')
@@ -291,58 +291,12 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
.should('have.text', '200.00');
});
it('must see a pegged order - ask', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_BUY,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_ASK,
offset: '250000',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Ask - 2.50 Peg limit');
});
it('must see a pegged order - bid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_BEST_BID,
offset: '100',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Bid + 0.001 Peg limit');
});
it('must see a pegged order - mid', () => {
updateOrder({
id: orderId,
side: Schema.Side.SIDE_SELL,
peggedOrder: {
__typename: 'PeggedOrder',
reference: Schema.PeggedReference.PEGGED_REFERENCE_MID,
offset: '0.5',
},
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="type"]')
.should('have.text', 'Mid + 0.00001 Peg limit');
});
it('for market typy must not see a price for active or parked orders', () => {
// 7003-MORD-005
updateOrder({
id: orderId,
type: Schema.OrderType.TYPE_MARKET,
status: Schema.OrderStatus.STATUS_PARKED,
peggedOrder: null,
});
cy.get(`[row-id=${orderId}]`)
.find('[col-id="price"]')
@@ -383,7 +337,6 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_ACTIVE,
peggedOrder: {},
liquidityProvisionId: '6536',
});
cy.get(`[row-id=${orderId}]`)
@@ -17,6 +17,7 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
it('can connect', () => {
// 0004-EWAL-001
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
@@ -29,6 +30,7 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
it('should see QR code modal for WalletConnect', () => {
// 0004-EWAL-003
cy.wait('@NetworkParams');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
cy.getByTestId('connect-eth-wallet-btn').click();
+3 -15
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/oracles';
import {
accountsQuery,
assetQuery,
@@ -10,7 +10,7 @@ import {
chainIdQuery,
chartQuery,
depositsQuery,
estimateFeesQuery,
estimateOrderQuery,
marginsQuery,
marketCandlesQuery,
marketDataQuery,
@@ -20,16 +20,12 @@ import {
marketsDataQuery,
marketsQuery,
networkParamsQuery,
nodeGuardQuery,
ordersQuery,
estimatePositionQuery,
positionsQuery,
proposalListQuery,
statisticsQuery,
tradesQuery,
withdrawalsQuery,
protocolUpgradeProposalsQuery,
blockStatisticsQuery,
} from '@vegaprotocol/mock';
import type { PartialDeep } from 'type-fest';
import type { MarketDataQuery, MarketsQuery } from '@vegaprotocol/market-list';
@@ -86,7 +82,6 @@ const mockTradingPage = (
) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(
req,
'Markets',
@@ -160,16 +155,9 @@ const mockTradingPage = (
aliasGQLQuery(req, 'Candles', candlesQuery());
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(req, 'EstimateFees', estimateFeesQuery());
aliasGQLQuery(req, 'EstimatePosition', estimatePositionQuery());
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
aliasGQLQuery(req, 'ProposalsList', proposalListQuery());
aliasGQLQuery(req, 'Deposits', depositsQuery());
aliasGQLQuery(
req,
'ProtocolUpgradeProposals',
protocolUpgradeProposalsQuery()
);
aliasGQLQuery(req, 'BlockStatistics', blockStatisticsQuery());
};
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
-6
View File
@@ -1,6 +0,0 @@
function ReactMarkdown({ children }) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{children}</>;
}
export default ReactMarkdown;
@@ -1,83 +0,0 @@
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();
});
});
@@ -1,58 +0,0 @@
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>
);
};
@@ -1,111 +0,0 @@
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/market-list';
import { MarketDataUpdateDocument } from '@vegaprotocol/market-list';
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);
});
});
@@ -1,115 +0,0 @@
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/market-list';
import { useMarketDataUpdateSubscription } from '@vegaprotocol/market-list';
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>
);
};
@@ -1,289 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MarketSelector } from './market-selector';
import { useMarketList } from '@vegaprotocol/market-list';
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';
jest.mock('@vegaprotocol/market-list');
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) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(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]);
});
});
@@ -1,276 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import uniqBy from 'lodash/uniqBy';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
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>
);
};
+1 -3
View File
@@ -9,8 +9,7 @@ import {
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
import { useGlobalStore, usePageTitleStore } from '../../stores';
import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { TradeGrid, TradePanels } from './trade-grid';
import { useNavigate, useParams } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
@@ -101,7 +100,6 @@ export const MarketPage = () => {
/>
);
}, [largeScreen, data, onSelect, navigate]);
if (!data && marketId) {
return (
<Splash>
@@ -1,50 +0,0 @@
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>
);
};
@@ -1,76 +0,0 @@
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>
);
};
+194 -65
View File
@@ -1,37 +1,120 @@
import { memo, useState } from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
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 { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import type { PinnedAsset } from '@vegaprotocol/accounts';
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 '../../components/banner';
import {
Tab,
LocalStoragePersistTabs as Tabs,
Splash,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { OracleBanner } from '@vegaprotocol/market-info';
import { AccountsContainer } from '../../components/accounts-container';
import type { Market } from '@vegaprotocol/market-list';
import { Filter } from '@vegaprotocol/orders';
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 {
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';
import { TradingViews } from './trade-views';
import { MarketSelector } from './market-selector';
import { HeaderStats } from './header-stats';
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;
interface TradeGridProps {
market: Market | null;
@@ -327,57 +410,18 @@ const MainGrid = memo(
);
MainGrid.displayName = 'MainGrid';
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,
});
export const TradeGrid = ({
market,
onSelect,
pinnedAsset,
}: TradeGridProps) => {
return (
<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">
<div className="h-full grid grid-rows-[min-content_1fr]">
<div>
<TradeMarketHeader market={market} onSelect={onSelect} />
<OracleBanner marketId={market?.id || ''} />
</div>
{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>
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
);
};
@@ -395,3 +439,88 @@ 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>
);
};
@@ -5,7 +5,13 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/market-list';
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { HeaderStat } from '../../components/header';
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 { MarketMarkPrice } from '../../components/market-mark-price';
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
import { MarketState } from '../../components/market-state';
@@ -13,90 +19,102 @@ import { HeaderStatMarketTradingMode } from '../../components/market-trading-mod
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
import { MarketState as State } from '@vegaprotocol/types';
interface HeaderStatsProps {
interface TradeMarketHeaderProps {
market: Market | null;
onSelect: (marketId: string, metaKey?: boolean) => void;
}
export const HeaderStats = ({ market }: HeaderStatsProps) => {
export const TradeMarketHeader = ({
market,
onSelect,
}: TradeMarketHeaderProps) => {
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 (
<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"
<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"
>
<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>
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
}}
>
<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>
{asset.symbol}
</ButtonLink>
</div>
</HeaderStat>
) : null}
<MarketLiquiditySupplied
marketId={market?.id}
assetDecimals={asset?.decimals || 0}
/>
<MarketProposalNotification marketId={market?.id} />
</Header>
);
};
@@ -1,146 +0,0 @@
import type { PinnedAsset } from '@vegaprotocol/accounts';
import type { Market } from '@vegaprotocol/market-list';
import { OracleBanner } from '@vegaprotocol/market-info';
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>
);
};
@@ -1,90 +0,0 @@
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 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 },
};
@@ -1,473 +0,0 @@
import merge from 'lodash/merge';
import { renderHook } from '@testing-library/react';
import { 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/market-list';
import type { Filter } from './market-selector';
import { subDays } from 'date-fns';
jest.mock('@vegaprotocol/market-list');
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_PENDING,
}),
];
mockUseMarketList.mockReturnValue({
data: markets,
loading: false,
error: undefined,
});
const { result } = setup();
const expectedFilteredMarkets = markets.filter((m) =>
[MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(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],
]);
});
});
@@ -1,82 +0,0 @@
import { useMemo } from 'react';
import orderBy from 'lodash/orderBy';
import { MarketState } from '@vegaprotocol/types';
import { useMarketList } from '@vegaprotocol/market-list';
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) => {
return [MarketState.STATE_ACTIVE, MarketState.STATE_SUSPENDED].includes(
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 };
};
@@ -6,8 +6,8 @@ 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 } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type {
@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
import type { Property } from '@vegaprotocol/types';
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 } from '@vegaprotocol/oracles';
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
import type { SettlementPriceCellProps } from './settlement-price-cell';
import { SettlementPriceCell } from './settlement-price-cell';
@@ -1,6 +1,6 @@
import { DApp, EXPLORER_ORACLE, useLinks } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useOracleSpecBindingData } from '@vegaprotocol/market-info';
import { useOracleSpecBindingData } from '@vegaprotocol/oracles';
import { Link } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
@@ -1,7 +1,7 @@
import { Icon, NavigationLink } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { Links, Routes } from '../../pages/client-router';
import { IconNames } from '@blueprintjs/icons';
import { COG } from '@blueprintjs/icons/src/generated/iconNames';
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
return (
@@ -9,7 +9,7 @@ export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
{withMobile ? (
t('Settings')
) : (
<Icon name={IconNames.COG} className="!align-middle" />
<Icon name={COG} className="!align-middle" />
)}
</NavigationLink>
);
@@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
import { useGlobalStore } from '../../stores';
export const AppFailure = ({
title,
@@ -9,13 +9,17 @@ export const AppFailure = ({
title: string;
error?: string | null;
}) => {
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const { setNodeSwitcher } = useGlobalStore((store) => ({
nodeSwitcherOpen: store.nodeSwitcherDialog,
setNodeSwitcher: (open: boolean) =>
store.update({ nodeSwitcherDialog: open }),
}));
const nonIdealWrapperClasses =
'h-full min-h-screen flex items-center justify-center';
return (
<div className={nonIdealWrapperClasses}>
<div className="text-center">
<p className="text-xl mb-4">{title}</p>
<h1 className="text-xl mb-4">{title}</h1>
{error && <p className="text-sm mb-8">{error}</p>}
<Button onClick={() => setNodeSwitcher(true)}>
{t('Change node')}
@@ -1,6 +1,5 @@
import type { InMemoryCacheConfig } from '@apollo/client';
import {
AppFailure,
NetworkLoader,
NodeGuard,
useEnvironment,
@@ -10,6 +9,7 @@ import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import dynamic from 'next/dynamic';
import type { ReactNode } from 'react';
import { AppFailure } from './app-failure';
import { Web3Provider } from './web3-provider';
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
@@ -1,2 +1,3 @@
export * from './app-failure';
export * from './app-loader';
export * from './web3-provider';
+1
View File
@@ -1 +1,2 @@
export * from './announcement-banner';
export * from './oracle-banner';
@@ -0,0 +1,45 @@
import { t } from '@vegaprotocol/i18n';
import { useMarketOracle } from '@vegaprotocol/market-info';
import ReactMarkdown from 'react-markdown';
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
const oracleStatuses = {
UNKNOWN: t(
"This public key's proofs have not been verified yet, or no proofs have been provided yet."
),
GOOD: t("This public key's proofs have been verified."),
SUSPICIOUS: t(
'This public key is suspected to be acting in bad faith, pending investigation.'
),
MALICIOUS: t('This public key has been observed acting in bad faith.'),
RETIRED: t('This public key is no longer in use.'),
COMPROMISED: t(
'This public key is no longer in the control of its original owners.'
),
};
export const OracleBanner = ({ marketId }: { marketId: string }) => {
const oracle = useMarketOracle(marketId);
if (!oracle || oracle.status === 'GOOD') {
return null;
}
return (
<NotificationBanner intent={Intent.Danger}>
<div>
Oracle status for this market is{' '}
<span data-testId="oracle-status">{oracle.status}</span>.{' '}
{oracleStatuses[oracle.status]}
</div>
{oracle.status_reason ? (
<ReactMarkdown
className="react-markdown-container"
skipHtml={true}
disallowedElements={['img']}
linkTarget="_blank"
>
{oracle.status_reason}
</ReactMarkdown>
) : null}
</NotificationBanner>
);
};
@@ -4,15 +4,18 @@ import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
import { MockedProvider } from '@apollo/client/testing';
import { Intent } from '@vegaprotocol/ui-toolkit';
const mockSetNodeSwitcher = jest.fn();
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
useEnvironment: jest.fn().mockImplementation(() => ({
VEGA_URL: 'https://vega-url.wtf',
VEGA_INCIDENT_URL: 'https://blog.vega.community',
})),
useNodeSwitcherStore: jest.fn(() => mockSetNodeSwitcher),
}));
const mockSetNodeSwitcher = jest.fn();
jest.mock('../../stores', () => ({
...jest.requireActual('../../stores'),
useGlobalStore: () => mockSetNodeSwitcher,
}));
describe('NodeHealth', () => {
+5 -6
View File
@@ -1,14 +1,11 @@
import { useCallback } from 'react';
import {
useEnvironment,
useNodeHealth,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import type { Intent } from '@vegaprotocol/ui-toolkit';
import { Indicator, ExternalLink } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { ButtonHTMLAttributes, ReactNode } from 'react';
import { useGlobalStore } from '../../stores';
export const Footer = () => {
return (
@@ -23,7 +20,9 @@ export const Footer = () => {
export const NodeHealth = () => {
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
const setNodeSwitcher = useGlobalStore(
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
);
const { datanodeBlockHeight, text, intent } = useNodeHealth();
const onClick = useCallback(() => {
setNodeSwitcher(true);
+2 -2
View File
@@ -43,7 +43,7 @@ export const HeaderStat = ({
testId?: string;
}) => {
const itemClass =
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default first:border-none text-neutral-500 dark:text-neutral-400';
'min-w-min w-[120px] whitespace-nowrap pb-3 px-4 border-l border-default text-neutral-500 dark:text-neutral-400';
const itemHeading = 'text-black dark:text-white';
return (
@@ -72,7 +72,7 @@ export const HeaderTitle = ({
secondaryContent: ReactNode;
}) => {
return (
<div className="text-left" data-testid="header-title">
<div className="text-left">
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
{primaryContent}
</div>
@@ -1,42 +0,0 @@
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { Navbar } from './navbar';
describe('Navbar', () => {
const pubKey = 'pubKey';
it('should be properly rendered', () => {
render(
<MockedProvider>
<MemoryRouter>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toBeInTheDocument();
expect(screen.getByTestId('Trading')).toBeInTheDocument();
expect(screen.getByTestId('Portfolio')).toBeInTheDocument();
});
it('Markets page route should not match empty market page', () => {
render(
<MockedProvider>
<MemoryRouter initialEntries={['/markets/all']}>
<VegaWalletContext.Provider
value={{ pubKey } as VegaWalletContextShape}
>
<Navbar theme="dark" />
</VegaWalletContext.Provider>
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByTestId('Markets')).toHaveClass('active');
expect(screen.getByTestId('Trading')).not.toHaveClass('active');
});
});
+1 -1
View File
@@ -75,7 +75,7 @@ export const Navbar = ({
</NavigationLink>
</NavigationItem>
<NavigationItem>
<NavigationLink data-testid="Trading" to={tradingPath} end>
<NavigationLink data-testid="Trading" to={tradingPath}>
{t('Trading')}
</NavigationLink>
</NavigationItem>
@@ -1,2 +1,3 @@
export * from './select-market-columns';
export * from './select-market-table';
export * from './select-market';
@@ -7,7 +7,7 @@ import {
} from '@vegaprotocol/market-list';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { PriceCell } from '@vegaprotocol/datagrid';
import { PriceCell, signedNumberCssClass } from '@vegaprotocol/datagrid';
import { Link as UILink, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
import isNil from 'lodash/isNil';
import type { CandleClose } from '@vegaprotocol/types';
@@ -341,3 +341,187 @@ export const columns = (
];
return selectMarketColumns;
};
export const columnsPositionMarkets = (
market: MarketMaybeWithDataAndCandles,
onSelect: (id: string, metaKey?: boolean) => void,
inViewRoot?: RefObject<HTMLElement>,
openVolume?: string,
onCellClick?: OnCellClickHandler
) => {
const candlesClose = market.candles
?.map((candle) => candle?.close)
.filter((c: string | undefined): c is CandleClose => !isNil(c));
const candleLow = market.candles && calcCandleLow(market.candles);
const candleHigh = market.candles && calcCandleHigh(market.candles);
const candleVolume = market.candles && calcCandleVolume(market.candles);
const selectMarketColumns: Column[] = [
{
kind: ColumnKind.Market,
value: (
<Link
to={Links[Routes.MARKET](market.id)}
data-testid={`market-link-${market.id}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onSelect(market.id, e.metaKey || e.ctrlKey);
}}
>
<UILink>{market.tradableInstrument.instrument.code}</UILink>
</Link>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.ProductType,
value: market.tradableInstrument.instrument.product.__typename,
className: `py-2 first:text-left hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.LastPrice,
value: (
<MarketMarkPrice
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={market.data?.markPrice}
asPriceCell
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Change24,
value: (
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market?.decimalPlaces}
inViewRoot={inViewRoot}
initialValue={candlesClose}
/>
),
className: cellClassNames,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Sparkline,
value: candlesClose && (
<Sparkline
width={100}
height={20}
muted={false}
data={candlesClose.map((c: string) => Number(c))}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.Asset,
value: (
<button
data-dialog-trigger
className="inline underline"
onClick={(e) => {
e.stopPropagation();
if (!onCellClick) return;
onCellClick(
e,
ColumnKind.Asset,
market.tradableInstrument.instrument.product.settlementAsset.id
);
}}
>
{market.tradableInstrument.instrument.product.settlementAsset.symbol}
</button>
),
className: `${cellClassNames} hidden sm:table-cell`,
onlyOnDetailed: false,
},
{
kind: ColumnKind.High24,
value: candleHigh ? (
<PriceCell
value={Number(candleHigh)}
valueFormatted={addDecimalsFormatNumber(
candleHigh.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Low24,
value: candleLow ? (
<PriceCell
value={Number(candleLow)}
valueFormatted={addDecimalsFormatNumber(
candleLow.toString(),
market.decimalPlaces,
2
)}
/>
) : (
'-'
),
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Volume,
value: (
<Last24hVolume
marketId={market.id}
inViewRoot={inViewRoot}
positionDecimalPlaces={market.positionDecimalPlaces}
initialValue={candleVolume}
formatDecimals={2}
/>
),
className: `${cellClassNames} hidden lg:table-cell font-mono`,
onlyOnDetailed: true,
dataTestId: 'market-volume',
},
{
kind: ColumnKind.TradingMode,
value: (
<MarketTradingMode
marketId={market?.id}
inViewRoot={inViewRoot}
initialTradingMode={market.tradingMode}
initialTrigger={market.data?.trigger}
/>
),
className: `${cellClassNames} hidden lg:table-cell`,
onlyOnDetailed: true,
dataTestId: 'trading-mode-col',
},
{
kind: ColumnKind.Fee,
value: <FeesCell feeFactors={market.fees.factors} />,
className: `${cellClassNames} hidden xl:table-cell font-mono`,
onlyOnDetailed: true,
},
{
kind: ColumnKind.Position,
value: (
<p className={signedNumberCssClass(openVolume || '')}>
{openVolume &&
addDecimalsFormatNumber(openVolume, market.positionDecimalPlaces)}
</p>
),
className: `${cellClassNames} hidden xxl:table-cell font-mono`,
onlyOnDetailed: true,
},
];
return selectMarketColumns;
};
@@ -1,6 +1,7 @@
import { columnHeaders } from './select-market-columns';
import classNames from 'classnames';
import type { Column } from './select-market-columns';
import type { ReactNode } from 'react';
export const SelectMarketTableHeader = ({
detailed = false,
@@ -61,3 +62,23 @@ export const SelectMarketTableRow = ({
</tr>
);
};
export const SelectMarketTableRowSplash = ({
children,
colSpan,
}: {
children: ReactNode;
colSpan: number;
}) => {
return (
<tr className={`relative`}>
<td
className="text-center p-10 pt-14 text-xs"
key="splash"
colSpan={colSpan}
>
{children}
</td>
</tr>
);
};
@@ -0,0 +1,203 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as Schema from '@vegaprotocol/types';
import { SelectAllMarketsTableBody } from './select-market';
import type {
MarketMaybeWithCandles,
MarketMaybeWithData,
MarketData,
} from '@vegaprotocol/market-list';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
type Market = MarketMaybeWithCandles & MarketMaybeWithData;
type PartialMarket = Partial<
Omit<Market, 'data'> & { data: Partial<MarketData> }
>;
const MARKET_A: PartialMarket = {
__typename: 'Market',
id: '1',
decimalPlaces: 2,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '1',
code: 'ABCDEF',
name: 'ABCDEF 1-Day',
product: {
__typename: 'Future',
quoteName: 'ABCDEF',
settlementAsset: {
__typename: 'Asset',
id: 'asset-ABC',
name: '',
decimals: 2,
symbol: 'ABC',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['ABC', 'DEF'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '1',
},
markPrice: '90',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '1000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
{
__typename: 'Candle',
high: '10',
low: '1',
open: '1',
close: '100',
volume: '1000',
periodStart: '2022-11-01T15:50:00Z',
},
],
};
const MARKET_B: PartialMarket = {
__typename: 'Market',
id: '2',
decimalPlaces: 2,
positionDecimalPlaces: 0,
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
id: '2',
code: 'XYZ',
name: 'XYZ 1-Day',
product: {
__typename: 'Future',
quoteName: 'XYZ',
settlementAsset: {
__typename: 'Asset',
id: 'asset-XYZ',
name: 'asset-XYZ',
decimals: 2,
symbol: 'XYZ',
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceSpec',
id: 'oracleId',
},
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
tradingTerminationProperty: 'trading-termination-property',
settlementDataProperty: 'settlement-data-property',
},
},
metadata: {
__typename: 'InstrumentMetadata',
tags: ['XYZ'],
},
},
},
fees: {
__typename: 'Fees',
factors: {
__typename: 'FeeFactors',
infrastructureFee: '0.01',
liquidityFee: '0.01',
makerFee: '0.01',
},
},
data: {
__typename: 'MarketData',
market: {
__typename: 'Market',
id: '2',
},
markPrice: '123.123',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_OPENING,
marketState: Schema.MarketState.STATE_PENDING,
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
indicativeVolume: '2000',
},
candles: [
{
__typename: 'Candle',
high: '100',
low: '10',
open: '10',
close: '80',
volume: '1000',
periodStart: '2022-11-01T15:49:00Z',
},
],
};
describe('SelectMarket', () => {
const table = document.createElement('table');
it('should render the SelectAllMarketsTableBody', () => {
const onSelect = jest.fn();
const onCellClick = jest.fn();
const { container } = render(
<MemoryRouter>
<SelectAllMarketsTableBody
markets={[MARKET_A as Market, MARKET_B as Market]}
onCellClick={onCellClick}
onSelect={onSelect}
/>
</MemoryRouter>,
{ wrapper: MockedProvider, container: document.body.appendChild(table) }
);
expect(screen.getByText('ABCDEF')).toBeTruthy(); // name
expect(screen.getByText('25.00%')).toBeTruthy(); // price change
expect(container).toHaveTextContent(/1,000/); // volume
fireEvent.click(screen.getAllByTestId(`market-link-1`)[0]);
expect(onSelect).toHaveBeenCalledWith('1', false);
});
});
@@ -0,0 +1,214 @@
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import type { RefObject } from 'react';
import { useMarketList } from '@vegaprotocol/market-list';
import { positionsDataProvider } from '@vegaprotocol/positions';
import { t } from '@vegaprotocol/i18n';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { ExternalLink, Icon, Loader, Popover } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
columnHeaders,
columnHeadersPositionMarkets,
columns,
columnsPositionMarkets,
} from './select-market-columns';
import {
SelectMarketTableHeader,
SelectMarketTableRow,
SelectMarketTableRowSplash,
} from './select-market-table';
import type { ReactNode } from 'react';
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/market-list';
import type { PositionFieldsFragment } from '@vegaprotocol/positions';
import type { Column, OnCellClickHandler } from './select-market-columns';
import {
DApp,
TOKEN_NEW_MARKET_PROPOSAL,
useLinks,
} from '@vegaprotocol/environment';
import { HeaderTitle } from '../header';
export const SelectAllMarketsTableBody = ({
markets,
positions,
onSelect,
onCellClick,
inViewRoot,
headers = columnHeaders,
tableColumns = (market) => columns(market, onSelect, onCellClick, inViewRoot),
}: {
markets?: MarketMaybeWithDataAndCandles[] | null;
positions?: PositionFieldsFragment[];
title?: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
headers?: Column[];
tableColumns?: (
market: MarketMaybeWithDataAndCandles,
inViewRoot?: RefObject<HTMLDivElement>,
openVolume?: string
) => Column[];
inViewRoot?: RefObject<HTMLDivElement>;
}) => {
const tokenLink = useLinks(DApp.Token);
if (!markets) return null;
return (
<>
<thead className="bg-neutral-100 dark:bg-neutral-800">
<SelectMarketTableHeader detailed={true} headers={headers} />
</thead>
{/* Border styles required to create space between tbody elements margin/padding don't work */}
<tbody className="border-b-[10px] border-transparent">
{markets.length > 0 ? (
markets?.map((market, i) => (
<SelectMarketTableRow
marketId={market.id}
key={i}
detailed
onSelect={onSelect}
columns={tableColumns(
market,
inViewRoot,
positions &&
positions.find((p) => p.market.id === market.id)?.openVolume
)}
/>
))
) : (
<SelectMarketTableRowSplash colSpan={12}>
{t('No markets ')}
<ExternalLink href={tokenLink(TOKEN_NEW_MARKET_PROPOSAL)}>
{t('Propose a new market')}
</ExternalLink>
</SelectMarketTableRowSplash>
)}
</tbody>
</>
);
};
export const SelectMarketPopover = ({
marketCode,
marketName,
onSelect,
onCellClick,
}: {
marketCode: string;
marketName: string;
onSelect: (id: string, metaKey?: boolean) => void;
onCellClick: OnCellClickHandler;
}) => {
const { pubKey } = useVegaWallet();
const [open, setOpen] = useState(false);
const inViewRoot = useRef<HTMLDivElement>(null);
const {
data,
loading: marketsLoading,
reload: marketListReload,
} = useMarketList();
const {
data: positions,
loading: positionsLoading,
reload,
} = useDataProvider({
dataProvider: positionsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const onSelectMarket = useCallback(
(marketId: string, metaKey?: boolean) => {
onSelect(marketId, metaKey);
setOpen(false);
},
[onSelect]
);
const iconClass = open ? 'rotate-180' : '';
const markets = useMemo(
() =>
data?.filter((market) =>
positions?.find((node) => node.market.id === market.id)
),
[data, positions]
);
useEffect(() => {
if (open) {
reload();
marketListReload();
}
}, [open, marketListReload, reload]);
return (
<Popover
open={open}
onChange={setOpen}
trigger={
<div className="flex items-center gap-2">
<HeaderTitle
primaryContent={marketCode}
secondaryContent={marketName}
/>
<Icon name="chevron-down" className={iconClass} size={6} />
</div>
}
>
<div
className="w-[90vw] max-h-[80vh] overflow-y-auto"
data-testid="select-market-list"
ref={inViewRoot}
>
{marketsLoading || (pubKey && positionsLoading) ? (
<div className="flex items-center gap-4">
<Loader size="small" />
{t('Loading market data')}
</div>
) : (
<table className="relative text-sm w-full whitespace-nowrap">
{pubKey && (positions?.length ?? 0) && (markets?.length ?? 0) ? (
<>
<TableTitle>{t('My markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={markets}
positions={positions || undefined}
onSelect={onSelectMarket}
onCellClick={onCellClick}
headers={columnHeadersPositionMarkets}
tableColumns={(market, inViewRoot, openVolume) =>
columnsPositionMarkets(
market,
onSelectMarket,
inViewRoot,
openVolume,
onCellClick
)
}
/>
</>
) : null}
<TableTitle>{t('All markets')}</TableTitle>
<SelectAllMarketsTableBody
inViewRoot={inViewRoot}
markets={data}
onSelect={onSelectMarket}
onCellClick={onCellClick}
/>
</table>
)}
</div>
</Popover>
);
};
const TableTitle = ({ children }: { children: ReactNode }) => {
return (
<thead>
<tr>
<th className="font-normal text-left">
<h3 className="text-lg">{children}</h3>
</th>
</tr>
</thead>
);
};
@@ -7,13 +7,7 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { RISK_ACCEPTED_KEY } from '../constants';
import { TelemetryApproval } from './telemetry-approval';
import {
DOCS_VEGA_WALLET,
GET_VEGA_WALLET_URL,
Networks,
useDocsLink,
useEnvironment,
} from '@vegaprotocol/environment';
import { Networks, useEnvironment } from '@vegaprotocol/environment';
import { useLocalStorage } from '@vegaprotocol/react-helpers';
interface Props {
@@ -52,9 +46,8 @@ export const RiskNoticeDialog = ({ onClose, network }: Props) => {
};
const TestnetContent = ({ network }: { network: Networks }) => {
const { GITHUB_FEEDBACK_URL } = useEnvironment();
const docsLink = useDocsLink();
const { VEGA_DOCS_URL, GITHUB_FEEDBACK_URL, VEGA_WALLET_URL } =
useEnvironment();
return (
<>
<p className="mb-4">
@@ -69,17 +62,17 @@ const TestnetContent = ({ network }: { network: Networks }) => {
[network]
)}
</p>
{GITHUB_FEEDBACK_URL && GET_VEGA_WALLET_URL && docsLink && (
{VEGA_DOCS_URL && GITHUB_FEEDBACK_URL && VEGA_WALLET_URL && (
<ul className="list-disc pl-4">
<li className="mb-1">
<Link href={GET_VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
<Link href={VEGA_WALLET_URL} target="_blank">
<span className="underline">{t('Learn about Vega wallet')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
<li className="mb-1">
<Link href={docsLink(DOCS_VEGA_WALLET)} target="_blank">
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
<Link href={VEGA_DOCS_URL} target="_blank">
<span className="underline">{t('View documentation')}</span>{' '}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</li>
+8 -8
View File
@@ -21,10 +21,9 @@ import {
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import './styles.css';
import { usePageTitleStore } from '../stores';
import { useGlobalStore, usePageTitleStore } from '../stores';
import { Footer } from '../components/footer';
import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager';
@@ -81,7 +80,7 @@ function AppBody({ Component }: AppProps) {
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
'grid-rows-[repeat(3,min-content),1fr]'
);
return (
@@ -93,7 +92,7 @@ function AppBody({ Component }: AppProps) {
<Title />
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
@@ -116,10 +115,11 @@ function AppBody({ Component }: AppProps) {
function VegaTradingApp(props: AppProps) {
const status = useEnvironment((store) => store.status);
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
store.dialogOpen,
store.setDialogOpen,
]);
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({
nodeSwitcherOpen: store.nodeSwitcherDialog,
setNodeSwitcher: (open: boolean) =>
store.update({ nodeSwitcherDialog: open }),
}));
useInitializeEnv();
-21
View File
@@ -127,24 +127,3 @@ html [data-theme='light'] {
.ag-theme-balham .ag-row.no-hover:hover {
background: var(--ag-background-color);
}
.virtualized-list {
/* Works on Firefox */
scrollbar-width: thin;
scrollbar-color: #999 #333;
}
/* Works on Chrome, Edge, and Safari */
.virtualized-list::-webkit-scrollbar {
width: 6px;
background-color: #999;
}
.virtualized-list::-webkit-scrollbar-thumb {
width: 6px;
background-color: #333;
}
.virtualized-list::-webkit-scrollbar-track {
box-shadow: inset 0 0 6px rgb(0 0 0 / 30%);
background-color: #999;
}

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