Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9691e0950 | ||
|
|
688eb6f5cb | ||
|
|
09778a5d3f | ||
|
|
59993e3ea6 | ||
|
|
54161a6dba | ||
|
|
26f524ea07 | ||
|
|
db13e6f21d | ||
|
|
4a679813ba | ||
|
|
a4279d5b5e | ||
|
|
1a274a67c3 | ||
|
|
1e9ff31fd7 | ||
|
|
aa2b755b93 | ||
|
|
4a7296312a | ||
|
|
1418a2259f | ||
|
|
7e5ef0157a | ||
|
|
0cf764f11e | ||
|
|
1c626aeeb2 | ||
|
|
c4cec47b0b | ||
|
|
fbe1233178 | ||
|
|
d4abb4db39 | ||
|
|
fc994ba52b | ||
|
|
1a67c41bb4 | ||
|
|
fc66240390 | ||
|
|
fc0da98683 | ||
|
|
4d0dafb113 | ||
|
|
eda431be9e | ||
|
|
4ff1749e54 | ||
|
|
e7a108fd7a | ||
|
|
5d3f2ea7d8 | ||
|
|
1ab27574ca | ||
|
|
0ea3c43e78 | ||
|
|
9e183568b0 | ||
|
|
69fd5b26b6 | ||
|
|
2d5b926609 | ||
|
|
98a2ba79a8 | ||
|
|
0e6e986937 | ||
|
|
b3b86f95c2 | ||
|
|
15d6ea0c05 | ||
|
|
0358ce6293 | ||
|
|
0791be4636 | ||
|
|
07442848ee | ||
|
|
d5f0bd419e | ||
|
|
2c7934a8c7 | ||
|
|
fae61c28ca | ||
|
|
fcd4541625 | ||
|
|
8e0a92fdec | ||
|
|
6e7109ed87 | ||
|
|
f2c698ce06 | ||
|
|
ad9a3a3400 | ||
|
|
efeac49d58 | ||
|
|
2d430710ab | ||
|
|
bca1a98985 | ||
|
|
8a080d3279 | ||
|
|
f121836b4e | ||
|
|
b641c82ad8 | ||
|
|
bf73559c30 | ||
|
|
653cec2592 | ||
|
|
579c884a5a | ||
|
|
b2279c7e47 | ||
|
|
a8c17b6807 | ||
|
|
b4c7dc6f59 | ||
|
|
d44392bebf | ||
|
|
bd679957e2 | ||
|
|
e30d48555e | ||
|
|
b4b2416780 | ||
|
|
dc7832ac81 | ||
|
|
91207d31ee | ||
|
|
58efb460a6 | ||
|
|
044f98777b | ||
|
|
00fdc5e81a | ||
|
|
adfad3bafb | ||
|
|
e6ae88905c | ||
|
|
3953ed9953 | ||
|
|
b48ab58e6a | ||
|
|
e44cf1ff53 | ||
|
|
bded1d32ba | ||
|
|
76ddf45f4c | ||
|
|
6a2c9004d8 | ||
|
|
77ee88b43e | ||
|
|
e4958ce3c0 | ||
|
|
ecedb879f8 | ||
|
|
89700803d6 | ||
|
|
65c9fa5df2 | ||
|
|
02399c40fc |
@@ -5,6 +5,8 @@ on:
|
||||
branches:
|
||||
- release/*
|
||||
- develop
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
@@ -97,15 +99,13 @@ 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"
|
||||
|
||||
@@ -33,5 +33,4 @@ jobs:
|
||||
config: baseUrl=${{ github.event.inputs.url }}
|
||||
env: grepTags=@live
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
######
|
||||
|
||||
- name: Run Cypress tests
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
|
||||
working-directory: frontend-monorepo
|
||||
env:
|
||||
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
|
||||
|
||||
@@ -30,13 +30,21 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to the Container registry
|
||||
- name: Log in to the Container registry (ghcr)
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry (docker hub)
|
||||
uses: docker/login-action@v2
|
||||
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:
|
||||
@@ -54,30 +62,24 @@ 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
|
||||
@@ -98,62 +100,60 @@ 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: ${{ env.DOCKERFILE }}
|
||||
file: docker/node-outside-docker.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' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
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"
|
||||
if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
|
||||
fi
|
||||
|
||||
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 .'
|
||||
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
|
||||
|
||||
- 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
|
||||
- name: Publish dist as docker image (ghcr)
|
||||
uses: docker/build-push-action@v3
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ github.event_name == 'pull_request' || (matrix.app == 'trading' && github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') ) }}
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKERFILE }}
|
||||
file: docker/node-outside-docker.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' }}
|
||||
if: ${{ github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
args: --acl private --follow-symlinks --delete
|
||||
env:
|
||||
@@ -169,3 +169,59 @@ 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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/dist-result
|
||||
/tmp
|
||||
/out-tsc
|
||||
/tools/executors/**/*.js
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.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
|
||||
@@ -103,25 +103,68 @@ 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.
|
||||
|
||||
# Docker & Vegacapsule
|
||||
# 🐋 Hosting a console
|
||||
|
||||
## Docker
|
||||
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).
|
||||
|
||||
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:
|
||||
In order to run a container on port 3000:
|
||||
|
||||
```bash
|
||||
docker run -p 3000:80 [TAG]
|
||||
```
|
||||
|
||||
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
|
||||
## 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**
|
||||
|
||||
## Config
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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 +1,2 @@
|
||||
# .env is stagnet1, so there are no overrides required
|
||||
# .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"}'
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import {
|
||||
AppFailure,
|
||||
NetworkLoader,
|
||||
NodeGuard,
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
useInitializeEnv,
|
||||
useNodeSwitcherStore,
|
||||
} 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>
|
||||
@@ -12,10 +21,23 @@ const splashLoading = (
|
||||
);
|
||||
|
||||
function App() {
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
|
||||
(store) => [store.dialogOpen, store.setDialogOpen]
|
||||
);
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||
<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}
|
||||
/>
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
useEnvironment,
|
||||
useNodeSwitcherStore,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { ENV } from '../../config/env';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const setNodeSwitcherOpen = useNodeSwitcherStore(
|
||||
(store) => store.setDialogOpen
|
||||
);
|
||||
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
|
||||
@@ -15,46 +21,40 @@ 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>
|
||||
</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>
|
||||
<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="flex pl-2 content-center">
|
||||
<ExternalLink href={ENV.addresses.feedback}>
|
||||
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
|
||||
</ExternalLink>
|
||||
</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>
|
||||
</footer>
|
||||
<NodeSwitcherDialog
|
||||
open={nodeSwitcherOpen}
|
||||
setOpen={setNodeSwitcherOpen}
|
||||
/>
|
||||
</>
|
||||
|
||||
<div className="flex pl-2 content-center">
|
||||
<ExternalLink href={ENV.addresses.feedback}>
|
||||
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+8
-2
@@ -1,3 +1,5 @@
|
||||
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'],
|
||||
@@ -28,7 +30,11 @@ export const ChainResponseCode = ({
|
||||
}: ChainResponseCodeProps) => {
|
||||
const isSuccess = successCodes.has(code);
|
||||
|
||||
const icon = isSuccess ? '✅' : '❌';
|
||||
const icon = isSuccess ? (
|
||||
<Icon name="tick-circle" className="fill-vega-green-550" />
|
||||
) : (
|
||||
<Icon name="cross" className="fill-vega-pink-550" />
|
||||
);
|
||||
const label = ErrorCodes.get(code) || 'Unknown response code';
|
||||
|
||||
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
|
||||
@@ -36,7 +42,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,6 +4,7 @@ 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;
|
||||
@@ -30,27 +31,22 @@ export const TxProposalVote = ({
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const vote = txData.command.voteSubmission.value ? '👍' : '👎';
|
||||
const vote = txData.command.voteSubmission.value === 'VALUE_YES';
|
||||
|
||||
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>{vote}</TableCell>
|
||||
<TableCell>
|
||||
<VoteIcon vote={vote} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { components } from '../../../types/explorer';
|
||||
import { VoteIcon } from '../vote-icon/vote-icon';
|
||||
|
||||
interface TxOrderTypeProps {
|
||||
orderType: string;
|
||||
@@ -137,12 +138,15 @@ 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-150';
|
||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250';
|
||||
|
||||
// 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);
|
||||
@@ -150,6 +154,16 @@ 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 py-2"
|
||||
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10"
|
||||
>
|
||||
<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:
|
||||
Success
|
||||
</span>
|
||||
{isNumber(code) ? (
|
||||
<ChainResponseCode code={code} hideLabel={true} />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -60,3 +60,7 @@
|
||||
--ag-row-hover-color: theme(colors.neutral[800]);
|
||||
--ag-font-size: 12px;
|
||||
}
|
||||
|
||||
.voteicon svg {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
waitForSpinner,
|
||||
navigateTo,
|
||||
navigation,
|
||||
turnTelemetryOff,
|
||||
} from '../../support/common.functions';
|
||||
import {
|
||||
clickOnValidatorFromList,
|
||||
@@ -67,6 +68,7 @@ context(
|
||||
'teardown wallet & drill into a specific validator',
|
||||
function () {
|
||||
cy.clearLocalStorage();
|
||||
turnTelemetryOff();
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
cy.connectVegaWallet();
|
||||
@@ -252,10 +254,10 @@ context(
|
||||
waitForBeginningOfEpoch();
|
||||
cy.getByTestId(stakeValidatorListStakePercentage).should(
|
||||
'have.text',
|
||||
'100%'
|
||||
'50.02%'
|
||||
);
|
||||
navigateTo(navigation.validators);
|
||||
validateValidatorListTotalStakeAndShare('0', '2.00', '100.00%');
|
||||
validateValidatorListTotalStakeAndShare('0', '3,002.00', '50.02%');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-status').should(
|
||||
'have.text',
|
||||
'Approved '
|
||||
'Approved by validators '
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -136,7 +136,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-state').should(
|
||||
'have.text',
|
||||
'Approved'
|
||||
'Approved by validators'
|
||||
);
|
||||
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*\.?\d*$/;
|
||||
const stakeNumberRegex = /^\d{1,3}(,\d{3})*(\.\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: 0.00');
|
||||
.should('contain', 'Staked by operator: 3,000.00');
|
||||
cy.get(stakedByDelegatesToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Staked by delegates: 0.00');
|
||||
cy.get(totalStakedToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total stake: 0.00');
|
||||
.should('contain', 'Total stake: 3,000.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: 0.00%');
|
||||
.should('contain', 'Unnormalised voting power: 20.00%');
|
||||
cy.get(normalisedVotingPowerToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Normalised voting power: 0.10%');
|
||||
.should('contain', 'Normalised voting power: 50.00%');
|
||||
});
|
||||
|
||||
// 2002-SINC-018
|
||||
@@ -126,13 +126,13 @@ context('Validators Page - verify elements on page', function () {
|
||||
|
||||
cy.get(performancePenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Performance penalty: 100.00%');
|
||||
.should('contain', 'Performance penalty: 0.00%');
|
||||
cy.get(overstakedPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Overstaked penalty:'); // value not asserted due to #2886
|
||||
.should('contain', 'Overstaked penalty: 60.00%'); // value not asserted due to #2886
|
||||
cy.get(totalPenaltyToolTip)
|
||||
.invoke('text')
|
||||
.should('contain', 'Total penalties: 0.00%');
|
||||
.should('contain', 'Total penalties: 60.00%');
|
||||
});
|
||||
|
||||
it('Should be able to see validator pending stake', function () {
|
||||
|
||||
@@ -33,4 +33,6 @@ before(() => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
});
|
||||
// Self stake validators so they are displayed
|
||||
cy.validatorsSelfDelegate();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
StakingBridge,
|
||||
Token,
|
||||
TokenVesting,
|
||||
TokenFaucetable,
|
||||
CollateralBridge,
|
||||
} from '@vegaprotocol/smart-contracts';
|
||||
import { ethers, Wallet } from 'ethers';
|
||||
@@ -41,7 +40,7 @@ export async function depositAsset(
|
||||
decimalPlaces: number
|
||||
) {
|
||||
// Approve asset
|
||||
const faucet = new TokenFaucetable(assetEthAddress, signer);
|
||||
const faucet = new Token(assetEthAddress, signer);
|
||||
cy.wrap(
|
||||
faucet.approve(Erc20BridgeAddress, amount + '0'.repeat(decimalPlaces + 1)),
|
||||
{
|
||||
@@ -62,7 +61,7 @@ export async function depositAsset(
|
||||
}
|
||||
|
||||
export async function faucetAsset(assetEthAddress: string) {
|
||||
const faucet = new TokenFaucetable(assetEthAddress, signer);
|
||||
const faucet = new Token(assetEthAddress, signer);
|
||||
await promiseWithTimeout(faucet.faucet(), 10 * 60 * 1000, 'faucet asset');
|
||||
}
|
||||
|
||||
|
||||
+90
-27
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -37,6 +36,10 @@ import {
|
||||
useEnvironment,
|
||||
NetworkLoader,
|
||||
useInitializeEnv,
|
||||
NodeGuard,
|
||||
AppFailure,
|
||||
NodeSwitcherDialog,
|
||||
useNodeSwitcherStore,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { ENV } from './config';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
@@ -48,6 +51,7 @@ import {
|
||||
TELEMETRY_ON,
|
||||
} from './components/telemetry-dialog/telemetry-dialog';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const cache: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
@@ -179,32 +183,83 @@ const ScrollToTop = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const removeQueryParams = (url: string) => {
|
||||
return url.split('?')[0];
|
||||
};
|
||||
|
||||
const AppContainer = () => {
|
||||
const { config, loading, error } = useEthereumConfig();
|
||||
const { VEGA_ENV, GIT_COMMIT_HASH, GIT_BRANCH, ETHEREUM_PROVIDER_URL } =
|
||||
useEnvironment();
|
||||
const {
|
||||
VEGA_ENV,
|
||||
VEGA_URL,
|
||||
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) {
|
||||
if (ENV.dsn && telemetryOn === 'true') {
|
||||
Sentry.init({
|
||||
dsn: ENV.dsn,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0.1,
|
||||
enabled: true,
|
||||
environment: VEGA_ENV,
|
||||
release: GIT_COMMIT_HASH,
|
||||
beforeSend(event) {
|
||||
if (event.request?.url?.includes('/claim?')) {
|
||||
return {
|
||||
...event,
|
||||
request: {
|
||||
...event.request,
|
||||
url: event.request?.url.split('?')[0],
|
||||
},
|
||||
};
|
||||
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;
|
||||
}
|
||||
return event;
|
||||
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
Sentry.setTag('branch', GIT_BRANCH);
|
||||
@@ -219,21 +274,29 @@ const AppContainer = () => {
|
||||
<ScrollToTop />
|
||||
<AppStateProvider>
|
||||
<div className="grid min-h-full text-white">
|
||||
<AsyncRenderer<EthereumConfig | null>
|
||||
loading={loading}
|
||||
data={config}
|
||||
error={error}
|
||||
render={(cnf) =>
|
||||
cnf && (
|
||||
<Web3Container
|
||||
chainId={Number(cnf.chain_id)}
|
||||
providerUrl={ETHEREUM_PROVIDER_URL}
|
||||
/>
|
||||
)
|
||||
<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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</NodeGuard>
|
||||
</div>
|
||||
</AppStateProvider>
|
||||
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
|
||||
export const EthConnectPrompt = () => {
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
return (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
onClick={open}
|
||||
fill={true}
|
||||
data-testid="connect-to-eth-btn"
|
||||
>
|
||||
|
||||
@@ -2,10 +2,7 @@ import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { useWeb3ConnectStore } from '@vegaprotocol/web3';
|
||||
|
||||
export const EthWalletContainer = ({
|
||||
children,
|
||||
@@ -14,21 +11,12 @@ export const EthWalletContainer = ({
|
||||
}) => {
|
||||
const { account } = useWeb3React();
|
||||
const { t } = useTranslation();
|
||||
const { appDispatch } = useAppState();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<div className="w-full text-center">
|
||||
<Button
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('connectEthWallet')}
|
||||
</Button>
|
||||
<Button onClick={open}>{t('connectEthWallet')}</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,11 @@ import {
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import colors from 'tailwindcss/colors';
|
||||
import { useBalances } from '../../lib/balances/balances-store';
|
||||
import { useEthereumConfig, useWeb3Disconnect } from '@vegaprotocol/web3';
|
||||
import {
|
||||
useEthereumConfig,
|
||||
useWeb3ConnectStore,
|
||||
useWeb3Disconnect,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { getChainName } from '@vegaprotocol/web3';
|
||||
|
||||
const removeLeadingAddressSymbol = (key: string) => {
|
||||
@@ -189,6 +193,7 @@ export const EthWallet = () => {
|
||||
const pendingTxs = usePendingTransactions();
|
||||
const disconnect = useWeb3Disconnect(connector);
|
||||
const { config } = useEthereumConfig();
|
||||
const { open } = useWeb3ConnectStore();
|
||||
|
||||
return (
|
||||
<WalletCard>
|
||||
@@ -233,12 +238,7 @@ export const EthWallet = () => {
|
||||
) : (
|
||||
<Button
|
||||
fill={true}
|
||||
onClick={() =>
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
})
|
||||
}
|
||||
onClick={open}
|
||||
data-testid="connect-to-eth-wallet-button"
|
||||
>
|
||||
{t('connectEthWalletToAssociate')}
|
||||
|
||||
@@ -25,20 +25,26 @@ export function Web3Connector({
|
||||
connectors,
|
||||
chainId,
|
||||
}: Web3ConnectorProps) {
|
||||
const { appState, appDispatch } = useAppState();
|
||||
const { open, close, isOpen } = useWeb3ConnectStore();
|
||||
|
||||
const setDialogOpen = useCallback(
|
||||
(isOpen: boolean) => {
|
||||
appDispatch({ type: AppStateActionType.SET_ETH_WALLET_OVERLAY, isOpen });
|
||||
if (isOpen) {
|
||||
open();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
},
|
||||
[appDispatch]
|
||||
[open, close]
|
||||
);
|
||||
|
||||
const appChainId = Number(chainId);
|
||||
return (
|
||||
<>
|
||||
<Web3Content appChainId={appChainId}>{children}</Web3Content>
|
||||
<Web3ConnectDialog
|
||||
connectors={connectors}
|
||||
dialogOpen={appState.ethConnectOverlay}
|
||||
dialogOpen={isOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
desiredChainId={appChainId}
|
||||
/>
|
||||
|
||||
@@ -34,9 +34,6 @@ 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;
|
||||
/**
|
||||
@@ -57,7 +54,6 @@ 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,
|
||||
@@ -81,10 +77,6 @@ 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,7 +16,6 @@ const initialAppState: AppState = {
|
||||
totalSupply: new BigNumber(0),
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
@@ -45,12 +44,6 @@ 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,6 +202,7 @@
|
||||
"tokenVotes": "Token votes",
|
||||
"liquidityVotes": "Liquidity votes",
|
||||
"castYourVote": "Cast your vote",
|
||||
"yourVote": "Your vote",
|
||||
"for": "For",
|
||||
"against": "Against",
|
||||
"majorityRequired": "Majority Required",
|
||||
@@ -787,9 +788,9 @@
|
||||
"homeVegaTokenButtonText": "Manage tokens",
|
||||
"downloadProposalJson": "Download proposal as JSON",
|
||||
"networkUpgrade": "Network Upgrade",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED": "Approved",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING": "Pending",
|
||||
"PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED": "Rejected",
|
||||
"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_UNSPECIFIED": "Unspecified",
|
||||
"vegaRelease{release}": "Vega Release {{release}}",
|
||||
"upgradeBlockHeight": "Upgrade block height",
|
||||
@@ -811,5 +812,6 @@
|
||||
"OptOutOfTelemetry": "You can opt out any time via settings",
|
||||
"NoThanks": "No thanks",
|
||||
"ShareData": "Share data",
|
||||
"ContinueSharingData": "Continue sharing data"
|
||||
"ContinueSharingData": "Continue sharing data",
|
||||
"NodeUnsuitable": "Node: {{url}} is unsuitable"
|
||||
}
|
||||
|
||||
@@ -50,7 +50,22 @@ export const useTranches = create<TranchesStore>()((set) => ({
|
||||
?.map((t) => {
|
||||
const tranche_progress =
|
||||
t.duration !== 0 ? (now - t.cliff_start) / t.duration : 0;
|
||||
const lockedDecimal = tranche_progress < 0 ? 1 : 1 - tranche_progress;
|
||||
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));
|
||||
|
||||
return {
|
||||
tranche_id: t.tranche_id,
|
||||
tranche_start: secondsToDate(t.cliff_start),
|
||||
@@ -60,7 +75,7 @@ export const useTranches = create<TranchesStore>()((set) => ({
|
||||
toBigNum(t.current_balance, decimals)
|
||||
),
|
||||
locked_amount: toBigNum(t.initial_balance, decimals).times(
|
||||
lockedDecimal
|
||||
clampedLockedDecimal
|
||||
),
|
||||
users: t.users,
|
||||
};
|
||||
|
||||
@@ -53,7 +53,6 @@ const mockAppState: AppState = {
|
||||
totalSupply: new BigNumber(65000000),
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
|
||||
@@ -24,7 +24,6 @@ const openProposalClosesNextMonth = generateProposal({
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextMonth.toString(),
|
||||
enactmentDatetime: nextMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,7 +35,6 @@ const openProposalClosesNextWeek = generateProposal({
|
||||
},
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -45,7 +43,6 @@ const enactedProposalClosedLastWeek = generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -54,7 +51,6 @@ const failedProposalClosedLastMonth = generateProposal({
|
||||
state: ProposalState.STATE_FAILED,
|
||||
terms: {
|
||||
closingDatetime: lastMonth.toString(),
|
||||
enactmentDatetime: lastMonth.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -32,7 +32,9 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
|
||||
|
||||
it('should render the state', () => {
|
||||
const { getByTestId } = renderComponent();
|
||||
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent('Pending');
|
||||
expect(getByTestId('protocol-upgrade-state')).toHaveTextContent(
|
||||
'Waiting for validator votes'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the vega release tag', () => {
|
||||
|
||||
+7
-1
@@ -26,28 +26,34 @@ 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 }) => {
|
||||
statuses.forEach(({ status, icon, text }) => {
|
||||
renderComponent({ ...proposal, status });
|
||||
const statusIcon = screen.getByTestId(icon);
|
||||
const textContent = screen.getByText(text);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(textContent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -111,7 +111,9 @@ 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,7 +2,12 @@ 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 } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
ButtonLink,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimal, toBigNum } from '@vegaprotocol/utils';
|
||||
import { ProposalState, VoteValue } from '@vegaprotocol/types';
|
||||
import {
|
||||
@@ -161,7 +166,12 @@ export const VoteButtons = ({
|
||||
{changeVote || (voteState === VoteState.NotCast && proposalVotable) ? (
|
||||
<>
|
||||
{currentStakeAvailable.isLessThanOrEqualTo(0) && (
|
||||
<p data-testid="no-stake-available">{t('noGovernanceTokens')}</p>
|
||||
<>
|
||||
<p data-testid="no-stake-available">{t('noGovernanceTokens')}.</p>
|
||||
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
|
||||
{t('findOutMoreAboutHowToVote')}
|
||||
</ExternalLink>
|
||||
</>
|
||||
)}
|
||||
|
||||
<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 } from '@vegaprotocol/ui-toolkit';
|
||||
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useVoteSubmit, VoteProgress } from '@vegaprotocol/proposals';
|
||||
@@ -202,7 +202,12 @@ export const VoteDetails = ({
|
||||
)}
|
||||
|
||||
<section className="mt-10">
|
||||
<SubHeading title={t('castYourVote')} />
|
||||
{proposal?.state === ProposalState.STATE_OPEN ? (
|
||||
<SubHeading title={t('castYourVote')} />
|
||||
) : (
|
||||
<SubHeading title={t('yourVote')} />
|
||||
)}
|
||||
|
||||
{pubKey ? (
|
||||
proposal && (
|
||||
<VoteButtonsContainer
|
||||
@@ -224,6 +229,9 @@ 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,7 +16,6 @@ const mockAppState: AppState = {
|
||||
totalSupply: mockTotalSupply,
|
||||
vegaWalletOverlay: false,
|
||||
vegaWalletManageOverlay: false,
|
||||
ethConnectOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
|
||||
@@ -20,8 +20,11 @@ import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
|
||||
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||
orderBy(
|
||||
arr,
|
||||
[(p) => new Date(p?.terms?.closingDatetime).getTime(), (p) => p.id],
|
||||
['desc', 'desc']
|
||||
[
|
||||
(p) => new Date(p?.terms?.closingDatetime).getTime(),
|
||||
(p) => new Date(p?.datetime).getTime(),
|
||||
],
|
||||
['asc', 'asc']
|
||||
);
|
||||
|
||||
const orderByUpgradeBlockHeight = (
|
||||
|
||||
@@ -54,22 +54,16 @@ export const TrancheItem = ({
|
||||
{formatNumber(total, 2)}
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
<LockedProgress
|
||||
locked={locked}
|
||||
unlocked={unlocked}
|
||||
|
||||
+18
-1
@@ -79,7 +79,24 @@ export const generateEpochIndividualRewardsList = ({
|
||||
epoch?.rewards.push(asset);
|
||||
}
|
||||
|
||||
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// totalAmount is the sum of all rewardTypes amounts
|
||||
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
|
||||
|
||||
+8
-2
@@ -34,6 +34,7 @@ import {
|
||||
formatNumberPercentage,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { VALIDATOR_LOGO_MAP } from './logo-map';
|
||||
|
||||
interface CanonisedConsensusNodeProps {
|
||||
id: string;
|
||||
@@ -161,6 +162,11 @@ export const ConsensusValidatorsTable = ({
|
||||
pendingUserStake,
|
||||
userStakeShare,
|
||||
}) => {
|
||||
const logo = VALIDATOR_LOGO_MAP[id]
|
||||
? VALIDATOR_LOGO_MAP[id]
|
||||
: avatarUrl
|
||||
? avatarUrl
|
||||
: null;
|
||||
const {
|
||||
rawValidatorScore: previousEpochValidatorScore,
|
||||
performanceScore: previousEpochPerformanceScore,
|
||||
@@ -171,7 +177,7 @@ export const ConsensusValidatorsTable = ({
|
||||
id,
|
||||
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
|
||||
[ValidatorFields.VALIDATOR]: {
|
||||
avatarUrl,
|
||||
avatarUrl: logo,
|
||||
name,
|
||||
},
|
||||
[ValidatorFields.STAKE]: stakedTotal,
|
||||
@@ -210,7 +216,7 @@ export const ConsensusValidatorsTable = ({
|
||||
: undefined,
|
||||
[ValidatorFields.PENDING_USER_STAKE]: pendingUserStake,
|
||||
[ValidatorFields.USER_STAKE_SHARE]: userStakeShare
|
||||
? formatNumberPercentage(new BigNumber(userStakeShare))
|
||||
? formatNumberPercentage(new BigNumber(userStakeShare), 2)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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,6 +16,7 @@ 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',
|
||||
@@ -102,13 +103,18 @@ 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">
|
||||
{avatarUrl && (
|
||||
{logo && (
|
||||
<img
|
||||
className="h-6 w-6 rounded-full mr-2"
|
||||
src={avatarUrl}
|
||||
src={logo}
|
||||
alt={`Avatar icon for ${name}`}
|
||||
onError={(e) => (e.currentTarget.style.display = 'none')}
|
||||
/>
|
||||
|
||||
@@ -78,37 +78,38 @@ export const ValidatorTables = ({
|
||||
const nextDelegation = delegations?.find(
|
||||
(d) => d.node.id === validatorId && d.epoch === Number(epochId) + 1
|
||||
);
|
||||
|
||||
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
|
||||
)
|
||||
);
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export const NodeContainer = ({
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { delegationsPagination } = ENV;
|
||||
|
||||
const { data, loading, error, refetch } = useStakingQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
@@ -37,7 +38,9 @@ export const NodeContainer = ({
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
errorPolicy: 'all',
|
||||
});
|
||||
|
||||
const { data: previousEpochData } = usePreviousEpochQuery({
|
||||
variables: {
|
||||
epochId: (Number(data?.epoch.id) - 1).toString(),
|
||||
@@ -47,7 +50,7 @@ export const NodeContainer = ({
|
||||
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
if (error) {
|
||||
if (error && !error.message.includes('failed to get party for ID')) {
|
||||
return (
|
||||
<Callout intent={Intent.Danger} title={t('Something went wrong')}>
|
||||
<pre>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ module.exports = defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
projectId: 'et4snf',
|
||||
defaultCommandTimeout: 10000,
|
||||
viewportWidth: 1440,
|
||||
viewportWidth: 1800,
|
||||
viewportHeight: 900,
|
||||
responseTimeout: 50000,
|
||||
requestTimeout: 20000,
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createMarketsDataFragment,
|
||||
assetQuery,
|
||||
networkParamsQuery,
|
||||
nodeGuardQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@@ -158,6 +159,7 @@ 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,6 +6,10 @@ 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(() => {
|
||||
@@ -28,7 +32,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
|
||||
it('show oracle banner', () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
cy.getByTestId('oracle-status').should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerStatus).should('contain.text', 'COMPROMISED');
|
||||
cy.getByTestId(oracleBannerDialogTrigger)
|
||||
.should('contain.text', 'Show more')
|
||||
.click();
|
||||
cy.getByTestId(oracleFullProfile).should('exist');
|
||||
});
|
||||
|
||||
it('current fees displayed', () => {
|
||||
@@ -181,9 +189,19 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId(marketTitle).contains('Oracle').click();
|
||||
|
||||
cy.getByTestId(accordionContent)
|
||||
.getByTestId('provider-name')
|
||||
.getByTestId(providerName)
|
||||
.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');
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
AuctionTrigger,
|
||||
MarketState,
|
||||
MarketTradingMode,
|
||||
} from '@vegaprotocol/types';
|
||||
|
||||
describe('markets selector', { tags: '@smoke' }, () => {
|
||||
const list = 'market-selector-list';
|
||||
const searchInput = 'search-term';
|
||||
|
||||
beforeEach(() => {
|
||||
cy.window().then((window) => {
|
||||
window.localStorage.setItem('marketId', 'market-1');
|
||||
});
|
||||
cy.mockTradingPage(
|
||||
MarketState.STATE_ACTIVE,
|
||||
MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
|
||||
);
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
|
||||
cy.wait('@Markets');
|
||||
cy.wait('@MarketsData');
|
||||
cy.wait('@MarketsCandles');
|
||||
});
|
||||
|
||||
it('can toggle the sidebar', () => {
|
||||
cy.getByTestId('market-selector').should('be.visible');
|
||||
cy.getByTestId('sidebar-toggle').click();
|
||||
cy.getByTestId('market-selector').should('not.exist');
|
||||
cy.getByTestId('sidebar-toggle').click();
|
||||
cy.getByTestId('market-selector').should('be.visible');
|
||||
});
|
||||
|
||||
// need function keyword as we need 'this' to access market data
|
||||
it('displays data as expected', () => {
|
||||
// TODO: load data from mocks in. Using alias and wrap intermittently fails
|
||||
const data = [
|
||||
{
|
||||
code: 'AAPL.MF21',
|
||||
name: 'Apple Monthly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'BTCUSD.MF21',
|
||||
name: 'ACTIVE MARKET',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'ETHBTC.QM21',
|
||||
name: 'ETHBTC Quarterly (30 Jun 2022)',
|
||||
markPrice: '46,126.90058',
|
||||
change: '+200.00%',
|
||||
},
|
||||
{
|
||||
code: 'SOLUSD',
|
||||
name: 'SUSPENDED MARKET',
|
||||
markPrice: '84.41',
|
||||
change: '+200.00%',
|
||||
},
|
||||
];
|
||||
cy.getByTestId(list)
|
||||
.find('a')
|
||||
.each((item, i) => {
|
||||
const market = data[i];
|
||||
// 6001-MARK-021
|
||||
expect(item.find('h3').text()).equals(market.code);
|
||||
// 6001-MARK-022
|
||||
expect(item.find('h4').text()).equals(market.name);
|
||||
// 6001-MARK-024
|
||||
expect(item.find('[data-testid="market-item-price"]').text()).equals(
|
||||
market.markPrice
|
||||
);
|
||||
// 6001-MARK-023
|
||||
expect(item.find('[data-testid="market-item-change"]').text()).equals(
|
||||
market.change
|
||||
);
|
||||
// 6001-MARK-025
|
||||
expect(item.find('[data-testid="sparkline-svg"]')).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
// 6001-MARK-27
|
||||
it('can use the filter options', () => {
|
||||
// product type
|
||||
cy.getByTestId('product-Spot').click();
|
||||
cy.getByTestId(list).contains('Spot markets coming soon.');
|
||||
cy.getByTestId('product-Perpetual').click();
|
||||
cy.getByTestId(list).contains('Perpetual markets coming soon.');
|
||||
cy.getByTestId('product-Future').click();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
|
||||
// 6001-MARK-29
|
||||
cy.getByTestId(searchInput).clear().type('btc');
|
||||
cy.getByTestId(list).find('a').should('have.length', 2);
|
||||
cy.getByTestId(list).find('a').eq(0).contains('BTCUSD.MF21');
|
||||
cy.getByTestId(list).find('a').eq(1).contains('ETHBTC.QM21');
|
||||
|
||||
cy.getByTestId(searchInput).clear();
|
||||
cy.getByTestId(list).find('a').should('have.length', 4);
|
||||
});
|
||||
});
|
||||
@@ -31,7 +31,7 @@ describe('Market trading page', () => {
|
||||
// 7002-SORD-001
|
||||
// 7002-SORD-002
|
||||
it('must display market name', () => {
|
||||
cy.getByTestId('popover-trigger').should('not.be.empty');
|
||||
cy.getByTestId('header-title').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('must see market expiry', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { marketsQuery } from '@vegaprotocol/mock';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
|
||||
const dialogCloseBtn = 'dialog-close';
|
||||
const popoverTrigger = 'popover-trigger';
|
||||
|
||||
describe('markets table', { tags: '@smoke' }, () => {
|
||||
beforeEach(() => {
|
||||
@@ -31,36 +30,6 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('sparkline-svg').should('be.visible');
|
||||
});
|
||||
|
||||
it('renders market list drop down', () => {
|
||||
openMarketDropDown();
|
||||
cy.getByTestId('price').invoke('text').should('not.be.empty');
|
||||
cy.getByTestId('trading-mode-col').should('not.be.empty');
|
||||
cy.getByTestId('taker-fee').should('contain.text', '%');
|
||||
cy.getByTestId('market-volume').should('not.be.empty');
|
||||
cy.getByTestId('market-name').should('not.be.empty');
|
||||
|
||||
cy.getByTestId('trading-mode-col')
|
||||
.contains('Monitoring auction - liquidity')
|
||||
.eq(0)
|
||||
.realHover();
|
||||
cy.get('[data-testid="trading-mode-tooltip"] p').should('have.class', '');
|
||||
cy.get(
|
||||
'[data-testid="market-trading-mode"] [data-testid="item-value"]'
|
||||
).realHover();
|
||||
cy.get('[data-testid="trading-mode-tooltip"] p').should(
|
||||
'have.class',
|
||||
'mb-4'
|
||||
);
|
||||
});
|
||||
|
||||
it('able to select market from dropdown', () => {
|
||||
openMarketDropDown();
|
||||
cy.getByTestId('market-link-market-0').first().should('be.visible').click();
|
||||
cy.contains('ACTIVE MARKET').should('be.visible');
|
||||
cy.url().should('include', '/markets/market-0');
|
||||
cy.getByTestId('popover-trigger').should('not.be.empty');
|
||||
});
|
||||
|
||||
it('able to open and sort full market list - market page', () => {
|
||||
const ExpectedSortedMarkets = [
|
||||
'AAPL.MF21',
|
||||
@@ -253,15 +222,3 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId(dialogCloseBtn).then((button) => {
|
||||
if (button.is(':visible')) {
|
||||
cy.get('[data-testid^="market-link-"]').should('not.be.empty');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
}
|
||||
cy.get('[data-testid^="ask-vol-"]').should('be.visible');
|
||||
cy.getByTestId(popoverTrigger).click({ force: true });
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
});
|
||||
|
||||
describe('limit order', () => {
|
||||
before(() => {
|
||||
cy.getByTestId(toggleLimit).click();
|
||||
@@ -98,7 +102,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,10 +1,6 @@
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import {
|
||||
accountsQuery,
|
||||
amendGeneralAccountBalance,
|
||||
estimateOrderQuery,
|
||||
} from '@vegaprotocol/mock';
|
||||
import { accountsQuery, amendGeneralAccountBalance } from '@vegaprotocol/mock';
|
||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||
import { createOrder } from '../support/create-order';
|
||||
|
||||
@@ -44,13 +40,10 @@ describe(
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage();
|
||||
const accounts = accountsQuery();
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '100000000');
|
||||
amendGeneralAccountBalance(accounts, 'market-0', '1');
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'Accounts', accounts);
|
||||
});
|
||||
cy.mockGQL((req) => {
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.wait('@Markets');
|
||||
@@ -66,7 +59,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'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.'
|
||||
'You may not have enough margin available to open this position. 5.00 tDAI is currently required. You have only 0.01001 tDAI available.'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||
cy.getByTestId('dialog-content')
|
||||
|
||||
@@ -291,12 +291,58 @@ 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"]')
|
||||
@@ -337,6 +383,7 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
|
||||
updateOrder({
|
||||
id: orderId,
|
||||
status: Schema.OrderStatus.STATUS_ACTIVE,
|
||||
peggedOrder: {},
|
||||
liquidityProvisionId: '6536',
|
||||
});
|
||||
cy.get(`[row-id=${orderId}]`)
|
||||
|
||||
@@ -17,7 +17,6 @@ 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();
|
||||
@@ -30,7 +29,6 @@ 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();
|
||||
|
||||
@@ -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/oracles';
|
||||
import type { Provider, Status } from '@vegaprotocol/market-info';
|
||||
import {
|
||||
accountsQuery,
|
||||
assetQuery,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
chainIdQuery,
|
||||
chartQuery,
|
||||
depositsQuery,
|
||||
estimateOrderQuery,
|
||||
estimateFeesQuery,
|
||||
marginsQuery,
|
||||
marketCandlesQuery,
|
||||
marketDataQuery,
|
||||
@@ -20,12 +20,16 @@ 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';
|
||||
@@ -82,6 +86,7 @@ const mockTradingPage = (
|
||||
) => {
|
||||
aliasGQLQuery(req, 'ChainId', chainIdQuery());
|
||||
aliasGQLQuery(req, 'Statistics', statisticsQuery());
|
||||
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
|
||||
aliasGQLQuery(
|
||||
req,
|
||||
'Markets',
|
||||
@@ -155,9 +160,16 @@ const mockTradingPage = (
|
||||
aliasGQLQuery(req, 'Candles', candlesQuery());
|
||||
aliasGQLQuery(req, 'Withdrawals', withdrawalsQuery());
|
||||
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
|
||||
aliasGQLQuery(req, 'EstimateOrder', estimateOrderQuery());
|
||||
aliasGQLQuery(req, 'EstimateFees', estimateFeesQuery());
|
||||
aliasGQLQuery(req, 'EstimatePosition', estimatePositionQuery());
|
||||
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
@@ -0,0 +1,6 @@
|
||||
function ReactMarkdown({ children }) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default ReactMarkdown;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { AssetDropdown } from './asset-dropdown';
|
||||
|
||||
const createAssets = (count = 3) => {
|
||||
return new Array(count).fill(null).map((_, i) => ({
|
||||
id: i.toString(),
|
||||
symbol: 'asset-1',
|
||||
}));
|
||||
};
|
||||
|
||||
describe('AssetDropdown', () => {
|
||||
const assets = createAssets();
|
||||
|
||||
it('renders and selects chosen assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[]}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
expect(items).toHaveLength(assets.length);
|
||||
expect(items.map((i) => i.textContent)).toEqual(
|
||||
assets.map((a) => a.symbol)
|
||||
);
|
||||
await userEvent.click(items[0]);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, true);
|
||||
});
|
||||
|
||||
it('unselects already selected assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[assets[0].id]}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
await userEvent.click(items[0]);
|
||||
expect(mockOnSelect).toHaveBeenCalledWith(assets[0].id, false);
|
||||
});
|
||||
|
||||
it('can be reset clearing all assets', async () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
const mockOnReset = jest.fn();
|
||||
render(
|
||||
<AssetDropdown
|
||||
checkedAssets={assets.map((a) => a.id)}
|
||||
assets={assets}
|
||||
onSelect={mockOnSelect}
|
||||
onReset={mockOnReset}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const items = screen.getAllByRole('menuitemcheckbox');
|
||||
// all should be checked
|
||||
items.forEach((item) => {
|
||||
expect(item).toBeChecked();
|
||||
});
|
||||
await userEvent.click(screen.getByText('Reset'));
|
||||
expect(mockOnReset).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('doesnt render if no assets provided', async () => {
|
||||
const { container } = render(
|
||||
<AssetDropdown
|
||||
checkedAssets={[]}
|
||||
assets={[]}
|
||||
onSelect={jest.fn()}
|
||||
onReset={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const AssetDropdown = ({
|
||||
assets,
|
||||
checkedAssets,
|
||||
onSelect,
|
||||
onReset,
|
||||
}: {
|
||||
assets: Array<{ id: string; symbol: string }> | undefined;
|
||||
checkedAssets: string[];
|
||||
onSelect: (id: string, checked: boolean) => void;
|
||||
onReset: () => void;
|
||||
}) => {
|
||||
if (!assets?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger data-testid="asset-trigger">
|
||||
<span className="px-1">$</span>
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{assets?.map((a) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={a.id}
|
||||
checked={checkedAssets.includes(a.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (typeof checked === 'boolean') {
|
||||
onSelect(a.id, checked);
|
||||
}
|
||||
}}
|
||||
data-testid={`asset-id-${a.id}`}
|
||||
>
|
||||
{a.symbol}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
+74
-92
@@ -5,13 +5,7 @@ import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { getExpiryDate, getMarketExpiryDate } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
ColumnKind,
|
||||
SelectMarketPopover,
|
||||
} from '../../components/select-market';
|
||||
import type { OnCellClickHandler } from '../../components/select-market';
|
||||
import { Header, HeaderStat } from '../../components/header';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { HeaderStat } from '../../components/header';
|
||||
import { MarketMarkPrice } from '../../components/market-mark-price';
|
||||
import { Last24hPriceChange, Last24hVolume } from '@vegaprotocol/market-info';
|
||||
import { MarketState } from '../../components/market-state';
|
||||
@@ -19,102 +13,90 @@ import { HeaderStatMarketTradingMode } from '../../components/market-trading-mod
|
||||
import { MarketLiquiditySupplied } from '../../components/liquidity-supplied';
|
||||
import { MarketState as State } from '@vegaprotocol/types';
|
||||
|
||||
interface TradeMarketHeaderProps {
|
||||
interface HeaderStatsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
}
|
||||
|
||||
export const TradeMarketHeader = ({
|
||||
market,
|
||||
onSelect,
|
||||
}: TradeMarketHeaderProps) => {
|
||||
export const HeaderStats = ({ market }: HeaderStatsProps) => {
|
||||
const { VEGA_EXPLORER_URL } = useEnvironment();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
|
||||
const asset = market?.tradableInstrument.instrument.product?.settlementAsset;
|
||||
|
||||
const onCellClick: OnCellClickHandler = (e, kind, value) => {
|
||||
if (value && kind === ColumnKind.Asset) {
|
||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Header
|
||||
title={
|
||||
<SelectMarketPopover
|
||||
marketCode={market?.tradableInstrument.instrument.code || NO_MARKET}
|
||||
marketName={market?.tradableInstrument.instrument.name || NO_MARKET}
|
||||
onSelect={onSelect}
|
||||
onCellClick={onCellClick}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<HeaderStat
|
||||
heading={t('Expiry')}
|
||||
description={
|
||||
market && (
|
||||
<ExpiryTooltipContent
|
||||
market={market}
|
||||
explorerUrl={VEGA_EXPLORER_URL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="market-expiry"
|
||||
>
|
||||
<ExpiryLabel market={market} />
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Price')} testId="market-price">
|
||||
<MarketMarkPrice
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Change (24h)')} testId="market-change">
|
||||
<Last24hPriceChange
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Volume (24h)')}
|
||||
testId="market-volume"
|
||||
description={t(
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<Last24hVolume
|
||||
marketId={market?.id}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
marketId={market?.id}
|
||||
initialTradingMode={market?.tradingMode}
|
||||
/>
|
||||
<MarketState market={market} />
|
||||
{asset ? (
|
||||
<HeaderStat
|
||||
heading={t('Settlement asset')}
|
||||
testId="market-settlement-asset"
|
||||
<div className="flex flex-col justify-end lg:pt-4">
|
||||
<div className="xl:flex xl:gap-4 items-end">
|
||||
<div
|
||||
data-testid="header-summary"
|
||||
className="flex flex-nowrap items-end xl:flex-1 w-full overflow-x-auto text-xs"
|
||||
>
|
||||
<div>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
|
||||
}}
|
||||
<HeaderStat
|
||||
heading={t('Expiry')}
|
||||
description={
|
||||
market && (
|
||||
<ExpiryTooltipContent
|
||||
market={market}
|
||||
explorerUrl={VEGA_EXPLORER_URL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="market-expiry"
|
||||
>
|
||||
<ExpiryLabel market={market} />
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Price')} testId="market-price">
|
||||
<MarketMarkPrice
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Change (24h)')} testId="market-change">
|
||||
<Last24hPriceChange
|
||||
marketId={market?.id}
|
||||
decimalPlaces={market?.decimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat
|
||||
heading={t('Volume (24h)')}
|
||||
testId="market-volume"
|
||||
description={t(
|
||||
'The total number of contracts traded in the last 24 hours.'
|
||||
)}
|
||||
>
|
||||
<Last24hVolume
|
||||
marketId={market?.id}
|
||||
positionDecimalPlaces={market?.positionDecimalPlaces}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
marketId={market?.id}
|
||||
initialTradingMode={market?.tradingMode}
|
||||
/>
|
||||
<MarketState market={market} />
|
||||
{asset ? (
|
||||
<HeaderStat
|
||||
heading={t('Settlement asset')}
|
||||
testId="market-settlement-asset"
|
||||
>
|
||||
{asset.symbol}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
) : null}
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market?.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
/>
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
</Header>
|
||||
<div>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(asset.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{asset.symbol}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</HeaderStat>
|
||||
) : null}
|
||||
<MarketLiquiditySupplied
|
||||
marketId={market?.id}
|
||||
assetDecimals={asset?.decimals || 0}
|
||||
/>
|
||||
<MarketProposalNotification marketId={market?.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { MarketSelectorItem } from './market-selector-item';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type {
|
||||
MarketDataUpdateFieldsFragment,
|
||||
MarketDataUpdateSubscription,
|
||||
} from '@vegaprotocol/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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,289 @@
|
||||
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]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
import { AsyncRenderer, ExternalLink, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { marketProvider, marketDataProvider } from '@vegaprotocol/market-list';
|
||||
import { useGlobalStore, usePageTitleStore } from '../../stores';
|
||||
import { TradeGrid, TradePanels } from './trade-grid';
|
||||
import { TradeGrid } from './trade-grid';
|
||||
import { TradePanels } from './trade-panels';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Links, Routes } from '../../pages/client-router';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
@@ -100,6 +101,7 @@ export const MarketPage = () => {
|
||||
/>
|
||||
);
|
||||
}, [largeScreen, data, onSelect, navigate]);
|
||||
|
||||
if (!data && marketId) {
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Make sure these match the available __typename properties on product
|
||||
export const Product = {
|
||||
Spot: 'Spot',
|
||||
Future: 'Future',
|
||||
Perpetual: 'Perpetual',
|
||||
} as const;
|
||||
|
||||
export type ProductType = keyof typeof Product;
|
||||
|
||||
const ProductTypeMapping: {
|
||||
[key in ProductType]: string;
|
||||
} = {
|
||||
[Product.Spot]: 'Spot',
|
||||
[Product.Future]: 'Futures',
|
||||
[Product.Perpetual]: 'Perpetuals',
|
||||
};
|
||||
|
||||
export const ProductSelector = ({
|
||||
product,
|
||||
onSelect,
|
||||
}: {
|
||||
product: ProductType;
|
||||
onSelect: (product: ProductType) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex gap-3 mb-3">
|
||||
{Object.keys(Product).map((t) => {
|
||||
const classes = classNames('py-1 border-b-2', {
|
||||
'border-vega-yellow text-black dark:text-white': t === product,
|
||||
'border-transparent text-vega-light-300 dark:text-vega-dark-300':
|
||||
t !== product,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => {
|
||||
onSelect(t as ProductType);
|
||||
}}
|
||||
className={classes}
|
||||
data-testid={`product-${t}`}
|
||||
>
|
||||
{ProductTypeMapping[t as ProductType]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Sort = {
|
||||
None: 'None',
|
||||
Gained: 'Gained',
|
||||
Lost: 'Lost',
|
||||
New: 'New',
|
||||
} as const;
|
||||
|
||||
export type SortType = keyof typeof Sort;
|
||||
|
||||
export const SortTypeMapping: {
|
||||
[key in SortType]: string;
|
||||
} = {
|
||||
[Sort.None]: 'None',
|
||||
[Sort.Gained]: 'Top gaining',
|
||||
[Sort.Lost]: 'Top losing',
|
||||
[Sort.New]: 'New markets',
|
||||
};
|
||||
|
||||
export const SortDropdown = ({
|
||||
currentSort,
|
||||
onSelect,
|
||||
onReset,
|
||||
}: {
|
||||
currentSort: SortType;
|
||||
onSelect: (sort: SortType) => void;
|
||||
onReset: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
trigger={
|
||||
<DropdownMenuTrigger data-testid="sort-trigger">
|
||||
<VegaIcon name={VegaIconNames.TREND_UP} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuRadioGroup
|
||||
value={currentSort}
|
||||
onValueChange={(value) => onSelect(value as SortType)}
|
||||
>
|
||||
<DropdownMenuItem onClick={onReset}>{t('Reset')}</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{Object.keys(Sort)
|
||||
.filter((s) => s !== Sort.None)
|
||||
.map((key) => {
|
||||
return (
|
||||
<DropdownMenuRadioItem
|
||||
inset
|
||||
key={key}
|
||||
value={key}
|
||||
data-testid={`sort-item-${key}`}
|
||||
>
|
||||
{SortTypeMapping[key as SortType]}
|
||||
<DropdownMenuItemIndicator />
|
||||
</DropdownMenuRadioItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
@@ -1,120 +1,37 @@
|
||||
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
|
||||
import { MarketInfoAccordionContainer } from '@vegaprotocol/market-info';
|
||||
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||
import { OrderListContainer, Filter } from '@vegaprotocol/orders';
|
||||
import type { OrderListContainerProps } from '@vegaprotocol/orders';
|
||||
import { FillsContainer } from '@vegaprotocol/fills';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { memo, useState } from 'react';
|
||||
import type { ReactNode, ComponentProps } from 'react';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
import { OracleBanner } from '../../components/banner';
|
||||
import {
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
Splash,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { TradeMarketHeader } from './trade-market-header';
|
||||
import { NO_MARKET } from './constants';
|
||||
import { LiquidityContainer } from '../liquidity/liquidity';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { OracleBanner } from '@vegaprotocol/market-info';
|
||||
import type { Market } from '@vegaprotocol/market-list';
|
||||
import { Filter } from '@vegaprotocol/orders';
|
||||
import {
|
||||
usePaneLayout,
|
||||
useScreenDimensions,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Tab,
|
||||
LocalStoragePersistTabs as Tabs,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useMarketClickHandler,
|
||||
useMarketLiquidityClickHandler,
|
||||
} from '../../lib/hooks/use-market-click-handler';
|
||||
import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
||||
import { HeaderTitle } from '../../components/header';
|
||||
import {
|
||||
ResizableGrid,
|
||||
ResizableGridPanel,
|
||||
} from '../../components/resizable-grid';
|
||||
|
||||
type MarketDependantView =
|
||||
| typeof CandlesChartContainer
|
||||
| typeof DepthChartContainer
|
||||
| typeof DealTicketContainer
|
||||
| typeof MarketInfoAccordionContainer
|
||||
| typeof OrderbookContainer
|
||||
| typeof TradesContainer;
|
||||
|
||||
type MarketDependantViewProps = ComponentProps<MarketDependantView>;
|
||||
|
||||
const requiresMarket = (View: MarketDependantView) => {
|
||||
const WrappedComponent = (props: MarketDependantViewProps) =>
|
||||
props.marketId ? <View {...props} /> : <Splash>{NO_MARKET}</Splash>;
|
||||
WrappedComponent.displayName = `RequiresMarket(${View.name})`;
|
||||
return WrappedComponent;
|
||||
};
|
||||
|
||||
const TradingViews = {
|
||||
candles: {
|
||||
label: 'Candles',
|
||||
component: requiresMarket(CandlesChartContainer),
|
||||
},
|
||||
depth: {
|
||||
label: 'Depth',
|
||||
component: requiresMarket(DepthChartContainer),
|
||||
},
|
||||
liquidity: {
|
||||
label: 'Liquidity',
|
||||
component: requiresMarket(LiquidityContainer),
|
||||
},
|
||||
ticket: {
|
||||
label: 'Ticket',
|
||||
component: requiresMarket(DealTicketContainer),
|
||||
},
|
||||
info: {
|
||||
label: 'Info',
|
||||
component: requiresMarket(MarketInfoAccordionContainer),
|
||||
},
|
||||
orderbook: {
|
||||
label: 'Orderbook',
|
||||
component: requiresMarket(OrderbookContainer),
|
||||
},
|
||||
trades: {
|
||||
label: 'Trades',
|
||||
component: requiresMarket(TradesContainer),
|
||||
},
|
||||
positions: { label: 'Positions', component: PositionsContainer },
|
||||
activeOrders: {
|
||||
label: 'Active',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Open} />
|
||||
),
|
||||
},
|
||||
closedOrders: {
|
||||
label: 'Closed',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Closed} />
|
||||
),
|
||||
},
|
||||
rejectedOrders: {
|
||||
label: 'Rejected',
|
||||
component: (props: OrderListContainerProps) => (
|
||||
<OrderListContainer {...props} filter={Filter.Rejected} />
|
||||
),
|
||||
},
|
||||
orders: {
|
||||
label: 'All',
|
||||
component: OrderListContainer,
|
||||
},
|
||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||
fills: { label: 'Fills', component: FillsContainer },
|
||||
};
|
||||
|
||||
type TradingView = keyof typeof TradingViews;
|
||||
import { TradingViews } from './trade-views';
|
||||
import { MarketSelector } from './market-selector';
|
||||
import { HeaderStats } from './header-stats';
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
@@ -410,18 +327,57 @@ const MainGrid = memo(
|
||||
);
|
||||
MainGrid.displayName = 'MainGrid';
|
||||
|
||||
export const TradeGrid = ({
|
||||
market,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: TradeGridProps) => {
|
||||
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const wrapperClasses = classNames(
|
||||
'h-full grid',
|
||||
'grid-rows-[min-content_min-content_1fr]',
|
||||
'grid-cols-[320px_1fr]'
|
||||
);
|
||||
const paneWrapperClasses = classNames('min-h-0', {
|
||||
'col-span-2 col-start-1': !sidebarOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<div>
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<div className={wrapperClasses}>
|
||||
<div className="border-b border-r border-default">
|
||||
<div className="flex gap-2 justify-between items-center px-4 py-2">
|
||||
<HeaderTitle
|
||||
primaryContent={market?.tradableInstrument.instrument.code}
|
||||
secondaryContent={market?.tradableInstrument.instrument.name}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setSidebarOpen((x) => !x)}
|
||||
className="p-2"
|
||||
data-testid="sidebar-toggle"
|
||||
>
|
||||
<span
|
||||
className={classNames('block', {
|
||||
'rotate-90 translate-x-1': !sidebarOpen,
|
||||
'-rotate-90 -translate-x-1': sidebarOpen,
|
||||
})}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-b border-default min-w-0">
|
||||
<HeaderStats market={market} />
|
||||
</div>
|
||||
<div className="col-span-2 bg-vega-green">
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
{sidebarOpen && (
|
||||
<div className="border-r border-default min-h-0">
|
||||
<div className="h-full pb-8">
|
||||
<MarketSelector currentMarketId={market?.id} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={paneWrapperClasses}>
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -439,88 +395,3 @@ const TradeGridChild = ({ children }: TradeGridChildProps) => {
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({
|
||||
market,
|
||||
onSelect,
|
||||
onClickCollateral,
|
||||
pinnedAsset,
|
||||
}: TradePanelsProps) => {
|
||||
const onMarketClick = useMarketClickHandler(true);
|
||||
const onOrderTypeClick = useMarketLiquidityClickHandler(true);
|
||||
|
||||
const [view, setView] = useState<TradingView>('candles');
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
onSelect: (marketId: string, metaKey?: boolean) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onOrderTypeClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}>(TradingViews[view].component);
|
||||
|
||||
if (!Component) {
|
||||
throw new Error(`No component for view: ${view}`);
|
||||
}
|
||||
|
||||
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
||||
|
||||
return (
|
||||
<Component
|
||||
marketId={market?.id}
|
||||
onSelect={onSelect}
|
||||
onClickCollateral={onClickCollateral}
|
||||
pinnedAsset={pinnedAsset}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
||||
<div>
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
</div>
|
||||
<div className="h-full">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
{renderView()}
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews).map((key) => {
|
||||
const isActive = view === key;
|
||||
const className = classNames('p-4 min-w-[100px] capitalize', {
|
||||
'text-black dark:text-vega-yellow': isActive,
|
||||
'bg-neutral-200 dark:bg-neutral-800': isActive,
|
||||
});
|
||||
return (
|
||||
<button
|
||||
data-testid={key}
|
||||
onClick={() => setView(key as TradingView)}
|
||||
className={className}
|
||||
key={key}
|
||||
>
|
||||
{TradingViews[key as keyof typeof TradingViews].label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import type { Market } from '@vegaprotocol/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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
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 },
|
||||
};
|
||||
@@ -0,0 +1,473 @@
|
||||
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],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
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/oracles';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
|
||||
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/oracles';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/oracles';
|
||||
import type { OracleSpecDataConnectionQuery } from '@vegaprotocol/market-info';
|
||||
import { OracleSpecDataConnectionDocument } from '@vegaprotocol/market-info';
|
||||
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/oracles';
|
||||
import { useOracleSpecBindingData } from '@vegaprotocol/market-info';
|
||||
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 { COG } from '@blueprintjs/icons/src/generated/iconNames';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
|
||||
export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
|
||||
return (
|
||||
@@ -9,7 +9,7 @@ export const SettingsButton = ({ withMobile }: { withMobile?: boolean }) => {
|
||||
{withMobile ? (
|
||||
t('Settings')
|
||||
) : (
|
||||
<Icon name={COG} className="!align-middle" />
|
||||
<Icon name={IconNames.COG} className="!align-middle" />
|
||||
)}
|
||||
</NavigationLink>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import {
|
||||
AppFailure,
|
||||
NetworkLoader,
|
||||
NodeGuard,
|
||||
useEnvironment,
|
||||
@@ -9,7 +10,6 @@ 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,3 +1,2 @@
|
||||
export * from './app-failure';
|
||||
export * from './app-loader';
|
||||
export * from './web3-provider';
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './announcement-banner';
|
||||
export * from './oracle-banner';
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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,18 +4,15 @@ 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',
|
||||
})),
|
||||
}));
|
||||
|
||||
const mockSetNodeSwitcher = jest.fn();
|
||||
jest.mock('../../stores', () => ({
|
||||
...jest.requireActual('../../stores'),
|
||||
useGlobalStore: () => mockSetNodeSwitcher,
|
||||
useNodeSwitcherStore: jest.fn(() => mockSetNodeSwitcher),
|
||||
}));
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||
import {
|
||||
useEnvironment,
|
||||
useNodeHealth,
|
||||
useNodeSwitcherStore,
|
||||
} 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 (
|
||||
@@ -20,9 +23,7 @@ export const Footer = () => {
|
||||
|
||||
export const NodeHealth = () => {
|
||||
const { VEGA_URL, VEGA_INCIDENT_URL } = useEnvironment();
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
|
||||
const { datanodeBlockHeight, text, intent } = useNodeHealth();
|
||||
const onClick = useCallback(() => {
|
||||
setNodeSwitcher(true);
|
||||
|
||||
@@ -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 text-neutral-500 dark:text-neutral-400';
|
||||
'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';
|
||||
const itemHeading = 'text-black dark:text-white';
|
||||
|
||||
return (
|
||||
@@ -72,7 +72,7 @@ export const HeaderTitle = ({
|
||||
secondaryContent: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className="text-left">
|
||||
<div className="text-left" data-testid="header-title">
|
||||
<div className="text-sm md:text-md lg:text-lg whitespace-nowrap leading-4">
|
||||
{primaryContent}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -75,7 +75,7 @@ export const Navbar = ({
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink data-testid="Trading" to={tradingPath}>
|
||||
<NavigationLink data-testid="Trading" to={tradingPath} end>
|
||||
{t('Trading')}
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
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, signedNumberCssClass } from '@vegaprotocol/datagrid';
|
||||
import { PriceCell } 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,187 +341,3 @@ 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,7 +1,6 @@
|
||||
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,
|
||||
@@ -62,23 +61,3 @@ 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
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,7 +7,13 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { RISK_ACCEPTED_KEY } from '../constants';
|
||||
import { TelemetryApproval } from './telemetry-approval';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import {
|
||||
DOCS_VEGA_WALLET,
|
||||
GET_VEGA_WALLET_URL,
|
||||
Networks,
|
||||
useDocsLink,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface Props {
|
||||
@@ -46,8 +52,9 @@ export const RiskNoticeDialog = ({ onClose, network }: Props) => {
|
||||
};
|
||||
|
||||
const TestnetContent = ({ network }: { network: Networks }) => {
|
||||
const { VEGA_DOCS_URL, GITHUB_FEEDBACK_URL, VEGA_WALLET_URL } =
|
||||
useEnvironment();
|
||||
const { GITHUB_FEEDBACK_URL } = useEnvironment();
|
||||
const docsLink = useDocsLink();
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4">
|
||||
@@ -62,17 +69,17 @@ const TestnetContent = ({ network }: { network: Networks }) => {
|
||||
[network]
|
||||
)}
|
||||
</p>
|
||||
{VEGA_DOCS_URL && GITHUB_FEEDBACK_URL && VEGA_WALLET_URL && (
|
||||
{GITHUB_FEEDBACK_URL && GET_VEGA_WALLET_URL && docsLink && (
|
||||
<ul className="list-disc pl-4">
|
||||
<li className="mb-1">
|
||||
<Link href={VEGA_WALLET_URL} target="_blank">
|
||||
<span className="underline">{t('Learn about Vega wallet')}</span>{' '}
|
||||
<Link href={GET_VEGA_WALLET_URL} target="_blank">
|
||||
<span className="underline">{t('Get a Vega Wallet')}</span>{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-1">
|
||||
<Link href={VEGA_DOCS_URL} target="_blank">
|
||||
<span className="underline">{t('View documentation')}</span>{' '}
|
||||
<Link href={docsLink(DOCS_VEGA_WALLET)} target="_blank">
|
||||
<span className="underline">{t('Learn about Vega Wallet')}</span>{' '}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -21,9 +21,10 @@ import {
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
useInitializeEnv,
|
||||
useNodeSwitcherStore,
|
||||
} from '@vegaprotocol/environment';
|
||||
import './styles.css';
|
||||
import { useGlobalStore, usePageTitleStore } from '../stores';
|
||||
import { usePageTitleStore } from '../stores';
|
||||
import { Footer } from '../components/footer';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
@@ -80,7 +81,7 @@ function AppBody({ Component }: AppProps) {
|
||||
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[repeat(3,min-content),1fr]'
|
||||
'grid-rows-[repeat(3,min-content),minmax(0,1fr)]'
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -92,7 +93,7 @@ function AppBody({ Component }: AppProps) {
|
||||
<Title />
|
||||
<div className={gridClasses}>
|
||||
<AnnouncementBanner />
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
|
||||
<div data-testid="banners">
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
@@ -115,11 +116,10 @@ function AppBody({ Component }: AppProps) {
|
||||
|
||||
function VegaTradingApp(props: AppProps) {
|
||||
const status = useEnvironment((store) => store.status);
|
||||
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({
|
||||
nodeSwitcherOpen: store.nodeSwitcherDialog,
|
||||
setNodeSwitcher: (open: boolean) =>
|
||||
store.update({ nodeSwitcherDialog: open }),
|
||||
}));
|
||||
const [nodeSwitcherOpen, setNodeSwitcher] = useNodeSwitcherStore((store) => [
|
||||
store.dialogOpen,
|
||||
store.setDialogOpen,
|
||||
]);
|
||||
|
||||
useInitializeEnv();
|
||||
|
||||
|
||||
@@ -127,3 +127,24 @@ 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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { create } from 'zustand';
|
||||
import produce from 'immer';
|
||||
|
||||
interface GlobalStore {
|
||||
nodeSwitcherDialog: boolean;
|
||||
marketId: string | null;
|
||||
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
|
||||
shouldDisplayWelcomeDialog: boolean;
|
||||
@@ -15,7 +14,6 @@ interface PageTitleStore {
|
||||
}
|
||||
|
||||
export const useGlobalStore = create<GlobalStore>()((set) => ({
|
||||
nodeSwitcherDialog: false,
|
||||
marketId: LocalStorage.getItem('marketId') || null,
|
||||
shouldDisplayWelcomeDialog: false,
|
||||
update: (newState) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user