Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc758e90c8 | ||
|
|
a70a7bcc8e | ||
|
|
4461a6a497 | ||
|
|
1041864ad8 | ||
|
|
015e0188ec | ||
|
|
228236c152 | ||
|
|
cd6e906dc6 | ||
|
|
8a47a92dbd | ||
|
|
af14c27df9 | ||
|
|
38edcd88c0 | ||
|
|
a74cf02030 | ||
|
|
6686755763 | ||
|
|
79cf8163be | ||
|
|
1688e26f4b | ||
|
|
5e101a8eaf | ||
|
|
65071f57d0 | ||
|
|
b87c981a90 | ||
|
|
b509e5b347 | ||
|
|
bb9fce4831 | ||
|
|
918825aca8 | ||
|
|
74bf183fad | ||
|
|
674baa1ee3 | ||
|
|
d18a0c7e15 | ||
|
|
76a6005b77 | ||
|
|
79414614c2 | ||
|
|
fde49e5446 | ||
|
|
45283161aa | ||
|
|
375d447fa8 | ||
|
|
1dd97a2bce | ||
|
|
d4a1a9b193 | ||
|
|
376c5241a8 | ||
|
|
8e6d8517cf | ||
|
|
5f9ec222c1 | ||
|
|
8954c41c0a | ||
|
|
4414ab4f47 | ||
|
|
d1d4bacc68 | ||
|
|
f476688c7f | ||
|
|
8845222700 | ||
|
|
e64c464091 | ||
|
|
b2c2d0d7d6 | ||
|
|
1fb61d313c | ||
|
|
ccf5ff632c | ||
|
|
54d6aaf56f | ||
|
|
736b262947 | ||
|
|
6a07127185 | ||
|
|
45f0ba7c0e | ||
|
|
6f4ea68778 | ||
|
|
90dda20891 | ||
|
|
278eb01c2f | ||
|
|
c3157de146 | ||
|
|
d28db485c5 | ||
|
|
caa0076396 | ||
|
|
451da40ed2 | ||
|
|
47694e7b72 | ||
|
|
e01fb7f9ae | ||
|
|
ffcdfb6a6a | ||
|
|
86f0d013bb | ||
|
|
c12ac45890 | ||
|
|
47ce68455a | ||
|
|
c1675e4b49 | ||
|
|
f8c4d39b93 | ||
|
|
e55cf5ecc6 | ||
|
|
406cf22810 | ||
|
|
8ff46bab5b | ||
|
|
5f177785b6 | ||
|
|
06779f36ca | ||
|
|
29991015b5 | ||
|
|
071a6ff8bb | ||
|
|
5b8df4c414 | ||
|
|
ce6873fe54 | ||
|
|
d0e64364e3 | ||
|
|
737ffb4c35 | ||
|
|
53048ac8ef | ||
|
|
05ca1a09b9 | ||
|
|
94e398dd1c | ||
|
|
7c0a4f61e9 | ||
|
|
056d3215e8 | ||
|
|
a68b2093e4 | ||
|
|
f440f57be2 |
+2
-1
@@ -51,7 +51,8 @@
|
||||
"ul": ["list"],
|
||||
"ol": ["list"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"no-console": ["error", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Feature Epic
|
||||
about: A template to capture and scope user requirements, high level process, and basic mockups for an upcoming feature as part of the initial core spec review process.
|
||||
title: 'Epic: '
|
||||
labels: feature-epic
|
||||
---
|
||||
|
||||
## Core Feature
|
||||
|
||||
<Name>
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Define high level requirements
|
||||
- [ ] Create basic mockups
|
||||
- [ ] Update "API Requirements" in core spec
|
||||
- [ ] Update "User-Interface Spec" in relevant front end repo
|
||||
- [ ] Create detailed user stories using normal template
|
||||
|
||||
## High Level Requirements
|
||||
|
||||
## Basic Mockups
|
||||
|
||||
## Link to API Requirements in Core spec
|
||||
|
||||
## Link to User Interface Specs
|
||||
|
||||
## Linked User Stories
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
jobs:
|
||||
after-release:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -30,18 +30,20 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Wait for publish to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.1
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
check-name: '(CD) publish dist / trading'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
- name: resolve ipfs hashes for release
|
||||
run: |
|
||||
echo "Name: ${{ github.event.release.name }}"
|
||||
echo "Description: ${{ github.event.release.body }}"
|
||||
echo "Tag: ${{ github.event.release.tag_name }}"
|
||||
commit="$(git rev-list -n 1 ${{ github.event.release.tag_name }})"
|
||||
echo "Commit: $commit"
|
||||
until docker pull vegaprotocol/trading:$commit; do
|
||||
echo "Image not pushed yet, waiting 60 seconds"
|
||||
sleep 60
|
||||
done
|
||||
docker run --rm vegaprotocol/trading:$commit cat /ipfs-hash > ipfs-hash
|
||||
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
|
||||
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
|
||||
tar -xzf kubo.tgz
|
||||
export PATH="$PATH:$PWD/kubo"
|
||||
|
||||
@@ -6,7 +6,7 @@ name: 'Add Issues To Project Board'
|
||||
types:
|
||||
- opened
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }}
|
||||
GH_TOKEN: ${{ secrets.PROJECT_MANAGE_ACTION }}
|
||||
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
USER: ${{ github.actor }}
|
||||
|
||||
@@ -81,6 +81,22 @@ jobs:
|
||||
with:
|
||||
main-branch-name: develop
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
run: |
|
||||
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 )"
|
||||
python3 tools/ci/check-affected.py --github-ref="${{ github.ref }}" --branch-slug="$branch_slug" --event-name="${{ github.event_name }}"
|
||||
|
||||
- name: Verify script result
|
||||
run: |
|
||||
echo "Check outputs from script"
|
||||
echo "projects: ${{ env.PROJECTS }}"
|
||||
echo "projects-e2e: ${{ env.PROJECTS_E2E }}"
|
||||
echo "preview_governance: ${{ env.PREVIEW_GOVERNANCE }}"
|
||||
echo "preview_trading: ${{ env.PREVIEW_TRADING }}"
|
||||
echo "preview_explorer: ${{ env.PREVIEW_EXPLORER }}"
|
||||
echo "preview_tools: ${{ env.PREVIEW_TOOLS }}"
|
||||
|
||||
- name: Check formatting
|
||||
run: yarn nx format:check
|
||||
|
||||
@@ -96,126 +112,6 @@ jobs:
|
||||
- name: Build affected
|
||||
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
|
||||
|
||||
# See affected apps
|
||||
- name: See affected apps
|
||||
run: |
|
||||
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
|
||||
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 )"
|
||||
|
||||
echo ">>>> debug"
|
||||
echo "NX_BASE: ${{ env.NX_BASE }}"
|
||||
echo "NX_HEAD: ${{ env.NX_HEAD }}"
|
||||
echo "Affected: ${affected}"
|
||||
echo "Branch slug: ${branch_slug}"
|
||||
echo "Current ref: ${{ github.ref }}"
|
||||
echo ">>>> eof debug"
|
||||
|
||||
projects_array=()
|
||||
|
||||
preview_governance="not deployed"
|
||||
preview_trading="not deployed"
|
||||
preview_explorer="not deployed"
|
||||
preview_tools="not deployed"
|
||||
|
||||
# parse if affected is any of three main applications, if none - use all of them
|
||||
if echo "$affected" | grep -q governance; then
|
||||
echo "Governance is affected"
|
||||
projects_array+=("governance")
|
||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||
fi
|
||||
if echo "$affected" | grep -q trading; then
|
||||
echo "Trading is affected"
|
||||
projects_array+=("trading")
|
||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||
fi
|
||||
if echo "$affected" | grep -q explorer; then
|
||||
echo "Explorer is affected"
|
||||
projects_array+=("explorer")
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
if [[ ${#projects_array[@]} -eq 0 ]]; then
|
||||
projects_array=("governance" "trading" "explorer")
|
||||
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
|
||||
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
|
||||
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
|
||||
fi
|
||||
|
||||
# applications parsed before this loop are applicable for running e2e-tests
|
||||
projects_e2e_array=()
|
||||
for project in "${projects_array[@]}"; do
|
||||
projects_e2e_array+=("${project}-e2e")
|
||||
done
|
||||
# all applications below this loop are not applicable for running e2e-test
|
||||
|
||||
# check if pull request event to deploy tools
|
||||
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
echo "Deploying tools on preview"
|
||||
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
|
||||
|
||||
projects_array+=("multisig-signer")
|
||||
fi
|
||||
# those apps deploy only from develop to mainnet
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
if echo "$affected" | grep -q multisig-signer; then
|
||||
echo "Tools are affected"
|
||||
echo "Deploying tools on s3"
|
||||
|
||||
projects_array+=("multisig-signer")
|
||||
fi
|
||||
if echo "$affected" | grep -q static; then
|
||||
echo "static is affected"
|
||||
echo "Deploying static on s3"
|
||||
|
||||
projects_array+=("static")
|
||||
fi
|
||||
if echo "$affected" | grep -q ui-toolkit; then
|
||||
echo "ui-toolkit is affected"
|
||||
echo "Deploying ui-toolkit on s3"
|
||||
|
||||
projects_array+=("ui-toolkit")
|
||||
fi
|
||||
fi
|
||||
|
||||
# if branch starts with release/ and ends with trading / governance or explorer - overwrite the array of affected projects with fixed single application
|
||||
if [[ "${{ github.ref }}" == *release* ]]; then
|
||||
echo ">> This is a relase branch"
|
||||
case "${{ github.ref }}" in
|
||||
*trading)
|
||||
echo ">> Only trading will be deployed"
|
||||
projects_array=(trading)
|
||||
projects_e2e_array=(trading)
|
||||
;;
|
||||
*governance)
|
||||
echo ">> Only governance will be deployed"
|
||||
projects_array=(governance)
|
||||
projects_e2e_array=(governance)
|
||||
;;
|
||||
*explorer)
|
||||
echo ">> Only explorer will be deployed"
|
||||
projects_array=(explorer)
|
||||
projects_e2e_array=(explorer)
|
||||
;;
|
||||
*)
|
||||
echo ">> All apps will be deployed"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "Projects: ${projects_array[@]}"
|
||||
echo "Projects E2E: ${projects_e2e_array[@]}"
|
||||
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
|
||||
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
|
||||
|
||||
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
|
||||
echo PROJECTS=$projects_json >> $GITHUB_ENV
|
||||
|
||||
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
|
||||
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
|
||||
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
|
||||
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
projects-e2e: ${{ env.PROJECTS_E2E }}
|
||||
@@ -224,6 +120,15 @@ jobs:
|
||||
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
|
||||
preview_tools: ${{ env.PREVIEW_TOOLS }}
|
||||
|
||||
console-e2e:
|
||||
needs: lint-test-build
|
||||
name: '(CI) console python'
|
||||
uses: ./.github/workflows/console-test-run.yml
|
||||
secrets: inherit
|
||||
if: ${{ contains(fromJSON(needs.lint-test-build.outputs.projects), 'trading') && github.event_name == 'pull_request' }}
|
||||
with:
|
||||
github-sha: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
||||
cypress:
|
||||
needs: lint-test-build
|
||||
name: '(CI) cypress'
|
||||
|
||||
@@ -1,55 +1,138 @@
|
||||
name: console-test-run
|
||||
name: (CI) Console tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
github-sha:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
console-test:
|
||||
timeout-minutes: 5
|
||||
runs-on: self-hosted-runner
|
||||
run-tests:
|
||||
name: run-tests
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
#----------------------------------------------
|
||||
# check-out frontend-monorepo
|
||||
#----------------------------------------------
|
||||
- name: Checkout console test repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ inputs.github-sha }}
|
||||
#----------------------------------------------
|
||||
# cache node modules
|
||||
#----------------------------------------------
|
||||
- name: Cache node modules
|
||||
id: cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
|
||||
# comment out "restore-keys" if you need to rebuild yarn from 0
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cache-node-modules-
|
||||
#----------------------------------------------
|
||||
# setup node
|
||||
#----------------------------------------------
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
|
||||
cache: yarn
|
||||
#----------------------------------------------
|
||||
# install deps if cache missing
|
||||
#----------------------------------------------
|
||||
- name: yarn install
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --pure-lockfile
|
||||
#----------------------------------------------
|
||||
# build trading
|
||||
#----------------------------------------------
|
||||
- name: Build affected spec
|
||||
run: |
|
||||
yarn env-cmd -f ./apps/trading/.env.stagnet1 yarn nx export trading
|
||||
#----------------------------------------------
|
||||
# run trading server
|
||||
#----------------------------------------------
|
||||
- name: Run trading server
|
||||
run: |
|
||||
docker run -d -p 80:4200 -v $PWD/docker/nginx.conf:/etc/nginx/conf.d/default.conf -v $PWD/dist/apps/trading/exported:/usr/share/nginx/html nginx:1.23-alpine@sha256:6318314189b40e73145a48060bff4783a116c34cc7241532d0d94198fb2c9629
|
||||
sleep 5
|
||||
docker ps
|
||||
#----------------------------------------------
|
||||
# check if container persists between runs
|
||||
#----------------------------------------------
|
||||
- name: Check server
|
||||
run: |
|
||||
docker ps
|
||||
#----------------------------------------------
|
||||
# check-out tests repo
|
||||
#----------------------------------------------
|
||||
- name: Checkout console test repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: vegaprotocol/console-test
|
||||
path: './console-test'
|
||||
|
||||
- name: Set up Python
|
||||
#----------------------------------------------
|
||||
# set-up python
|
||||
#----------------------------------------------
|
||||
- name: Set up python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10.11'
|
||||
|
||||
#----------------------------------------------
|
||||
# ----- install & configure poetry -----
|
||||
#----------------------------------------------
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
virtualenvs-path: console-test/.venv
|
||||
#----------------------------------------------
|
||||
# load cached venv if cache exists
|
||||
#----------------------------------------------
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: console-test/.venv
|
||||
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
#----------------------------------------------
|
||||
# install dependencies if cache does not exist
|
||||
#----------------------------------------------
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --no-root
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: load Binaries
|
||||
run: |
|
||||
poetry run python -m vega_sim.tools.load_binaries
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
run: poetry install --no-interaction --no-root
|
||||
#----------------------------------------------
|
||||
# install vega binaries
|
||||
#----------------------------------------------
|
||||
- name: Install vega binaries
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: pull console
|
||||
run: |
|
||||
poetry run docker pull ghcr.io/vegaprotocol/frontend/trading:${{ inputs.github-sha }}
|
||||
|
||||
- name: Update container_name in config.py
|
||||
run: |
|
||||
sed -i "s/container_name = \".*\"/container_name = \"vegaprotocol\/frontend\/trading:${{ inputs.github-sha }}\"/g" config.py
|
||||
|
||||
run: poetry run python -m vega_sim.tools.load_binaries --force
|
||||
#----------------------------------------------
|
||||
# install playwright
|
||||
#----------------------------------------------
|
||||
- name: install playwright
|
||||
run: poetry run playwright install
|
||||
working-directory: ./console-test
|
||||
|
||||
- name: run tests
|
||||
run: poetry run pytest --numprocesses auto
|
||||
#----------------------------------------------
|
||||
# run tests
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
working-directory: ./console-test
|
||||
|
||||
run: poetry run pytest --numprocesses auto
|
||||
- name: Check files
|
||||
run: |
|
||||
ls -al .
|
||||
ls -al console-test
|
||||
#----------------------------------------------
|
||||
# upload traces
|
||||
#----------------------------------------------
|
||||
- name: Upload Playwright Trace
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
|
||||
@@ -28,6 +28,13 @@ jobs:
|
||||
echo IS_MAINNET_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_TESTNET_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_IPFS_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_S3_RELEASE=false >> $GITHUB_ENV
|
||||
echo IS_DEV_IMAGE=false >> $GITHUB_ENV
|
||||
|
||||
- name: Is dev image
|
||||
if: ${{ contains(github.ref, 'develop') && github.event_name == 'push' && matrix.app == 'trading' }}
|
||||
run: |
|
||||
echo IS_DEV_IMAGE=true >> $GITHUB_ENV
|
||||
|
||||
- name: Is PR
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
@@ -35,7 +42,7 @@ jobs:
|
||||
echo IS_PR=true >> $GITHUB_ENV
|
||||
|
||||
- name: Is mainnet release
|
||||
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
|
||||
if: ${{ contains(github.ref, 'release/mainnet') && !contains(github.ref, 'mirror') }}
|
||||
run: |
|
||||
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
|
||||
|
||||
@@ -49,6 +56,11 @@ jobs:
|
||||
run: |
|
||||
echo IS_IPFS_RELEASE=true >> $GITHUB_ENV
|
||||
|
||||
- name: Is S3 Release
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'false' && github.event_name == 'push' }}
|
||||
run: |
|
||||
echo IS_S3_RELEASE=true >> $GITHUB_ENV
|
||||
|
||||
- name: Set up QEMU
|
||||
id: quemu
|
||||
uses: docker/setup-qemu-action@v2
|
||||
@@ -69,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Log in to the Container registry (docker hub)
|
||||
uses: docker/login-action@v2
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -91,52 +103,13 @@ jobs:
|
||||
- name: Define dist variables
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
envName=''
|
||||
domain="vega.rocks"
|
||||
bucketName=''
|
||||
python3 tools/ci/define-dist-variables.py --github-ref="${{ github.ref }}" --app="${{ matrix.app }}"
|
||||
|
||||
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
|
||||
# remove prefixing release/ and take the first string limited by - which is supposed to be name of the environment for releasing (format: release/testnet-trading)
|
||||
envName="$(echo ${{ github.ref }} | sed -e "s|refs/heads/release/||" | cut -d '-' -f 1 )"
|
||||
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
|
||||
envName="stagnet1"
|
||||
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="tools.vega.xyz"
|
||||
fi
|
||||
if [[ "${{ matrix.app }}" = "static" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="static.vega.xyz"
|
||||
fi
|
||||
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
|
||||
envName="mainnet"
|
||||
bucketName="ui.vega.rocks"
|
||||
fi
|
||||
elif [[ "${{ github.ref }}" =~ .*mainnet$ ]]; then
|
||||
envName="mainnet"
|
||||
fi
|
||||
|
||||
if [[ "${envName}" = "mainnet" ]]; then
|
||||
domain="vega.xyz"
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
fi
|
||||
elif [[ "${envName}" = "testnet" ]]; then
|
||||
domain="fairground.wtf"
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
bucketName="${{ matrix.app }}.${domain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "${bucketName}" ]]; then
|
||||
bucketName="${{ matrix.app }}.${envName}.${domain}"
|
||||
fi
|
||||
|
||||
echo "bucket name: ${bucketName}"
|
||||
echo "env name: ${envName}"
|
||||
|
||||
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
|
||||
echo ENV_NAME=${envName} >> $GITHUB_ENV
|
||||
- name: Verify script result
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: |
|
||||
echo "BUCKET_NAME=${{ env.BUCKET_NAME }}"
|
||||
echo "ENV_NAME=${{ env.ENV_NAME }}"
|
||||
|
||||
- name: Build local dist
|
||||
run: |
|
||||
@@ -151,8 +124,12 @@ jobs:
|
||||
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
|
||||
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
|
||||
DIST_LOCATION=dist/storybook/ui-toolkit
|
||||
elif [ "${{ matrix.app }}" = "static" ]; then
|
||||
yarn nx build static || (yarn install && yarn nx build static)
|
||||
else
|
||||
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }})
|
||||
fi
|
||||
if [[ -z "$DIST_LOCATION" ]]; then
|
||||
DIST_LOCATION=dist/apps/${{ matrix.app }}
|
||||
fi
|
||||
mv $DIST_LOCATION dist-result
|
||||
@@ -202,7 +179,7 @@ jobs:
|
||||
uses: docker/build-push-action@v3
|
||||
continue-on-error: true
|
||||
id: dockerhub-push
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'true' || env.IS_DEV_IMAGE == 'true' }}
|
||||
with:
|
||||
context: .
|
||||
file: docker/node-outside-docker.Dockerfile
|
||||
@@ -212,7 +189,7 @@ jobs:
|
||||
ENV_NAME=${{ env.ENV_NAME }}
|
||||
tags: |
|
||||
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
|
||||
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
|
||||
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || env.IS_DEV_IMAGE == 'true' && 'develop' || '' }}
|
||||
|
||||
- name: Publish dist as docker image (ghcr - retry)
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -245,7 +222,7 @@ jobs:
|
||||
- name: Publish dist to s3
|
||||
uses: jakejarvis/s3-sync-action@master
|
||||
# s3 releases are not happening for trading on mainnet - it's IPFS
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'false' }}
|
||||
if: ${{ env.IS_S3_RELEASE == 'true' }}
|
||||
with:
|
||||
args: --acl private --follow-symlinks --delete
|
||||
env:
|
||||
@@ -256,11 +233,11 @@ jobs:
|
||||
SOURCE_DIR: 'dist-result'
|
||||
|
||||
- name: Install aws CLI
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'false' }}
|
||||
if: ${{ env.IS_S3_RELEASE == 'true' }}
|
||||
uses: unfor19/install-aws-cli-action@master
|
||||
|
||||
- name: Perform cache invalidation
|
||||
if: ${{ env.IS_IPFS_RELEASE == 'false' }}
|
||||
if: ${{ env.IS_S3_RELEASE == 'true' }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
@@ -16,5 +16,10 @@ NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
CYPRESS_VEGA_URL=http://localhost:3008/graphql
|
||||
CYPRESS_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit supply green clown session
|
||||
CYPRESS_ETHEREUM_PROVIDER_URL=http://localhost:8545
|
||||
CYPRESS_FAUCET_URL=http://localhost:1790/api/v1/mint
|
||||
CYPRESS_VEGA_PUBLIC_KEY=02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65
|
||||
CYPRESS_VEGA_WALLET_URL=http://localhost:1789
|
||||
CYPRESS_VEGA_WALLET_API_TOKEN=
|
||||
|
||||
@@ -1,19 +1,130 @@
|
||||
//Tests set to skip until market bug for capsule checkpoint is fixed
|
||||
context.skip('Market page', { tags: '@regression' }, function () {
|
||||
context('Market page', { tags: '@regression' }, function () {
|
||||
describe('Verify elements on page', function () {
|
||||
const marketHeaders = '[data-testid="markets-header"]';
|
||||
const marketNavigation = 'a[href="/markets"]';
|
||||
const marketHeaders = 'markets-heading';
|
||||
const createdMarketId =
|
||||
'2eab0e66545a789047561bc5a2e5cbc3b19eb708da41104e3cac2474ee36c4d4';
|
||||
|
||||
it('Markets page is displayed', function () {
|
||||
before('Create market', function () {
|
||||
cy.visit('/');
|
||||
cy.get(marketNavigation).click();
|
||||
cy.common_validate_blocks_data_displayed(marketHeaders);
|
||||
cy.createMarket();
|
||||
});
|
||||
|
||||
it('Markets page displayed on mobile', function () {
|
||||
beforeEach('Get market id', function () {
|
||||
cy.navigate_to('markets');
|
||||
cy.get('[col-id="id"]').eq(1).invoke('text').as('createdMarketId');
|
||||
});
|
||||
|
||||
it('Market displayed on market page', function () {
|
||||
cy.navigate_to('markets');
|
||||
cy.getByTestId(marketHeaders).should('be.visible');
|
||||
cy.get(`[row-id="${this.createdMarketId}"]`)
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.get_element_by_col_id('code').should('have.text', 'TEST.24h');
|
||||
cy.get_element_by_col_id('name').should('have.text', 'Test market 1');
|
||||
cy.get_element_by_col_id('state').should('have.text', 'Pending');
|
||||
cy.get_element_by_col_id('asset').should('have.text', 'fUSDC');
|
||||
cy.get_element_by_col_id('id').should(
|
||||
'have.text',
|
||||
this.createdMarketId
|
||||
);
|
||||
cy.get_element_by_col_id('actions')
|
||||
.find('a')
|
||||
.should('have.attr', 'href', `/markets/${this.createdMarketId}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('Able to go to market details page', function () {
|
||||
cy.navigate_to('markets');
|
||||
cy.get_element_by_col_id('actions').eq(1).click();
|
||||
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
|
||||
cy.validate_element_from_table('Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Market ID', this.createdMarketId);
|
||||
cy.validate_element_from_table('Trading Mode', 'Opening auction');
|
||||
cy.validate_element_from_table('Market Decimal Places', '5');
|
||||
cy.validate_element_from_table('Position Decimal Places', '5');
|
||||
cy.validate_element_from_table('Settlement Asset Decimal Places', '5');
|
||||
// Instrument
|
||||
cy.validate_element_from_table('Market Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Code', 'TEST.24h');
|
||||
cy.validate_element_from_table('Product Type', 'Future');
|
||||
cy.validate_element_from_table('Quote Name', 'fUSDC');
|
||||
// Settlement Asset
|
||||
cy.validate_element_from_table('Quote Name', 'fUSDC');
|
||||
cy.validate_element_from_table('Symbol', 'fUSDC');
|
||||
cy.validate_element_from_table('Decimals', '5');
|
||||
cy.validate_element_from_table('Quantum', '0.00');
|
||||
cy.validate_element_from_table('Status', 'Enabled');
|
||||
cy.validate_element_from_table('Max faucet amount', '10,000,000.00');
|
||||
cy.validate_element_from_table(
|
||||
'Infrastructure fee account balance',
|
||||
'0.00'
|
||||
);
|
||||
cy.validate_element_from_table(
|
||||
'Global reward pool account balance',
|
||||
'0.00'
|
||||
);
|
||||
// Metadata
|
||||
cy.validate_element_from_table('Sector', 'tech');
|
||||
cy.validate_element_from_table('Source', 'docs.vega.xyz');
|
||||
// Risk model
|
||||
cy.validate_element_from_table('Tau', '0.0001140771161');
|
||||
cy.validate_element_from_table('Risk Aversion Parameter', '0.01');
|
||||
// Risk parameters
|
||||
// cy.validate_element_from_table('R', '0.016')
|
||||
cy.validate_element_from_table('Sigma', '0.5');
|
||||
// Risk factors
|
||||
cy.validate_element_from_table('Short', '0.0143218738374871');
|
||||
cy.validate_element_from_table('Long', '0.0141450471498822');
|
||||
// Price monitoring settings 1
|
||||
cy.validate_element_from_table('Horizon Secs', '43,200');
|
||||
cy.validate_element_from_table('Probability', '1');
|
||||
cy.validate_element_from_table('Auction Extension Secs', '600');
|
||||
// Liquidity Monitoring
|
||||
cy.validate_element_from_table('Triggering Ratio', '0.7');
|
||||
cy.validate_element_from_table('Time Window', '3,600');
|
||||
cy.validate_element_from_table('Scaling Factor', '10');
|
||||
// Liquidity
|
||||
cy.validate_element_from_table('Target Stake', '0.00 fUSDC');
|
||||
cy.validate_element_from_table('Supplied Stake', '0.00 fUSDC');
|
||||
cy.validate_element_from_table('Market Value Proxy', '0.00 fUSDC');
|
||||
// Liquidity price range
|
||||
cy.validate_element_from_table(
|
||||
'Liquidity Price Range',
|
||||
'1,000.00% of mid price'
|
||||
);
|
||||
cy.validate_element_from_table('Lowest Price', '0.00 fUSDC');
|
||||
cy.validate_element_from_table('Highest Price', '0.00 fUSDC');
|
||||
|
||||
cy.getByTestId('oracle-spec-links')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
'/oracles/bf242aa5c9f64fcbb77808aa8582e73711519f4b35264eb797a80f1803590a24'
|
||||
);
|
||||
|
||||
// Able to view Json
|
||||
cy.contains('View JSON').click();
|
||||
cy.get('.language-json').should('exist');
|
||||
});
|
||||
|
||||
// Skipping due to resize observer loop limit error
|
||||
it.skip('Markets page displayed on mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.get(marketNavigation).click();
|
||||
cy.common_validate_blocks_data_displayed(marketHeaders);
|
||||
cy.navigate_to('markets', true);
|
||||
cy.getByTestId(marketHeaders).should('be.visible');
|
||||
cy.get(`[row-id="${createdMarketId}"]`)
|
||||
.should('be.visible')
|
||||
.within(() => {
|
||||
cy.get_element_by_col_id('code').should('have.text', 'TEST.24h');
|
||||
cy.get_element_by_col_id('name').should('have.text', 'Test market 1');
|
||||
cy.get_element_by_col_id('state').should('have.text', 'Pending');
|
||||
cy.get_element_by_col_id('asset').should('have.text', 'fUSDC');
|
||||
cy.get_element_by_col_id('id').should('have.text', createdMarketId);
|
||||
cy.get_element_by_col_id('actions')
|
||||
.find('a')
|
||||
.should('have.attr', 'href', `/markets/${createdMarketId}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
context('Proposal page', { tags: '@smoke' }, function () {
|
||||
describe('Verify elements on page', function () {
|
||||
const proposalHeading = 'proposals-heading';
|
||||
const dateTimeRegex =
|
||||
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
|
||||
const proposalTitle = 'Add Lorem Ipsum market';
|
||||
|
||||
before('Create market proposal', function () {
|
||||
cy.visit('/');
|
||||
cy.createMarket();
|
||||
});
|
||||
|
||||
it('Able to view proposal', function () {
|
||||
cy.navigate_to('governanceProposals');
|
||||
cy.getByTestId(proposalHeading).should('be.visible');
|
||||
// get first proposal in list
|
||||
cy.get('[row-index="0"]').within(() => {
|
||||
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
|
||||
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
|
||||
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
|
||||
cy.getByTestId('vote-progress').should('be.visible');
|
||||
cy.get('[col-id="cDate"]')
|
||||
.invoke('text')
|
||||
.should('match', dateTimeRegex);
|
||||
cy.get('[col-id="eDate"]')
|
||||
.invoke('text')
|
||||
.should('match', dateTimeRegex);
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('contains', 'https://governance.fairground.wtf/proposals/');
|
||||
cy.contains('View terms').should('exist').click();
|
||||
});
|
||||
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
|
||||
cy.get('.language-json').should('exist');
|
||||
});
|
||||
|
||||
it('Proposal page displayed on mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.navigate_to('governanceProposals', true);
|
||||
cy.getByTestId(proposalHeading).should('be.visible');
|
||||
cy.get('[row-index="0"]').within(() => {
|
||||
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,7 +40,7 @@ context.skip('Transactions page', function () {
|
||||
.first()
|
||||
.click({ force: true });
|
||||
} else {
|
||||
cy.slack('Unable to find any transactions on page');
|
||||
cy.log('Unable to find any transactions on page');
|
||||
cy.screenshot();
|
||||
}
|
||||
});
|
||||
@@ -78,7 +78,7 @@ context.skip('Transactions page', function () {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cy.slack('Unable to find any transactions on page');
|
||||
cy.log('Unable to find any transactions on page');
|
||||
cy.screenshot();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,7 +7,11 @@ context('Validator page', { tags: '@smoke' }, function () {
|
||||
it('should be able to see validator tiles', function () {
|
||||
cy.getNodes().then((nodes) => {
|
||||
nodes.forEach((node) => {
|
||||
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
||||
if (node.rankingScore.performanceScore > 0) {
|
||||
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
|
||||
} else {
|
||||
cy.get(`[validator-id="${node.id}"]`).should('not.exist');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ Cypress.Commands.add('switchToMobile', () => {
|
||||
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
|
||||
cy.viewport('iphone-x');
|
||||
cy.visit('/');
|
||||
cy.get('[data-testid="open-menu"]').click();
|
||||
cy.get('[data-testid="button-menu-drawer"]').click();
|
||||
});
|
||||
|
||||
Cypress.Commands.add('monitor_clipboard', () => {
|
||||
@@ -88,3 +88,42 @@ Cypress.Commands.add('convert_number_to_max_four_decimal', (number) => {
|
||||
maximumFractionDigits: 4,
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add('navigate_to', (page, mobileView = false) => {
|
||||
const navigation = {
|
||||
transactions: '/txs',
|
||||
blocks: '/blocks',
|
||||
oracles: '/oracles',
|
||||
validators: '/validators',
|
||||
parties: '/parties',
|
||||
assets: '/assets',
|
||||
markets: '/markets',
|
||||
governanceProposals: '/governance',
|
||||
networkParameters: '/network-parameters',
|
||||
genesisParameters: '/genesis',
|
||||
};
|
||||
|
||||
const topLevelRoutes = ['transactions', 'blocks', 'oracles', 'validators'];
|
||||
|
||||
cy.getByTestId('navigation').within(() => {
|
||||
if (!topLevelRoutes.includes(page) && mobileView === false) {
|
||||
cy.getByTestId('other').first().click();
|
||||
}
|
||||
cy.get(`[href="${navigation[page]}"]`).first().click();
|
||||
});
|
||||
cy.url().should('include', navigation[page]);
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_element_by_col_id', (elementName) => {
|
||||
cy.get(`[col-id="${elementName}"]`);
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
'validate_element_from_table',
|
||||
(tableRowName, tableRowValue) => {
|
||||
cy.contains(tableRowName)
|
||||
.parentsUntil("[data-testid='key-value-table-row']")
|
||||
.siblings()
|
||||
.should('have.text', tableRowValue);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
|
||||
NX_VEGA_ENV=MAINNET-MIRROR
|
||||
NX_VEGA_ENV=MAINNET_MIRROR
|
||||
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.rocks/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://governance.mainnet-mirror.vega.rocks
|
||||
|
||||
@@ -95,7 +95,10 @@ export const Header = () => {
|
||||
{mainItems.map(routeToNavigationItem)}
|
||||
{groupedItems && groupedItems.length > 0 && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||
<NavigationTrigger
|
||||
isActive={Boolean(isOnOther)}
|
||||
data-testid="other"
|
||||
>
|
||||
{t('Other')}
|
||||
</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
hide: window.innerWidth <= BREAKPOINT_MD,
|
||||
headerName: t('Enactment date'),
|
||||
field: 'terms.enactmentDatetime',
|
||||
valueFormatte: ({
|
||||
valueFormatter: ({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
ProposalListFieldsFragment,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export interface TxDetailsOrderIcebergDetailsProps {
|
||||
iceberg: components['schemas']['v1IcebergOpts'];
|
||||
size: components['schemas']['v1OrderSubmission']['size'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
.iceberg .sub {
|
||||
vertical-align: bottom;
|
||||
color: #954F01;
|
||||
}
|
||||
|
||||
.iceberg .sup {
|
||||
vertical-align: top;
|
||||
color: #515E1E;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Visualises the iceberg details of an order, showing the minimum visible size,
|
||||
* the full size and the peak size.
|
||||
*/
|
||||
export const TxOrderIcebergDetails = ({
|
||||
iceberg,
|
||||
size,
|
||||
}: TxDetailsOrderIcebergDetailsProps) => {
|
||||
return (
|
||||
<div
|
||||
className="inline-block iceberg bg-vega-blue-300 border-vega-blue-350 border-[1px] text-[70%] font-mono py-[2px] pl-2 pr-3 height-[1em] leading-[1em] cursor-pointer"
|
||||
style={{ clipPath: 'polygon(10% 0%, 100% 0%, 90% 100%, 0% 100%)' }}
|
||||
>
|
||||
<Tooltip description={t('Iceberg: Minimum visible size')}>
|
||||
<span className="align-bottom text-vega-orange-650">
|
||||
{iceberg.minimumVisibleSize || '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip description={t('Iceberg: Total size')}>
|
||||
<span className="text-sm text-vega-blue-600 mx-3">{size}</span>
|
||||
</Tooltip>
|
||||
<Tooltip description={t('Iceberg: Visible peak')}>
|
||||
<span className="align-top text-vega-yellow-600">
|
||||
{iceberg.peakSize || '-'}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||
import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
|
||||
import Hash from '../../links/hash';
|
||||
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
|
||||
import { TxOrderIcebergDetails } from './order/tx-order-iceberg';
|
||||
|
||||
interface TxDetailsOrderProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -32,6 +33,8 @@ export const TxDetailsOrder = ({
|
||||
const marketId = txData.command.orderSubmission.marketId || '-';
|
||||
const reference = txData.command.orderSubmission.peggedOrder;
|
||||
const side = txData.command.orderSubmission.side;
|
||||
const iceberg = txData.command.orderSubmission.icebergOpts;
|
||||
const size = txData.command.orderSubmission.size;
|
||||
|
||||
let deterministicId = '';
|
||||
|
||||
@@ -74,6 +77,14 @@ export const TxDetailsOrder = ({
|
||||
marketId={marketId}
|
||||
/>
|
||||
) : null}
|
||||
{iceberg ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Iceberg details')}</TableCell>
|
||||
<TableCell>
|
||||
<TxOrderIcebergDetails iceberg={iceberg} size={size} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
|
||||
{deterministicId.length > 0 ? (
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSubContent,
|
||||
Icon,
|
||||
Button,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { FilterLabel } from './tx-filter-label';
|
||||
@@ -100,15 +99,13 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
|
||||
<DropdownMenu
|
||||
modal={false}
|
||||
trigger={
|
||||
<DropdownMenuTrigger className="ml-0">
|
||||
<Button size="xs" data-testid="filter-trigger">
|
||||
<FilterLabel filters={filters} />
|
||||
</Button>
|
||||
<DropdownMenuTrigger>
|
||||
<FilterLabel filters={filters} />
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
<DropdownMenuContent>
|
||||
{filters.size > 0 ? null : (
|
||||
{filters.size > 1 ? null : (
|
||||
<>
|
||||
<DropdownMenuCheckboxItem
|
||||
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
|
||||
|
||||
@@ -44,7 +44,7 @@ export const TxsListNavigation = ({
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
disabled={!hasMoreTxs || loading}
|
||||
disabled={!hasMoreTxs}
|
||||
onClick={() => {
|
||||
nextPage();
|
||||
}}
|
||||
|
||||
@@ -166,6 +166,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
'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
|
||||
// Note that colours are currently arbitrary
|
||||
if (type === 'Chain Event' && !!command?.chainEvent) {
|
||||
type = getLabelForChainEvent(command.chainEvent);
|
||||
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
|
||||
@@ -177,6 +178,9 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
type = getLabelForProposal(command.proposalSubmission);
|
||||
}
|
||||
colours = 'text-black bg-vega-yellow';
|
||||
} else if (type === 'Order' && command) {
|
||||
type = getLabelForOrderType(orderType, command);
|
||||
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
|
||||
}
|
||||
|
||||
if (type === 'Vote on Proposal') {
|
||||
|
||||
@@ -86,14 +86,18 @@ describe('Txs infinite list item', () => {
|
||||
render(
|
||||
<MockedProvider>
|
||||
<MemoryRouter>
|
||||
<TxsInfiniteListItem
|
||||
type="testType"
|
||||
submitter="testPubKey"
|
||||
hash="testTxHash"
|
||||
block="1"
|
||||
code={0}
|
||||
command={{}}
|
||||
/>
|
||||
<table>
|
||||
<tbody>
|
||||
<TxsInfiniteListItem
|
||||
type="testType"
|
||||
submitter="testPubKey"
|
||||
hash="testTxHash"
|
||||
block="1"
|
||||
code={0}
|
||||
command={{}}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,9 @@ const DEFAULT_TRUNCATE_LENGTH = 7;
|
||||
|
||||
export function getIdTruncateLength(screen: Screen): number {
|
||||
if (['xxxl', 'xxl'].includes(screen)) {
|
||||
return 64;
|
||||
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||
return 32;
|
||||
} else if (['xl', 'lg', 'md'].includes(screen)) {
|
||||
return 16;
|
||||
}
|
||||
return DEFAULT_TRUNCATE_LENGTH;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,17 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
|
||||
import { Side } from '@vegaprotocol/types';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
const generateHash = (): string =>
|
||||
Array.from(
|
||||
{ length: 64 },
|
||||
() => '0123456789ABCDEF'[Math.floor(Math.random() * 16)]
|
||||
).join('');
|
||||
|
||||
const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
|
||||
return Array.from(Array(number)).map((_) => ({
|
||||
block: '87901',
|
||||
index: 2,
|
||||
hash: '0F8B98DA0923A50786B852D9CA11E051CACC4C733E1DB93D535C7D81DBD10F6F',
|
||||
hash: generateHash(),
|
||||
submitter:
|
||||
'4b782482f587d291e8614219eb9a5ee9280fa2c58982dee71d976782a9be1964',
|
||||
type: 'Submit Order',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Table, TableRow } from '../table';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
|
||||
import { getTxsDataUrl } from '../../hooks/use-txs-data';
|
||||
import { getTxsDataUrl } from '../../hooks/get-txs-data-url';
|
||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import EmptyList from '../empty-list/empty-list';
|
||||
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||
@@ -14,7 +14,7 @@ interface TxsPerBlockProps {
|
||||
|
||||
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
|
||||
const filters = `filters[block.height]=${blockHeight}`;
|
||||
const url = getTxsDataUrl({ limit: txCount.toString(), filters });
|
||||
const url = getTxsDataUrl({ filters, count: txCount });
|
||||
const {
|
||||
state: { data, loading, error },
|
||||
} = useFetch<BlockExplorerTransactions>(url);
|
||||
|
||||
@@ -5,8 +5,6 @@ const windowOrDefault = (key: string) => {
|
||||
return (process.env[key] as string) || '';
|
||||
};
|
||||
|
||||
const truthy = ['1', 'true'];
|
||||
|
||||
export const ENV = {
|
||||
// Data sources
|
||||
// Environment
|
||||
@@ -20,19 +18,6 @@ export const ENV = {
|
||||
governanceUrl: windowOrDefault('NX_VEGA_GOVERNANCE_URL'),
|
||||
vegaRepoUrl: windowOrDefault('NX_VEGA_REPO_URL'),
|
||||
},
|
||||
flags: {
|
||||
assets: truthy.includes(windowOrDefault('NX_EXPLORER_ASSETS')),
|
||||
genesis: truthy.includes(windowOrDefault('NX_EXPLORER_GENESIS')),
|
||||
governance: truthy.includes(windowOrDefault('NX_EXPLORER_GOVERNANCE')),
|
||||
markets: truthy.includes(windowOrDefault('NX_EXPLORER_MARKETS')),
|
||||
oracles: truthy.includes(windowOrDefault('NX_EXPLORER_ORACLES')),
|
||||
txsList: truthy.includes(windowOrDefault('NX_EXPLORER_TXS_LIST')),
|
||||
networkParameters: truthy.includes(
|
||||
windowOrDefault('NX_EXPLORER_NETWORK_PARAMETERS')
|
||||
),
|
||||
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
|
||||
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
|
||||
},
|
||||
addresses: {
|
||||
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
|
||||
},
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ENV } from './env';
|
||||
|
||||
export default {
|
||||
...ENV.flags,
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DATA_SOURCES } from '../config';
|
||||
|
||||
type IGetTxsDataFirstPage = {
|
||||
baseUrl?: string;
|
||||
count?: number;
|
||||
party?: string;
|
||||
filters?: string;
|
||||
};
|
||||
|
||||
interface IGetTxsDataPrevious extends IGetTxsDataFirstPage {
|
||||
before: string;
|
||||
}
|
||||
|
||||
interface IGetTxsDataNext extends IGetTxsDataFirstPage {
|
||||
after: string;
|
||||
}
|
||||
|
||||
type IGetTxsDataUrl =
|
||||
| IGetTxsDataPrevious
|
||||
| IGetTxsDataNext
|
||||
| IGetTxsDataFirstPage;
|
||||
|
||||
export const BE_TXS_PER_REQUEST = 25;
|
||||
|
||||
/**
|
||||
* Properly encodes the filters and parameters for a request to the block explorer
|
||||
* API for transactions. As the API uses a slightly less common format for encoding
|
||||
* filters, some of it is more manual than you might expect.
|
||||
*
|
||||
* @param params An object containing the pagination and filters
|
||||
* @returns string URL to call
|
||||
*/
|
||||
export const getTxsDataUrl = (params: IGetTxsDataUrl) => {
|
||||
const baseUrl =
|
||||
params.baseUrl || `${DATA_SOURCES.blockExplorerUrl}/transactions`;
|
||||
const url = new URL(baseUrl);
|
||||
const count = `${params.count || BE_TXS_PER_REQUEST}`;
|
||||
|
||||
if ('before' in params && params.before?.length > 0) {
|
||||
url.searchParams.append('last', count);
|
||||
url.searchParams.append('before', params.before);
|
||||
} else if ('after' in params && params.after?.length > 0) {
|
||||
url.searchParams.append('first', count);
|
||||
url.searchParams.append('after', params.after);
|
||||
} else {
|
||||
url.searchParams.append('first', count);
|
||||
}
|
||||
|
||||
// Hacky fix for param as array
|
||||
let urlAsString = url.toString();
|
||||
if (params.filters && params.filters?.length > 0) {
|
||||
urlAsString += '&' + params.filters.replaceAll(' ', '%20');
|
||||
}
|
||||
if (params.party && params.party?.length > 0) {
|
||||
urlAsString += `&filters[tx.submitter]=${params.party}`;
|
||||
}
|
||||
|
||||
return urlAsString;
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { getTxsDataUrl } from './get-txs-data-url'; // import the function to be tested
|
||||
|
||||
describe('getTxsDataUrl', () => {
|
||||
it('should return the correct URL without filters and party', () => {
|
||||
const params = {
|
||||
count: 10,
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?first=10';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with "before" in params', () => {
|
||||
const params = {
|
||||
count: 5,
|
||||
before: '100.1',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?last=5&before=100.1';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with "after" in params', () => {
|
||||
const params = {
|
||||
count: 5,
|
||||
after: '222.1',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl = 'https://example.com/transactions?first=5&after=222.1';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return the correct URL with filters and party', () => {
|
||||
const params = {
|
||||
count: 10,
|
||||
filters: 'filters[cmd.type]=Made Up Transaction',
|
||||
party: '1234',
|
||||
baseUrl: 'https://example.com/transactions',
|
||||
};
|
||||
const expectedUrl =
|
||||
'https://example.com/transactions?first=10&filters[cmd.type]=Made%20Up%20Transaction&filters[tx.submitter]=1234';
|
||||
|
||||
expect(getTxsDataUrl(params)).toEqual(expectedUrl);
|
||||
});
|
||||
});
|
||||
@@ -1,130 +1,141 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import type { URLSearchParamsInit } from 'react-router-dom';
|
||||
import { useCallback } from 'react';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
BlockExplorerTransactionResult,
|
||||
BlockExplorerTransactions,
|
||||
} from '../routes/types/block-explorer-response';
|
||||
import { DATA_SOURCES } from '../config';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import { AllFilterOptions } from '../components/txs/tx-filter';
|
||||
import type { FilterOption } from '../components/txs/tx-filter';
|
||||
import { BE_TXS_PER_REQUEST, getTxsDataUrl } from './get-txs-data-url';
|
||||
|
||||
export interface TxsStateProps {
|
||||
txsData: BlockExplorerTransactionResult[];
|
||||
hasMoreTxs: boolean;
|
||||
cursor: string;
|
||||
previousCursors: string[];
|
||||
hasPreviousPage: boolean;
|
||||
export function getTypeFilters(filters?: Set<FilterOption>) {
|
||||
if (!filters) {
|
||||
return '';
|
||||
} else if (filters.size > 1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const forcedSingleFilter = Array.from(filters)[0];
|
||||
return `filters[cmd.type]=${forcedSingleFilter}`;
|
||||
}
|
||||
|
||||
export interface IUseTxsData {
|
||||
limit: number;
|
||||
filters?: string;
|
||||
count?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
party?: string;
|
||||
filters?: Set<FilterOption>;
|
||||
}
|
||||
|
||||
interface IGetTxsDataUrl {
|
||||
limit: string;
|
||||
filters?: string;
|
||||
}
|
||||
export const useTxsData = ({
|
||||
count = 25,
|
||||
before,
|
||||
after,
|
||||
filters,
|
||||
party,
|
||||
}: IUseTxsData) => {
|
||||
const [, setSearchParams] = useSearchParams();
|
||||
let hasMoreTxs = true;
|
||||
let txsData: BlockExplorerTransactionResult[] = [];
|
||||
|
||||
export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
|
||||
const url = new URL(`${DATA_SOURCES.blockExplorerUrl}/transactions`);
|
||||
|
||||
if (limit) {
|
||||
url.searchParams.append('limit', limit);
|
||||
}
|
||||
|
||||
// Hacky fix for param as array
|
||||
let urlAsString = url.toString();
|
||||
if (filters) {
|
||||
urlAsString += '&' + filters.replace(' ', '%20');
|
||||
}
|
||||
|
||||
return urlAsString;
|
||||
};
|
||||
|
||||
export const useTxsData = ({ limit, filters }: IUseTxsData) => {
|
||||
const [
|
||||
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage },
|
||||
setTxsState,
|
||||
] = useState<TxsStateProps>({
|
||||
txsData: [],
|
||||
hasMoreTxs: false,
|
||||
previousCursors: [],
|
||||
cursor: '',
|
||||
hasPreviousPage: false,
|
||||
const url = getTxsDataUrl({
|
||||
filters: getTypeFilters(filters),
|
||||
count,
|
||||
before,
|
||||
after,
|
||||
party,
|
||||
});
|
||||
|
||||
const url = getTxsDataUrl({ limit: limit.toString(), filters });
|
||||
|
||||
const {
|
||||
state: { data, error, loading },
|
||||
refetch,
|
||||
} = useFetch<BlockExplorerTransactions>(url, {}, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && isNumber(data.transactions.length)) {
|
||||
setTxsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
txsData: data.transactions,
|
||||
hasMoreTxs: data.transactions.length >= limit,
|
||||
cursor: data?.transactions.at(-1)?.cursor || '',
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [loading, setTxsState, data, limit]);
|
||||
if (!loading && data && isNumber(data.transactions.length)) {
|
||||
hasMoreTxs = data.transactions.length >= count;
|
||||
txsData = data.transactions;
|
||||
}
|
||||
|
||||
const nextPage = useCallback(() => {
|
||||
const c = data?.transactions.at(0)?.cursor;
|
||||
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
|
||||
|
||||
setTxsState((prev) => ({
|
||||
...prev,
|
||||
hasPreviousPage: true,
|
||||
previousCursors: newPreviousCursors,
|
||||
}));
|
||||
|
||||
return refetch({
|
||||
limit,
|
||||
before: cursor,
|
||||
});
|
||||
}, [data, previousCursors, cursor, limit, refetch]);
|
||||
const after = data?.transactions.at(-1)?.cursor || '';
|
||||
const params: URLSearchParamsInit = { after };
|
||||
if (filters) {
|
||||
params.filters = Array.from(filters).join(',');
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [filters, data, setSearchParams]);
|
||||
|
||||
const previousPage = useCallback(() => {
|
||||
const previousCursor = [...previousCursors].pop();
|
||||
const newPreviousCursors = previousCursors.slice(0, -1);
|
||||
setTxsState((prev) => ({
|
||||
...prev,
|
||||
hasPreviousPage: newPreviousCursors.length > 0,
|
||||
previousCursors: newPreviousCursors,
|
||||
}));
|
||||
return refetch({
|
||||
limit,
|
||||
before: previousCursor,
|
||||
});
|
||||
}, [previousCursors, limit, refetch]);
|
||||
const before = data?.transactions[0]?.cursor || '';
|
||||
const params: URLSearchParamsInit = { before };
|
||||
if (filters && filters.size > 0 && filters.size === 1) {
|
||||
params.filters = Array.from(filters)[0];
|
||||
}
|
||||
setSearchParams(params);
|
||||
}, [filters, data, setSearchParams]);
|
||||
|
||||
const refreshTxs = useCallback(async () => {
|
||||
setTxsState(() => ({
|
||||
txsData: [],
|
||||
cursor: '',
|
||||
previousCursors: [],
|
||||
hasMoreTxs: false,
|
||||
hasPreviousPage: false,
|
||||
}));
|
||||
const params: URLSearchParamsInit = {};
|
||||
if (filters && filters.size > 0 && filters.size === 1) {
|
||||
params.filters = Array.from(filters)[0];
|
||||
}
|
||||
setSearchParams(params);
|
||||
|
||||
refetch({ limit });
|
||||
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
refetch({ count: BE_TXS_PER_REQUEST });
|
||||
}, [setSearchParams, refetch, filters]);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
(newFilters: Set<FilterOption>) => {
|
||||
const params: URLSearchParamsInit = {};
|
||||
if (newFilters && newFilters.size === 1) {
|
||||
params.filters = Array.from(newFilters)[0];
|
||||
}
|
||||
|
||||
setSearchParams(params);
|
||||
},
|
||||
[setSearchParams]
|
||||
);
|
||||
|
||||
return {
|
||||
updateFilters,
|
||||
txsData,
|
||||
loading,
|
||||
error,
|
||||
hasMoreTxs,
|
||||
hasPreviousPage,
|
||||
previousCursors,
|
||||
cursor,
|
||||
refreshTxs,
|
||||
nextPage,
|
||||
previousPage,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a Set of filters based on the URLSearchParams, or
|
||||
* defaults to all.
|
||||
* @param params
|
||||
* @returns Set
|
||||
*/
|
||||
export function getInitialFilters(params: URLSearchParams): Set<FilterOption> {
|
||||
const defaultFilters = new Set(AllFilterOptions);
|
||||
|
||||
const p = params.get('filters');
|
||||
|
||||
if (!p) {
|
||||
return defaultFilters;
|
||||
}
|
||||
|
||||
const filters = new Set<FilterOption>();
|
||||
p.split(',').forEach((f) => {
|
||||
if (AllFilterOptions.includes(f as FilterOption)) {
|
||||
filters.add(f as FilterOption);
|
||||
}
|
||||
});
|
||||
|
||||
if (filters.size === 0) {
|
||||
return defaultFilters;
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ describe('Block', () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: createBlockResponse(1), loading: false, error: null },
|
||||
});
|
||||
|
||||
render(renderComponent(1));
|
||||
await waitFor(() => screen.getByTestId('block-header'));
|
||||
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import type { TendermintBlocksResponse } from '../tendermint-blocks-response';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { TimeAgo } from '../../../components/time-ago';
|
||||
import {
|
||||
@@ -14,7 +12,7 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Routes } from '../../route-names';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { useBlockInfo } from '@vegaprotocol/tendermint';
|
||||
import { NodeLink } from '../../../components/links';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
import EmptyList from '../../../components/empty-list/empty-list';
|
||||
@@ -24,10 +22,7 @@ const Block = () => {
|
||||
useDocumentTitle(['Blocks', `Block #${block}`]);
|
||||
const {
|
||||
state: { data: blockData, loading, error },
|
||||
} = useFetch<TendermintBlocksResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/block?height=${block}`,
|
||||
{ cache: 'force-cache' }
|
||||
);
|
||||
} = useBlockInfo(Number(block));
|
||||
|
||||
return (
|
||||
<section>
|
||||
|
||||
@@ -54,7 +54,7 @@ export const PartyBlockStake = ({
|
||||
{p?.stakingSummary.currentStakeAvailable ? (
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<div>{t('Available stake')}</div>
|
||||
<div>{t('Associated to key')}</div>
|
||||
<div>
|
||||
<GovernanceAssetBalance
|
||||
price={p.stakingSummary.currentStakeAvailable}
|
||||
@@ -62,7 +62,7 @@ export const PartyBlockStake = ({
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow noBorder={true}>
|
||||
<div>{t('Active stake')}</div>
|
||||
<div>{t('Staked to validator')}</div>
|
||||
<div>
|
||||
<GovernanceAssetBalance price={linkedStake || '0'} />
|
||||
</div>
|
||||
|
||||
@@ -15,9 +15,13 @@ import { PartyBlockAccounts } from './components/party-block-accounts';
|
||||
import { isValidPartyId } from './components/party-id-error';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const Party = () => {
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||
const { party } = useParams<{ party: string }>();
|
||||
|
||||
@@ -27,24 +31,21 @@ const Party = () => {
|
||||
const partyId = toNonHex(party ? party : '');
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
|
||||
const baseFilters = `filters[tx.submitter]=${partyId}`;
|
||||
const f =
|
||||
filters && filters.size === 1
|
||||
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}`
|
||||
: baseFilters;
|
||||
|
||||
const {
|
||||
hasMoreTxs,
|
||||
nextPage,
|
||||
refreshTxs,
|
||||
previousPage,
|
||||
error,
|
||||
refreshTxs,
|
||||
loading,
|
||||
txsData,
|
||||
hasPreviousPage,
|
||||
hasMoreTxs,
|
||||
updateFilters,
|
||||
} = useTxsData({
|
||||
limit: 25,
|
||||
filters: f,
|
||||
filters: filters.size === 1 ? filters : undefined,
|
||||
before: params.get('before') || undefined,
|
||||
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||
party: partyId,
|
||||
});
|
||||
|
||||
const variables = useMemo(() => ({ partyId }), [partyId]);
|
||||
@@ -67,7 +68,7 @@ const Party = () => {
|
||||
text: t('Go back'),
|
||||
action: () => navigate(-1),
|
||||
className: 'py-1',
|
||||
size: 'sm',
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -102,15 +103,21 @@ const Party = () => {
|
||||
refreshTxs={refreshTxs}
|
||||
nextPage={nextPage}
|
||||
previousPage={previousPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
hasPreviousPage={true}
|
||||
loading={loading}
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
>
|
||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||
<TxsFilter
|
||||
filters={filters}
|
||||
setFilters={(f) => {
|
||||
setFilters(f);
|
||||
updateFilters(f as Set<FilterOption>);
|
||||
}}
|
||||
/>
|
||||
</TxsListNavigation>
|
||||
{!error && txsData ? (
|
||||
<TxsInfiniteList
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
hasMoreTxs={true}
|
||||
areTxsLoading={loading}
|
||||
txs={txsData}
|
||||
loadMoreTxs={nextPage}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Block } from './blocks/id';
|
||||
import { Blocks } from './blocks/home';
|
||||
import { Tx } from './txs/id';
|
||||
import { TxsList } from './txs/home';
|
||||
import flags from '../config/flags';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
@@ -30,6 +29,7 @@ import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||
import { Disclaimer } from './pages/disclaimer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
@@ -59,7 +59,7 @@ type Route = RouteItem & {
|
||||
children?: RouteItem[];
|
||||
};
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
const partiesRoutes: Route[] = FLAGS.EXPLORER_PARTIES
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
@@ -119,7 +119,7 @@ const partiesRoutes: Route[] = flags.parties
|
||||
]
|
||||
: [];
|
||||
|
||||
const assetsRoutes: Route[] = flags.assets
|
||||
const assetsRoutes: Route[] = FLAGS.EXPLORER_ASSETS
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
@@ -147,7 +147,7 @@ const assetsRoutes: Route[] = flags.assets
|
||||
]
|
||||
: [];
|
||||
|
||||
const genesisRoutes: Route[] = flags.genesis
|
||||
const genesisRoutes: Route[] = FLAGS.EXPLORER_GENESIS
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
@@ -163,7 +163,7 @@ const genesisRoutes: Route[] = flags.genesis
|
||||
]
|
||||
: [];
|
||||
|
||||
const governanceRoutes: Route[] = flags.governance
|
||||
const governanceRoutes: Route[] = FLAGS.EXPLORER_GOVERNANCE
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
@@ -179,7 +179,7 @@ const governanceRoutes: Route[] = flags.governance
|
||||
]
|
||||
: [];
|
||||
|
||||
const marketsRoutes: Route[] = flags.markets
|
||||
const marketsRoutes: Route[] = FLAGS.EXPLORER_MARKETS
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
@@ -207,7 +207,7 @@ const marketsRoutes: Route[] = flags.markets
|
||||
]
|
||||
: [];
|
||||
|
||||
const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
const networkParametersRoutes: Route[] = FLAGS.EXPLORER_NETWORK_PARAMETERS
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
@@ -225,7 +225,7 @@ const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
]
|
||||
: [];
|
||||
|
||||
const validators: Route[] = flags.validators
|
||||
const validators: Route[] = FLAGS.EXPLORER_VALIDATORS
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { TxsInfiniteList } from '../../../components/txs';
|
||||
import { useTxsData } from '../../../hooks/use-txs-data';
|
||||
import { useTxsData, getInitialFilters } from '../../../hooks/use-txs-data';
|
||||
import { useDocumentTitle } from '../../../hooks/use-document-title';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import { TxsFilter } from '../../../components/txs/tx-filter';
|
||||
import type { FilterOption } from '../../../components/txs/tx-filter';
|
||||
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
|
||||
|
||||
const BE_TXS_PER_REQUEST = 25;
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export const TxsList = () => {
|
||||
useDocumentTitle(['Transactions']);
|
||||
@@ -27,25 +27,22 @@ export const TxsList = () => {
|
||||
* @returns {JSX.Element} Transaction List and controls
|
||||
*/
|
||||
export const TxsListFiltered = () => {
|
||||
const [filters, setFilters] = useState(new Set(AllFilterOptions));
|
||||
|
||||
const f =
|
||||
filters && filters.size === 1
|
||||
? `filters[cmd.type]=${Array.from(filters)[0]}`
|
||||
: '';
|
||||
const [params] = useSearchParams();
|
||||
const [filters, setFilters] = useState(getInitialFilters(params));
|
||||
|
||||
const {
|
||||
hasMoreTxs,
|
||||
nextPage,
|
||||
previousPage,
|
||||
error,
|
||||
refreshTxs,
|
||||
loading,
|
||||
txsData,
|
||||
hasPreviousPage,
|
||||
hasMoreTxs,
|
||||
updateFilters,
|
||||
} = useTxsData({
|
||||
limit: BE_TXS_PER_REQUEST,
|
||||
filters: f,
|
||||
filters: filters.size === 1 ? filters : undefined,
|
||||
before: params.get('before') || undefined,
|
||||
after: !params.get('before') ? params.get('after') || undefined : undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -54,11 +51,17 @@ export const TxsListFiltered = () => {
|
||||
refreshTxs={refreshTxs}
|
||||
nextPage={nextPage}
|
||||
previousPage={previousPage}
|
||||
hasPreviousPage={hasPreviousPage}
|
||||
hasPreviousPage={true}
|
||||
loading={loading}
|
||||
hasMoreTxs={hasMoreTxs}
|
||||
>
|
||||
<TxsFilter filters={filters} setFilters={setFilters} />
|
||||
<TxsFilter
|
||||
filters={filters}
|
||||
setFilters={(f) => {
|
||||
setFilters(f);
|
||||
updateFilters(f as Set<FilterOption>);
|
||||
}}
|
||||
/>
|
||||
</TxsListNavigation>
|
||||
<TxsInfiniteList
|
||||
hasFilters={filters.size > 0}
|
||||
|
||||
@@ -15,6 +15,9 @@ query ExplorerNodes {
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
pendingStake
|
||||
rankingScore {
|
||||
performanceScore
|
||||
}
|
||||
epochData {
|
||||
total
|
||||
offline
|
||||
|
||||
@@ -6,7 +6,7 @@ const defaultOptions = {} as const;
|
||||
export type ExplorerNodesQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
|
||||
export type ExplorerNodesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, infoUrl: string, avatarUrl?: string | null, pubkey: string, tmPubkey: string, ethereumAddress: string, location: string, status: Types.NodeStatus, stakedByOperator: string, stakedByDelegates: string, stakedTotal: string, pendingStake: string, rankingScore: { __typename?: 'RankingScore', performanceScore: string }, epochData?: { __typename?: 'EpochData', total: number, offline: number, online: number } | null } } | null> | null } };
|
||||
|
||||
|
||||
export const ExplorerNodesDocument = gql`
|
||||
@@ -27,6 +27,9 @@ export const ExplorerNodesDocument = gql`
|
||||
stakedByDelegates
|
||||
stakedTotal
|
||||
pendingStake
|
||||
rankingScore {
|
||||
performanceScore
|
||||
}
|
||||
epochData {
|
||||
total
|
||||
offline
|
||||
|
||||
@@ -91,7 +91,15 @@ export const ValidatorsPage = () => {
|
||||
const { data: tmData } = useTendermintValidators(5000);
|
||||
const { data, loading, error, refetch } = useExplorerNodesQuery();
|
||||
|
||||
const validators = compact(data?.nodesConnection.edges?.map((e) => e?.node));
|
||||
const validators = compact(
|
||||
data?.nodesConnection.edges
|
||||
?.map((e) => e?.node)
|
||||
.filter(
|
||||
(node) =>
|
||||
node?.rankingScore?.performanceScore &&
|
||||
new BigNumber(node.rankingScore.performanceScore).isGreaterThan(0)
|
||||
)
|
||||
);
|
||||
|
||||
// voting power
|
||||
const powers = compact(tmData?.result.validators).map(
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"changes": {
|
||||
"decimalPlaces": "5",
|
||||
"positionDecimalPlaces": "5",
|
||||
"linearSlippageFactor": "0.001",
|
||||
"quadraticSlippageFactor": "0",
|
||||
"lpPriceRange": "10",
|
||||
"instrument": {
|
||||
"name": "Token test market",
|
||||
"code": "Token.24h",
|
||||
"future": {
|
||||
"settlementAsset": "816af99af60d684502a40824758f6b5377e6af48e50a9ee8ef478ecb879ea8bc",
|
||||
"quoteName": "fUSDC",
|
||||
"dataSourceSpecForSettlementData": {
|
||||
"external": {
|
||||
"oracle": {
|
||||
"signers": [
|
||||
{
|
||||
"pubKey": {
|
||||
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"key": {
|
||||
"name": "prices.BTC.value",
|
||||
"type": "TYPE_INTEGER",
|
||||
"numberDecimalPlaces": "0"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"operator": "OPERATOR_GREATER_THAN",
|
||||
"value": "0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataSourceSpecForTradingTermination": {
|
||||
"external": {
|
||||
"oracle": {
|
||||
"signers": [
|
||||
{
|
||||
"pubKey": {
|
||||
"key": "70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680"
|
||||
}
|
||||
}
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"key": {
|
||||
"name": "trading.terminated.ETH5",
|
||||
"type": "TYPE_BOOLEAN"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"operator": "OPERATOR_EQUALS",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"dataSourceSpecBinding": {
|
||||
"settlementDataProperty": "prices.BTC.value",
|
||||
"tradingTerminationProperty": "trading.terminated.ETH5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"metadata": ["sector:food", "sector:materials", "source:docs.vega.xyz"],
|
||||
"priceMonitoringParameters": {
|
||||
"triggers": [
|
||||
{
|
||||
"horizon": "43200",
|
||||
"probability": "0.9999999",
|
||||
"auctionExtension": "600"
|
||||
}
|
||||
]
|
||||
},
|
||||
"liquidityMonitoringParameters": {
|
||||
"targetStakeParameters": {
|
||||
"timeWindow": "3600",
|
||||
"scalingFactor": 10
|
||||
},
|
||||
"triggeringRatio": "0.7",
|
||||
"auctionExtension": "1"
|
||||
},
|
||||
"logNormal": {
|
||||
"tau": 0.0001140771161,
|
||||
"riskAversionParameter": 0.001,
|
||||
"params": {
|
||||
"mu": 0,
|
||||
"r": 0.016,
|
||||
"sigma": 0.8
|
||||
}
|
||||
},
|
||||
"successor": {
|
||||
"parentMarketId": "",
|
||||
"insurancePoolFraction": "0.75"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,17 +218,33 @@ describe(
|
||||
getProposalInformationFromTable('Tokens for proposal')
|
||||
.should('have.text', (1).toFixed(2))
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||
// 3002-PROP-021
|
||||
cy.getByTestId('user-voted-yes').should('exist');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(changeVoteButton).should('be.visible').click();
|
||||
voteForProposal('against');
|
||||
cy.getByTestId(proposalVoteProgressAgainstPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
cy.getByTestId(voteBreakdownToggle).click();
|
||||
getProposalInformationFromTable('Tokens against proposal')
|
||||
.should('have.text', (1).toFixed(2))
|
||||
.and('be.visible');
|
||||
getProposalInformationFromTable('Number of voting parties')
|
||||
.should('have.text', '1')
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
|
||||
getProposalFromTitle(rawProposal.rationale.title).within(() => {
|
||||
cy.getByTestId('user-voted-no').should('exist');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
|
||||
|
||||
@@ -26,6 +26,8 @@ const votesTable = 'votes-table';
|
||||
const openProposals = 'open-proposals';
|
||||
const proposalVoteProgressForPercentage =
|
||||
'vote-progress-indicator-percentage-for';
|
||||
const majorityVoteReached = 'majority-reached';
|
||||
const minParticipationReached = 'participation-reached';
|
||||
const proposalTimeout = { timeout: 8000 };
|
||||
|
||||
context(
|
||||
@@ -72,7 +74,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
// 3001-VOTE-020 3001-VOTE-021 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
it('Able to enact proposal by voting', function () {
|
||||
const proposalTitle = 'Add New proposal with short enactment';
|
||||
const proposalTx = createUpdateNetworkProposalTxBody();
|
||||
@@ -85,10 +87,18 @@ context(
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil(proposalListItem)
|
||||
.last()
|
||||
.within(() => cy.getByTestId(viewProposalButton).click());
|
||||
.within(() => {
|
||||
// 3001-VOTE-019 time to vote is highlighted red
|
||||
cy.getByTestId('vote-details')
|
||||
.find('span')
|
||||
.should('have.class', 'text-vega-pink');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
cy.getByTestId(proposalStatus).should('have.text', 'Open');
|
||||
voteForProposal('for');
|
||||
cy.getByTestId(majorityVoteReached).should('exist');
|
||||
cy.getByTestId(minParticipationReached).should('exist');
|
||||
cy.getByTestId(proposalStatus, proposalTimeout)
|
||||
.should('have.text', 'Passed')
|
||||
.then(() => {
|
||||
@@ -104,6 +114,14 @@ context(
|
||||
cy.getByTestId(proposalVoteProgressForPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
navigateTo(navigation.proposals);
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil(proposalListItem)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.getByTestId(majorityVoteReached).should('exist');
|
||||
cy.getByTestId(minParticipationReached).should('exist');
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-047
|
||||
|
||||
@@ -220,7 +220,7 @@ context(
|
||||
function () {
|
||||
const proposalTitle = 'Test new market proposal';
|
||||
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
|
||||
cy.getByTestId(newProposalTitle).type('Test new market proposal');
|
||||
cy.getByTestId(newProposalTitle).type(proposalTitle);
|
||||
cy.getByTestId(newProposalDescription).type('E2E test for proposals');
|
||||
cy.fixture('/proposals/new-market').then((newMarketProposal) => {
|
||||
const newMarketPayload = JSON.stringify(newMarketProposal);
|
||||
@@ -606,6 +606,94 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
it('able to submit successor market proposal', function () {
|
||||
const proposalTitle = 'Test successor market proposal';
|
||||
|
||||
cy.createMarket();
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
cy.getByTestId('closed-proposals').within(() => {
|
||||
cy.contains('Add Lorem Ipsum market')
|
||||
.parentsUntil(proposalListItem)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.getByTestId(viewProposalBtn).click();
|
||||
});
|
||||
});
|
||||
getProposalInformationFromTable('ID').invoke('text').as('parentMarketId');
|
||||
goToMakeNewProposal(governanceProposalType.NEW_MARKET);
|
||||
cy.getByTestId(newProposalTitle).type(proposalTitle);
|
||||
cy.getByTestId(newProposalDescription).type(
|
||||
'E2E test for successor market'
|
||||
);
|
||||
cy.fixture('/proposals/successor-market').then((newMarketProposal) => {
|
||||
newMarketProposal.changes.successor.parentMarketId =
|
||||
this.parentMarketId;
|
||||
const newMarketPayload = JSON.stringify(newMarketProposal);
|
||||
cy.getByTestId(newProposalTerms).type(newMarketPayload, {
|
||||
parseSpecialCharSequences: false,
|
||||
delay: 2,
|
||||
});
|
||||
});
|
||||
cy.getByTestId(proposalDownloadBtn)
|
||||
.should('be.visible')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.wrap(
|
||||
getDownloadedProposalJsonPath('vega-new-market-proposal-')
|
||||
).then((filePath) => {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
submitUniqueRawProposal({ proposalBody: filePath });
|
||||
});
|
||||
});
|
||||
navigateTo(navigation.proposals);
|
||||
getProposalFromTitle(proposalTitle).within(() => {
|
||||
// 3003-PMAN-008
|
||||
cy.getByTestId('proposal-successor-info')
|
||||
.should('have.text', 'Successor market to: TEST.24h')
|
||||
.find('a')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', this.parentMarketId);
|
||||
cy.getByTestId('view-proposal-btn').click();
|
||||
});
|
||||
// #3003-PMAN-010
|
||||
cy.getByTestId(proposalJsonToggle).click();
|
||||
cy.get('.language-json').within(() => {
|
||||
cy.get('.hljs-attr').should('contain.text', 'parentMarketId');
|
||||
cy.get('.hljs-string').should('contain.text', this.parentMarketId);
|
||||
cy.get('.hljs-attr').should('contain.text', 'insurancePoolFraction');
|
||||
cy.get('.hljs-string').should('contain.text', '0.75');
|
||||
});
|
||||
cy.getByTestId('proposal-market-data').within(() => {
|
||||
cy.getByTestId('proposal-market-data-toggle').click();
|
||||
cy.contains('Key details').click();
|
||||
// 3003-PMAN-009
|
||||
getMarketProposalDetailsFromTable('Parent Market ID').should(
|
||||
'have.text',
|
||||
this.parentMarketId
|
||||
);
|
||||
getMarketProposalDetailsFromTable('Insurance Pool Fraction').should(
|
||||
'have.text',
|
||||
'0.75'
|
||||
);
|
||||
getMarketProposalDetailsFromTable('Trading Mode').should(
|
||||
'have.text',
|
||||
'No trading'
|
||||
);
|
||||
});
|
||||
// 3003-PMAN-011
|
||||
cy.contains('Parent Market ID').realHover();
|
||||
cy.getByTestId('tooltip-content').should(
|
||||
'contain.text',
|
||||
'The ID of the market this market succeeds.'
|
||||
);
|
||||
cy.contains('Insurance Pool Fraction').realHover();
|
||||
cy.getByTestId('tooltip-content').should(
|
||||
'contain.text',
|
||||
'The fraction of the insurance pool balance that is carried over from the parent market to the successor.'
|
||||
);
|
||||
});
|
||||
|
||||
after('Disassociate from second wallet key if present', function () {
|
||||
cy.reload();
|
||||
waitForSpinner();
|
||||
|
||||
@@ -49,8 +49,8 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
|
||||
// 3001-VOTE-018
|
||||
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
|
||||
// 3001-VOTE-005
|
||||
const proposalDays = [364, 50, 2];
|
||||
for (let index = 0; index < proposalDays.length; index++) {
|
||||
goToMakeNewProposal(governanceProposalType.RAW);
|
||||
@@ -120,16 +120,24 @@ describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||
submitUniqueRawProposal({ proposalTitle: proposalTitle });
|
||||
getProposalFromTitle(proposalTitle).within(() => {
|
||||
// 3001-VOTE-039
|
||||
cy.getByTestId(voteStatus).should(
|
||||
cy.getByTestId('participation-not-reached').should(
|
||||
'have.text',
|
||||
'Participation not reached'
|
||||
'Min. participation not reached'
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
voteForProposal('for');
|
||||
navigateTo(navigation.proposals);
|
||||
getProposalFromTitle(proposalTitle).within(() => {
|
||||
cy.getByTestId(voteStatus).should('have.text', 'Set to pass');
|
||||
cy.getByTestId(voteStatus).should(
|
||||
'have.text',
|
||||
'Currently expected to pass'
|
||||
);
|
||||
cy.getByTestId('user-voted-yes').should('exist');
|
||||
cy.getByTestId('participation-reached').should(
|
||||
'have.text',
|
||||
'Min. participation reached'
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(voteBreakDownToggle).click();
|
||||
|
||||
@@ -96,12 +96,14 @@ context('rewards - flow', { tags: '@slow' }, function () {
|
||||
.within(() => {
|
||||
cy.get('h2').first().should('contain.text', 'EPOCH');
|
||||
cy.getByTestId('individual-rewards-asset').should('have.text', 'Vega');
|
||||
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut)
|
||||
.should('contain.text', '0.4415')
|
||||
.and('contain.text', '(44.15%)');
|
||||
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')
|
||||
.should('contain.text', '0.0004')
|
||||
.and('contain.text', '(44.15%)');
|
||||
cy.getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD', rewardsTimeOut).should(
|
||||
'contain.text',
|
||||
'0.4415'
|
||||
);
|
||||
cy.getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE').should(
|
||||
'contain.text',
|
||||
'0.0004'
|
||||
);
|
||||
cy.getByTestId('total').should('have.text', '0.4419');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +144,7 @@ context(
|
||||
closeStakingDialog();
|
||||
navigateTo(navigation.validators);
|
||||
cy.getByTestId(viewStakedByMeToggle).click();
|
||||
cy.getByTestId(userStakeBtn).should('have.length', 2);
|
||||
cy.getByTestId(userStakeBtn, epochTimeout).should('have.length', 2);
|
||||
});
|
||||
|
||||
it('Able to stake against a validator - using vega from vesting contract', function () {
|
||||
|
||||
@@ -351,6 +351,12 @@ context(
|
||||
);
|
||||
stakingPageDisassociateAllTokens();
|
||||
});
|
||||
|
||||
it('Able to associate over 1 million tokens', function () {
|
||||
cy.get(ethWalletAssociateButton).click();
|
||||
stakingPageAssociateTokens('2000000', { approve: true });
|
||||
stakingPageDisassociateAllTokens();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, vote, and propose');
|
||||
.and('have.text', 'See all proposals');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const governanceDocsUrl = 'https://vega.xyz/governance';
|
||||
const networkUpgradeProposalListItem = 'protocol-upgrade-proposals-list-item';
|
||||
const closedProposals = 'closed-proposals';
|
||||
const closedProposalToggle = 'closed-proposals-toggle-networkUpgrades';
|
||||
const protocolUpgradeTime = 'protocol-upgrade-time';
|
||||
|
||||
context(
|
||||
'Governance Page - verify elements on page',
|
||||
@@ -161,7 +162,7 @@ context(
|
||||
);
|
||||
cy.getByTestId('protocol-upgrade-proposal-status').should(
|
||||
'have.text',
|
||||
'Approved by validators '
|
||||
'Approved by validators'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -174,6 +175,7 @@ context(
|
||||
});
|
||||
});
|
||||
|
||||
// 3009-NTWU-003 3009-NTWU-004 3009-NTWU-007
|
||||
it('should see details of network upgrade proposal', function () {
|
||||
mockNetworkUpgradeProposal();
|
||||
navigateTo(navigation.proposals);
|
||||
@@ -227,5 +229,51 @@ context(
|
||||
cy.getByTestId(networkUpgradeProposalListItem).should('not.exist');
|
||||
cy.getByTestId(closedProposalToggle).should('not.exist');
|
||||
});
|
||||
|
||||
// 3009-NTWU-001 3009-NTWU-002 3009-NTWU-006 3009-NTWU-009
|
||||
it('should display network upgrade banner with estimate', function () {
|
||||
mockNetworkUpgradeProposal();
|
||||
cy.visit('/');
|
||||
cy.getByTestId('banners').within(() => {
|
||||
cy.get('div')
|
||||
.should('contain.text', 'The network will upgrade to v1 in ')
|
||||
.and(
|
||||
'contain.text',
|
||||
'Trading activity will be interrupted, manage your risk appropriately.'
|
||||
);
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', '/proposals/protocol-upgrade/v1');
|
||||
});
|
||||
|
||||
// estimate does not display possibly due to mocks or Cypress unless the proposal is clicked on several times
|
||||
// By default the application waits for 10 blocks until showing estimate - roughly 10 seconds
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(3000);
|
||||
navigateTo(navigation.proposals);
|
||||
cy.getByTestId(networkUpgradeProposalListItem)
|
||||
.first()
|
||||
.find('[data-testid="view-proposal-btn"]')
|
||||
.click();
|
||||
}
|
||||
cy.getByTestId('upgrade-proposal-estimate')
|
||||
.invoke('text')
|
||||
.as('displayedEstimate');
|
||||
cy.get('@displayedEstimate').then((estimateText) => {
|
||||
// Estimated time should automatically update every second
|
||||
cy.getByTestId(protocolUpgradeTime)
|
||||
.invoke('text')
|
||||
.should('not.eq', estimateText);
|
||||
});
|
||||
// time estimate on proposal detail
|
||||
cy.getByTestId('protocol-upgrade-proposal').within(() => {
|
||||
cy.get('@displayedEstimate').then((estimateText) => {
|
||||
cy.getByTestId(protocolUpgradeTime)
|
||||
.invoke('text')
|
||||
.should('not.eq', estimateText);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -339,7 +339,7 @@ context(
|
||||
cy.getByTestId(vegaWalletCurrencyTitle)
|
||||
.contains(name)
|
||||
.parent()
|
||||
.siblings()
|
||||
.siblings(txTimeout)
|
||||
.should((elementAmount) => {
|
||||
const displayedAmount = parseFloat(elementAmount.text());
|
||||
expect(displayedAmount).be.gte(expectedAmount);
|
||||
|
||||
@@ -107,7 +107,7 @@ export function dissociateFromSecondWalletKey() {
|
||||
cy.getByTestId('vega-in-wallet')
|
||||
.first()
|
||||
.within(() => {
|
||||
cy.getByTestId('eth-wallet-associated-balances')
|
||||
cy.getByTestId('eth-wallet-associated-balances', txTimeout)
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.getByTestId('associated-key')
|
||||
|
||||
@@ -78,13 +78,21 @@ export function stakingPageAssociateTokens(
|
||||
}
|
||||
cy.get(tokenAmountInputBox, epochTimeout).type(amount);
|
||||
if (approve) {
|
||||
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
|
||||
cy.contains('Approve $VEGA Tokens for staking on Vega').should(
|
||||
'be.visible'
|
||||
);
|
||||
cy.contains('Approve $VEGA Tokens for staking on Vega', txTimeout).should(
|
||||
'not.exist'
|
||||
);
|
||||
cy.getByTestId('wallet-associate').then((walletAssociateField) => {
|
||||
if (
|
||||
walletAssociateField.find('[data-testid="token-input-approve-button"]')
|
||||
.length
|
||||
) {
|
||||
cy.get(tokenInputApprove, txTimeout).should('be.enabled').click();
|
||||
cy.contains('Approve $VEGA Tokens for staking on Vega').should(
|
||||
'be.visible'
|
||||
);
|
||||
cy.contains(
|
||||
'Approve $VEGA Tokens for staking on Vega',
|
||||
txTimeout
|
||||
).should('not.exist');
|
||||
}
|
||||
});
|
||||
}
|
||||
cy.get(tokenSubmitButton, txTimeout).should('be.enabled').click();
|
||||
|
||||
@@ -174,7 +182,8 @@ export function clickOnValidatorFromList(
|
||||
cy.get(`[row-id="${validatorNumber}"]`)
|
||||
.should('be.visible')
|
||||
.find(stakeValidatorListName)
|
||||
.click();
|
||||
.as('validatorOnList');
|
||||
cy.get('@validatorOnList').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
|
||||
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
LC_ALL="en_US.UTF-8"
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -19,6 +19,11 @@ NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=http://localhost:26617
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
|
||||
|
||||
#Test configuration variables
|
||||
CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -13,3 +13,9 @@ NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -14,3 +14,9 @@ NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -1,8 +1,8 @@
|
||||
# App configuration variables
|
||||
NX_VEGA_ENV=MAINNET-MIRROR
|
||||
NX_VEGA_ENV=MAINNET_MIRROR
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml
|
||||
NX_VEGA_URL=https://api.mainnet-mirror.vega.rocks/graphql
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https://governance.fairground.wtf","MAINNET":"https://governance.vega.xyz","MAINNET-MIRROR":"https://governance.mainnet-mirror.vega.rocks","STAGNET1":"https://trading.stagnet1.vega.rocks"}'
|
||||
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","TESTNET":"https://governance.fairground.wtf","MAINNET":"https://governance.vega.xyz","MAINNET_MIRROR":"https://governance.mainnet-mirror.vega.rocks","STAGNET1":"https://trading.stagnet1.vega.rocks"}'
|
||||
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
@@ -13,3 +13,9 @@ NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -9,3 +9,9 @@ NX_DELEGATIONS_PAGINATION=50
|
||||
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -14,3 +14,9 @@ NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
@@ -11,3 +11,9 @@ NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
|
||||
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=false
|
||||
@@ -2,14 +2,13 @@ import * as Sentry from '@sentry/react';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SplashError } from './components/splash-error';
|
||||
import { SplashLoader } from './components/splash-loader';
|
||||
import { Flags } from './config';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
@@ -80,7 +79,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!Flags.NETWORK_DOWN) {
|
||||
if (!FLAGS.GOVERNANCE_NETWORK_DOWN) {
|
||||
run();
|
||||
}
|
||||
}, [token, appDispatch, staking, vesting]);
|
||||
@@ -148,7 +147,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
};
|
||||
|
||||
// Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22
|
||||
if (Flags.NETWORK_LIMITS) {
|
||||
if (FLAGS.GOVERNANCE_NETWORK_LIMITS) {
|
||||
getNetworkLimits();
|
||||
}
|
||||
|
||||
@@ -157,7 +156,7 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
};
|
||||
}, [appDispatch, VEGA_URL, t]);
|
||||
|
||||
if (Flags.NETWORK_DOWN) {
|
||||
if (FLAGS.GOVERNANCE_NETWORK_DOWN) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashError />
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './json-diff';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import { JsonDiff } from './json-diff';
|
||||
|
||||
describe('JsonDiff', () => {
|
||||
afterEach(cleanup);
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(<JsonDiff left={{}} right={{}} />);
|
||||
expect(screen.getByTestId('json-diff')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the correct message when both objects are identical', () => {
|
||||
render(<JsonDiff left={{}} right={{}} />);
|
||||
expect(screen.getByText('Data is identical')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the "identical" message when both objects are not identical', () => {
|
||||
render(
|
||||
<JsonDiff
|
||||
left={{
|
||||
name: 'test',
|
||||
}}
|
||||
right={{
|
||||
name: 'test2',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByText('Data is identical')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatters, create } from 'jsondiffpatch';
|
||||
import 'jsondiffpatch/dist/formatters-styles/html.css';
|
||||
import 'jsondiffpatch/dist/formatters-styles/annotated.css';
|
||||
|
||||
export type JsonValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| JsonValue[]
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
interface JsonDiffProps {
|
||||
left: JsonValue;
|
||||
right: JsonValue;
|
||||
objectHash?: (obj: unknown) => string | undefined;
|
||||
}
|
||||
|
||||
export const JsonDiff = ({ right, left, objectHash }: JsonDiffProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [html, setHtml] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const delta = create({
|
||||
objectHash,
|
||||
}).diff(left, right);
|
||||
|
||||
if (delta) {
|
||||
const deltaHtml = formatters.html.format(delta, left);
|
||||
|
||||
formatters.html.hideUnchanged();
|
||||
|
||||
setHtml(deltaHtml);
|
||||
} else {
|
||||
setHtml(undefined);
|
||||
}
|
||||
}, [right, left, objectHash]);
|
||||
|
||||
return html ? (
|
||||
<div data-testid="json-diff" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
) : (
|
||||
<p data-testid="json-diff">{t('dataIsIdentical')}</p>
|
||||
);
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import { VegaWallet } from '../vega-wallet';
|
||||
import { useLocation, useMatch } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { useTelemetryDialog } from '../telemetry-dialog/telemetry-dialog';
|
||||
import { ProtocolUpgradeCountdown } from '@vegaprotocol/proposals';
|
||||
|
||||
export const SettingsLink = () => {
|
||||
const { open, isOpen, close } = useTelemetryDialog();
|
||||
@@ -61,7 +62,12 @@ export const Nav = ({ theme }: Pick<NavigationProps, 'theme'>) => {
|
||||
appName="Governance"
|
||||
theme={theme}
|
||||
breakpoints={[458, 959]}
|
||||
actions={<SettingsLink />}
|
||||
actions={
|
||||
<>
|
||||
<SettingsLink />
|
||||
<ProtocolUpgradeCountdown />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<NavigationList
|
||||
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"
|
||||
|
||||
@@ -4,7 +4,11 @@ import type { ReactNode } from 'react';
|
||||
import { AnnouncementBanner } from '@vegaprotocol/announcements';
|
||||
import { Nav } from '../nav';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import React from 'react';
|
||||
import {
|
||||
ProtocolUpgradeCountdownMode,
|
||||
ProtocolUpgradeProposalNotification,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
@@ -31,8 +35,23 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
|
||||
/>
|
||||
)}
|
||||
<Nav theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
|
||||
<NotificationsContainer />
|
||||
</div>
|
||||
<div className={AppLayoutClasses}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationsContainer = () => {
|
||||
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
|
||||
return (
|
||||
<div data-testid="banners">
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
/>
|
||||
{isReadOnly ? (
|
||||
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import React from 'react';
|
||||
|
||||
export interface TemplateSidebarProps {
|
||||
@@ -8,24 +6,18 @@ export interface TemplateSidebarProps {
|
||||
}
|
||||
|
||||
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
|
||||
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
|
||||
return (
|
||||
<>
|
||||
{isReadOnly ? (
|
||||
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
|
||||
) : null}
|
||||
<div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[1fr] lg:grid-cols-[1fr_450px]">
|
||||
<main className="max-w-[100vw] col-start-1 p-4 overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
|
||||
{sidebar.map((Component, i) => (
|
||||
<section className="mb-4 last:mb-0" key={i}>
|
||||
{Component}
|
||||
</section>
|
||||
))}
|
||||
</aside>
|
||||
</div>
|
||||
</>
|
||||
<div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[1fr] lg:grid-cols-[1fr_450px]">
|
||||
<main className="max-w-[100vw] col-start-1 p-4 overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
|
||||
{sidebar.map((Component, i) => (
|
||||
<section className="mb-4 last:mb-0" key={i}>
|
||||
{Component}
|
||||
</section>
|
||||
))}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export const ContractAddresses: {
|
||||
claimAddress: customClaimAddress ?? '0x0',
|
||||
lockedAddress: customLockedAddress ?? '0x0',
|
||||
},
|
||||
MAINNET_MIRROR: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
},
|
||||
DEVNET: {
|
||||
claimAddress: '0x8Cef746ab7C83B61F6461cC92882bD61AB65a994', // TODO not deployed to this env, but random address so app doesn't error
|
||||
lockedAddress: '0x0', // TODO not deployed to this env
|
||||
@@ -63,12 +67,6 @@ export const ENV = {
|
||||
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
|
||||
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
|
||||
rest: windowOrDefault('NX_VEGA_REST_URL'),
|
||||
flags: {
|
||||
NETWORK_DOWN: TRUTHY.includes(windowOrDefault('NX_NETWORK_DOWN')),
|
||||
MOCK: TRUTHY.includes(windowOrDefault('NX_MOCKED')),
|
||||
FAIRGROUND: TRUTHY.includes(windowOrDefault('NX_FAIRGROUND')),
|
||||
NETWORK_LIMITS: TRUTHY.includes(windowOrDefault('NX_NETWORK_LIMITS')),
|
||||
},
|
||||
addresses:
|
||||
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ENV } from './env';
|
||||
|
||||
export const Flags = {
|
||||
...ENV.flags,
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export * from './flags';
|
||||
export * from './env';
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"proposedEnactment": "Proposed enactment",
|
||||
"Enacted": "Enacted",
|
||||
"enactedOn": "Enacted on",
|
||||
"enactedOn{{date}}": "Enacted on {{enactmentDate}}",
|
||||
"status": "Status",
|
||||
"state": "State",
|
||||
"shouldPass": "Should pass",
|
||||
@@ -185,6 +186,7 @@
|
||||
"proposedOn": "Proposed on",
|
||||
"proposedBy": "Proposed by",
|
||||
"toEnactOn": "Enacts on",
|
||||
"enactsOn{{date}}": "Enacts on {{enactmentDate}}",
|
||||
"closesOn": "Closes on",
|
||||
"closedOn": "Closed on",
|
||||
"errorDetails": "Error details",
|
||||
@@ -217,6 +219,7 @@
|
||||
"votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.",
|
||||
"noGovernanceTokens": "You need some VEGA tokens to participate in governance",
|
||||
"youVoted": "You voted",
|
||||
"voted": "Voted",
|
||||
"changeVote": "Change vote",
|
||||
"txRequested": "Confirm transaction in wallet",
|
||||
"votePending": "Casting vote",
|
||||
@@ -628,7 +631,6 @@
|
||||
"pendingDescriptionLinkText": "set up and run a node on Vega",
|
||||
"pendingDescription2": ". A node can move from being a candidate into standby based on how much nomination it attracts, assuming it has proven reliability by sending heartbeats to the network.",
|
||||
"n/a": "N/A",
|
||||
"Set to": "Set to",
|
||||
"pass": "pass",
|
||||
"fail": "fail",
|
||||
"New asset": "New asset",
|
||||
@@ -788,7 +790,7 @@
|
||||
"overstakedPenalty": "Overstaked penalty",
|
||||
"multisigPenalty": "Multisig penalty",
|
||||
"homeProposalsIntro": "Decisions on the Vega network are on-chain, with tokenholders creating proposals that other tokenholders vote to approve or reject. Network upgrades are proposed and approved by validators.",
|
||||
"homeProposalsButtonText": "Browse, vote, and propose",
|
||||
"homeProposalsButtonText": "See all proposals",
|
||||
"homeValidatorsIntro": "Vega runs on a delegated proof of stake blockchain, where validators earn fees for validating block transactions. Tokenholders can nominate validators by staking tokens to them.",
|
||||
"homeValidatorsButtonText": "Browse, and stake",
|
||||
"homeRewardsIntro": "Track rewards you've earned for trading, liquidity provision, market creation, and staking.",
|
||||
@@ -837,5 +839,21 @@
|
||||
"RejectedProposals": "Rejected proposals",
|
||||
"networkGovernance": "Network governance",
|
||||
"networkUpgrades": "Network upgrades",
|
||||
"assetSpecification": "Asset specification"
|
||||
"assetSpecification": "Asset specification",
|
||||
"viewDetails": "View details",
|
||||
"vegaGovernance": "Vega Governance",
|
||||
"latestProposals": "Latest proposals",
|
||||
"vegaToken": "VEGA Token",
|
||||
"majorityVotedForProposal": "majority voted for this proposal",
|
||||
"majorityNotVotedForProposal": "majority not voted for this proposal",
|
||||
"requiredMajorityVotedForProposal": "Required majority voted for this proposal",
|
||||
"requiredMajorityNotVotedForProposal": "Required majority not voted for this proposal",
|
||||
"minParticipationReached": "Min. participation reached",
|
||||
"minParticipationNotReached": "Min. participation not reached",
|
||||
"consensusNodes": "consensus nodes",
|
||||
"activeNodes": "active nodes",
|
||||
"Estimated time to upgrade": "Estimated time to upgrade",
|
||||
"Upgraded at": "Upgraded at",
|
||||
"dataIsIdentical": "Data is identical",
|
||||
"updatesToMarket": "Updates to market"
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ export const downloadJson = (jsonString: string, proposalTitle: string) => {
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
getNotRejectedProposals,
|
||||
getNotRejectedProtocolUpgradeProposals,
|
||||
} from '../proposals/proposals/proposals-container';
|
||||
import { Heading } from '../../components/heading';
|
||||
import { Heading, SubHeading } from '../../components/heading';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { RouteChildProps } from '..';
|
||||
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
|
||||
@@ -31,6 +31,11 @@ import {
|
||||
orderByDate,
|
||||
orderByUpgradeBlockHeight,
|
||||
} from '../proposals/components/proposals-list/proposals-list';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
|
||||
const nodesToShow = 6;
|
||||
|
||||
@@ -42,33 +47,57 @@ const HomeProposals = ({
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
params: networkParams,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
]);
|
||||
|
||||
return (
|
||||
<section className="mb-16" data-testid="home-proposals">
|
||||
<Heading title={t('Proposals')} />
|
||||
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||
<div className="flex items-center mb-8 gap-8">
|
||||
<Link to={`${Routes.PROPOSALS}`}>
|
||||
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||
</Link>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<section className="mb-16" data-testid="home-proposals">
|
||||
<Heading title={t('vegaGovernance')} />
|
||||
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||
<div className="mb-8">
|
||||
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
|
||||
{t(`readMoreGovernance`)}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
|
||||
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
|
||||
{t(`readMoreGovernance`)}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
<ul
|
||||
data-testid="home-proposal-list"
|
||||
className="grid md:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2 gap-6"
|
||||
>
|
||||
{protocolUpgradeProposals.map((proposal, index) => (
|
||||
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||
))}
|
||||
<SubHeading title={t('latestProposals')} />
|
||||
<ul data-testid="home-proposal-list" className="grid gap-6">
|
||||
{protocolUpgradeProposals.map((proposal, index) => (
|
||||
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||
))}
|
||||
|
||||
{proposals.map((proposal) => (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
{proposals.map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div className="mt-6">
|
||||
<Link to={`${Routes.PROPOSALS}`}>
|
||||
<Button size="md">{t('homeProposalsButtonText')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -86,8 +115,8 @@ const HomeNodes = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const highlightedNodeData = [
|
||||
{ title: t('active nodes'), length: activeNodes.length },
|
||||
{ title: t('consensus nodes'), length: consensusNodes.length },
|
||||
{ title: t('activeNodes'), length: activeNodes.length },
|
||||
{ title: t('consensusNodes'), length: consensusNodes.length },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -233,7 +262,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
[protocolUpgradeProposals]
|
||||
);
|
||||
|
||||
const totalProposalsDesired = 4;
|
||||
const totalProposalsDesired = 3;
|
||||
const protocolUpgradeProposalsToShow = sortedProtocolUpgradeProposals.slice(
|
||||
0,
|
||||
totalProposalsDesired
|
||||
@@ -248,6 +277,8 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
|
||||
const activeNodes = removePaginationWrapper(
|
||||
validatorsData?.nodesConnection.edges
|
||||
).filter((node) =>
|
||||
new BigNumber(node.rankingScore.performanceScore).isGreaterThan(0)
|
||||
);
|
||||
|
||||
const trimmedActiveNodes = activeNodes?.slice(0, nodesToShow);
|
||||
@@ -287,7 +318,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
</div>
|
||||
|
||||
<div data-testid="home-vega-token">
|
||||
<Heading title={t('VEGA Token')} marginTop={false} />
|
||||
<Heading title={t('vegaToken')} marginTop={false} />
|
||||
<h3 className="mb-6">{t('homeVegaTokenIntro')}</h3>
|
||||
<div className="flex items-center mb-8 gap-4">
|
||||
<Link to={Routes.WITHDRAWALS}>
|
||||
|
||||
+10
-32
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
@@ -18,53 +17,32 @@ export const CurrentProposalState = ({
|
||||
|
||||
switch (proposal?.state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Enacted')}</span>
|
||||
<Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Enacted');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Passed')}</span>
|
||||
<Icon name={'tick'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Passed');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_WaitingForNodeVote')}</span>
|
||||
<Icon name={'time'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_WaitingForNodeVote');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
variant = 'primary' as ProposalInfoLabelVariant;
|
||||
proposalStatus = <>{t('voteState_Open')}</>;
|
||||
proposalStatus = t('voteState_Open');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Declined')}</span>
|
||||
<Icon name={'cross'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Declined');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
proposalStatus = (
|
||||
<>
|
||||
<span className="mr-2">{t('voteState_Rejected')}</span>
|
||||
<Icon name={'warning-sign'} />
|
||||
</>
|
||||
);
|
||||
proposalStatus = t('voteState_Rejected');
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_FAILED: {
|
||||
proposalStatus = t('voteState_Failed');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+80
-20
@@ -1,21 +1,66 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import {
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
VoteValue,
|
||||
} from '@vegaprotocol/types';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalHeader } from './proposal-header';
|
||||
import {
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
mockNetworkParams,
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import { lastWeek, nextWeek } from '../../test-helpers/mocks';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
...jest.requireActual('@vegaprotocol/proposals'),
|
||||
useSuccessorMarketProposalDetails: () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
isListItem = true
|
||||
) => render(<ProposalHeader proposal={proposal} isListItem={isListItem} />);
|
||||
isListItem = true,
|
||||
mocks: MockedResponse[] = []
|
||||
) =>
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<BrowserRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalHeader
|
||||
proposal={proposal}
|
||||
isListItem={isListItem}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</BrowserRouter>
|
||||
</AppStateProvider>
|
||||
);
|
||||
|
||||
describe('Proposal header', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Renders New market proposal', () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = true;
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
@@ -48,6 +93,9 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'tGBP settled future.'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
|
||||
'PARENT_CODE'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
@@ -317,22 +365,6 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Open');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - majority not reached', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Declined');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Rejected', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
@@ -346,4 +378,32 @@ describe('Proposal header', () => {
|
||||
);
|
||||
expect(screen.getByTestId('proposal-status')).toHaveTextContent('Rejected');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted against', async () => {
|
||||
const proposal = generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
renderComponent(proposal, true, [
|
||||
// @ts-ignore generateProposal always creates an id
|
||||
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_NO),
|
||||
]);
|
||||
expect(await screen.findByTestId('user-voted-no')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted for', async () => {
|
||||
const proposal = generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
renderComponent(proposal, true, [
|
||||
// @ts-ignore generateProposal always creates an id
|
||||
createUserVoteQueryMock(proposal.id, VoteValue.VALUE_YES),
|
||||
]);
|
||||
expect(await screen.findByTestId('user-voted-yes')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+66
-14
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { shorten } from '@vegaprotocol/utils';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -8,15 +8,25 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import { ProposalVotingStatus } from '../proposal-voting-status';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import Routes from '../../../routes';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
isListItem = true,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
isListItem?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { voteState } = useUserVote(proposal?.id);
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
let details: ReactNode;
|
||||
@@ -33,6 +43,9 @@ export const ProposalHeader = ({
|
||||
fallbackTitle = t('NewMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<SuccessorCode proposalId={proposal?.id} />
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
@@ -119,6 +132,35 @@ export const ProposalHeader = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4 mb-6 text-sm">
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(`${proposalType}`)}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{(voteState === 'Yes' || voteState === 'No') && (
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
data-testid={`user-voted-${voteState.toLowerCase()}`}
|
||||
>
|
||||
<div className="text-vega-green" data-testid="you-voted-icon">
|
||||
<VegaIcon name={VegaIconNames.VOTE} size={24} />
|
||||
</div>
|
||||
<div>
|
||||
{t('voted')}{' '}
|
||||
<span className="uppercase">{t(`voteState_${voteState}`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div data-testid="proposal-status">
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-testid="proposal-title">
|
||||
{isListItem ? (
|
||||
<header>
|
||||
@@ -133,23 +175,33 @@ export const ProposalHeader = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(`${proposalType}`)}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
|
||||
<div data-testid="proposal-status">
|
||||
<CurrentProposalState proposal={proposal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details && (
|
||||
<div data-testid="proposal-details" className="break-words my-10">
|
||||
<div
|
||||
data-testid="proposal-details"
|
||||
className="break-words mb-6 text-vega-light-200"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SuccessorCode = ({ proposalId }: { proposalId?: string | null }) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
|
||||
return successor.parentMarketId || successor.code ? (
|
||||
<span className="block" data-testid="proposal-successor-info">
|
||||
{t('Successor market to')}:{' '}
|
||||
<Link
|
||||
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{successor.code || successor.parentMarketId}
|
||||
</Link>
|
||||
</span>
|
||||
) : null;
|
||||
};
|
||||
|
||||
+4
-4
@@ -7,10 +7,10 @@ export type ProposalInfoLabelVariant =
|
||||
| 'tertiary'
|
||||
| 'highlight';
|
||||
|
||||
const base = 'rounded-md px-2 py-1 font-alpha';
|
||||
const primary = 'bg-vega-light-150 text-black';
|
||||
const secondary = 'bg-vega-dark-200 text-white';
|
||||
const tertiary = 'bg-vega-dark-150 text-white';
|
||||
const base = 'rounded-full px-3 py-1 font-alpha';
|
||||
const primary = 'bg-vega-green text-black';
|
||||
const secondary = 'bg-vega-dark-200 text-vega-light-200';
|
||||
const tertiary = 'bg-vega-dark-150 text-vega-light-200';
|
||||
const highlight = 'bg-vega-yellow text-black';
|
||||
|
||||
const getClassname = (variant: ProposalInfoLabelVariant) => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './proposal-market-changes';
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import {
|
||||
ProposalMarketChanges,
|
||||
applyImmutableKeysFromEarlierVersion,
|
||||
} from './proposal-market-changes';
|
||||
import type { JsonValue } from '../../../../components/json-diff';
|
||||
|
||||
describe('applyImmutableKeysFromEarlierVersion', () => {
|
||||
it('returns an empty object if any argument is not an object or null', () => {
|
||||
const earlierVersion: JsonValue = null;
|
||||
const updatedVersion: JsonValue = null;
|
||||
expect(
|
||||
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
it('overrides updatedVersion with values from earlierVersion for immutable keys', () => {
|
||||
const earlierVersion: JsonValue = {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 3,
|
||||
instrument: {
|
||||
name: 'Instrument1',
|
||||
future: {
|
||||
settlementAsset: 'Asset1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updatedVersion: JsonValue = {
|
||||
decimalPlaces: 3, // should be overridden by 2
|
||||
positionDecimalPlaces: 4, // should be overridden by 3
|
||||
instrument: {
|
||||
name: 'Instrument2', // should be overridden by 'Instrument1'
|
||||
future: {
|
||||
settlementAsset: 'Asset2', // should be overridden by 'Asset1'
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expected: JsonValue = {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 3,
|
||||
instrument: {
|
||||
name: 'Instrument1',
|
||||
future: {
|
||||
settlementAsset: 'Asset1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
applyImmutableKeysFromEarlierVersion(earlierVersion, updatedVersion)
|
||||
).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProposalMarketChanges', () => {
|
||||
it('renders correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
);
|
||||
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is not visible when showChanges is false', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
);
|
||||
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is visible when showChanges is true', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
|
||||
expect(getByTestId('json-diff')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import { JsonDiff } from '../../../../components/json-diff';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { JsonValue } from '../../../../components/json-diff';
|
||||
|
||||
const immutableKeys = [
|
||||
'decimalPlaces',
|
||||
'positionDecimalPlaces',
|
||||
'instrument.name',
|
||||
'instrument.future.settlementAsset',
|
||||
];
|
||||
|
||||
export const applyImmutableKeysFromEarlierVersion = (
|
||||
earlierVersion: JsonValue,
|
||||
updatedVersion: JsonValue
|
||||
) => {
|
||||
if (
|
||||
typeof earlierVersion !== 'object' ||
|
||||
earlierVersion === null ||
|
||||
typeof updatedVersion !== 'object' ||
|
||||
updatedVersion === null
|
||||
) {
|
||||
// If either version is not an object or is null, return null or throw an error
|
||||
return {};
|
||||
}
|
||||
|
||||
const updatedVersionCopy = cloneDeep(updatedVersion);
|
||||
|
||||
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
|
||||
immutableKeys.forEach((key) => {
|
||||
set(updatedVersionCopy, key, get(earlierVersion, key));
|
||||
});
|
||||
|
||||
return updatedVersionCopy;
|
||||
};
|
||||
|
||||
interface ProposalMarketChangesProps {
|
||||
originalProposal: JsonValue;
|
||||
latestEnactedProposal: JsonValue | undefined;
|
||||
updatedProposal: JsonValue;
|
||||
}
|
||||
|
||||
export const ProposalMarketChanges = ({
|
||||
originalProposal,
|
||||
latestEnactedProposal,
|
||||
updatedProposal,
|
||||
}: ProposalMarketChangesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showChanges, setShowChanges] = useState(false);
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-market-changes">
|
||||
<CollapsibleToggle
|
||||
toggleState={showChanges}
|
||||
setToggleState={setShowChanges}
|
||||
dataTestId={'proposal-market-changes-toggle'}
|
||||
>
|
||||
<SubHeading title={t('updatesToMarket')} />
|
||||
</CollapsibleToggle>
|
||||
|
||||
{showChanges && (
|
||||
<div className="mb-6">
|
||||
<JsonDiff
|
||||
left={latestEnactedProposal || originalProposal}
|
||||
right={
|
||||
latestEnactedProposal
|
||||
? applyImmutableKeysFromEarlierVersion(
|
||||
latestEnactedProposal,
|
||||
updatedProposal
|
||||
)
|
||||
: applyImmutableKeysFromEarlierVersion(
|
||||
originalProposal,
|
||||
updatedProposal
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -200,7 +200,7 @@ export const ProposalMarketData = ({
|
||||
title={marketData.tradableInstrument.instrument.code}
|
||||
open={isOpen}
|
||||
onChange={(isOpen) => (isOpen ? open() : close())}
|
||||
size="medium"
|
||||
size="large"
|
||||
dataTestId="market-json-dialog"
|
||||
>
|
||||
<CopyWithTooltip text={JSON.stringify(marketData)}>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './proposal-voting-status';
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
lastWeek,
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
nextWeek,
|
||||
mockNetworkParams,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { ProposalVotingStatus } from './proposal-voting-status';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
generateYesVotes,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
import type { AppState } from '../../../../contexts/app-state/app-state-context';
|
||||
|
||||
const mockTotalSupply = new BigNumber(100);
|
||||
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
const mockAppState: AppState = {
|
||||
totalAssociated: new BigNumber('50063005'),
|
||||
decimals: 18,
|
||||
totalSupply: mockTotalSupply,
|
||||
vegaWalletManageOverlay: false,
|
||||
transactionOverlay: false,
|
||||
bannerMessage: '',
|
||||
disconnectNotice: false,
|
||||
};
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
appState: mockAppState,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||
) =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalVotingStatus
|
||||
proposal={proposal}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('ProposalVotingStatus', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Renders majority reached', () => {
|
||||
const yesVotes = 100;
|
||||
const noVotes = 0;
|
||||
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('majority-reached')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders majority not reached', () => {
|
||||
const yesVotes = 20;
|
||||
const noVotes = 80;
|
||||
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation reached', () => {
|
||||
const yesVotes = 1000;
|
||||
const noVotes = 0;
|
||||
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('participation-reached')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation not reached', () => {
|
||||
const yesVotes = 0;
|
||||
const noVotes = 0;
|
||||
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
__typename: 'ProposalVotes',
|
||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { BigNumber } from '../../../../lib/bignumber';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
const statusClasses = (reached: boolean) =>
|
||||
classNames('flex items-center gap-2 px-4 py-2 rounded-md', {
|
||||
'bg-vega-green-700': reached,
|
||||
'bg-vega-red-700': !reached,
|
||||
});
|
||||
|
||||
const MajorityStatus = ({
|
||||
reached,
|
||||
requiredMajority,
|
||||
}: {
|
||||
reached: boolean;
|
||||
requiredMajority: string | null | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={statusClasses(reached)}
|
||||
data-testid="proposal-majority-status"
|
||||
>
|
||||
{reached ? <Icon name="tick" /> : <Icon name="cross" />}
|
||||
{reached ? (
|
||||
<div data-testid="majority-reached">
|
||||
{requiredMajority ? (
|
||||
<>
|
||||
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
|
||||
{t('majorityVotedForProposal')}
|
||||
</>
|
||||
) : (
|
||||
t('requiredMajorityVotedForProposal')
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="majority-not-reached">
|
||||
{requiredMajority ? (
|
||||
<>
|
||||
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
|
||||
{t('majorityNotVotedForProposal')}
|
||||
</>
|
||||
) : (
|
||||
t('requiredMajorityNotVotedForProposal')
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ParticipationStatus = ({ reached }: { reached: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={statusClasses(reached)}>
|
||||
{reached ? (
|
||||
<>
|
||||
<Icon name="tick" />
|
||||
<div data-testid="participation-reached">
|
||||
{t('minParticipationReached')}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="cross" />
|
||||
<div data-testid="participation-not-reached">
|
||||
{t('minParticipationNotReached')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProposalVotingStatus = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { majorityMet, majorityLPMet, participationMet, participationLPMet } =
|
||||
useVoteInformation({
|
||||
proposal,
|
||||
});
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
||||
|
||||
let requiredVotingMajority = null;
|
||||
let requiredVotingMajorityLP = null;
|
||||
|
||||
if (networkParams) {
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'NewMarket':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_market_requiredMajority;
|
||||
break;
|
||||
case 'UpdateMarket':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_updateMarket_requiredMajority;
|
||||
requiredVotingMajorityLP =
|
||||
networkParams.governance_proposal_updateMarket_requiredMajorityLP;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_asset_requiredMajority;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_updateAsset_requiredMajority;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_updateNetParam_requiredMajority;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
requiredVotingMajority =
|
||||
networkParams.governance_proposal_freeform_requiredMajority;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdateMarket) {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<p>{t('Token vote')}</p>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-4 items-center"
|
||||
data-testid="token-vote-statuses"
|
||||
>
|
||||
<MajorityStatus
|
||||
reached={majorityMet}
|
||||
requiredMajority={requiredVotingMajority}
|
||||
/>{' '}
|
||||
<ParticipationStatus reached={participationMet} />
|
||||
</div>
|
||||
|
||||
<p className="mt-4">{t('Liquidity provider vote')}</p>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-4 items-center"
|
||||
data-testid="lp-vote-statuses"
|
||||
>
|
||||
<MajorityStatus
|
||||
reached={majorityLPMet}
|
||||
requiredMajority={requiredVotingMajorityLP}
|
||||
/>{' '}
|
||||
<ParticipationStatus reached={participationLPMet} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 items-center mb-6">
|
||||
<MajorityStatus
|
||||
reached={majorityMet}
|
||||
requiredMajority={requiredVotingMajority}
|
||||
/>{' '}
|
||||
<ParticipationStatus reached={participationMet} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { Proposal } from './proposal';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { mockNetworkParams } from '../../test-helpers/mocks';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
...jest.requireActual('@vegaprotocol/network-parameters'),
|
||||
@@ -46,6 +47,7 @@ const renderComponent = (proposal: ProposalQuery['proposal']) => {
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal as ProposalQuery['proposal']}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { AsyncRenderer, Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { Icon, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalDescription } from '../proposal-description';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
@@ -21,6 +17,8 @@ import type { MarketInfoWithData } from '@vegaprotocol/markets';
|
||||
import type { AssetQuery } from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
|
||||
export enum ProposalType {
|
||||
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
|
||||
@@ -32,28 +30,27 @@ export enum ProposalType {
|
||||
}
|
||||
export interface ProposalProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
newMarketData?: MarketInfoWithData | null;
|
||||
assetData?: AssetQuery | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restData: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
originalMarketProposalRestData?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mostRecentlyEnactedAssociatedMarketProposal?: any;
|
||||
}
|
||||
|
||||
export const Proposal = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
restData,
|
||||
newMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
}: ProposalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { params, loading, error } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
|
||||
NetworkParams.governance_proposal_asset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
|
||||
NetworkParams.governance_proposal_freeform_minVoterBalance,
|
||||
NetworkParams.spam_protection_voting_min_tokens,
|
||||
]);
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
@@ -79,122 +76,140 @@ export const Proposal = ({
|
||||
let minVoterBalance = null;
|
||||
let proposalType = null;
|
||||
|
||||
if (params) {
|
||||
if (networkParams) {
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'NewMarket':
|
||||
minVoterBalance = params.governance_proposal_market_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NEW_MARKET;
|
||||
break;
|
||||
case 'UpdateMarket':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateMarket_minVoterBalance;
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance = params.governance_proposal_asset_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NEW_ASSET;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateAsset_minVoterBalance;
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_UPDATE_ASSET;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
params.governance_proposal_updateNetParam_minVoterBalance;
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance = params.governance_proposal_freeform_minVoterBalance;
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
proposalType = ProposalType.PROPOSAL_FREEFORM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon name={'chevron-left'} />
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1 mb-6">
|
||||
<Icon name={'chevron-left'} />
|
||||
|
||||
{proposal.state === ProposalState.STATE_REJECTED ? (
|
||||
<div data-testid="rejected-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('RejectedProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="all-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS}>
|
||||
{t('AllProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProposalHeader proposal={proposal} isListItem={false} />
|
||||
|
||||
<div id="details">
|
||||
<div className="my-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
{proposal.state === ProposalState.STATE_REJECTED ? (
|
||||
<div data-testid="rejected-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('RejectedProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div data-testid="all-proposals-link">
|
||||
<Link className="underline" to={Routes.PROPOSALS}>
|
||||
{t('AllProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ProposalHeader
|
||||
proposal={proposal}
|
||||
isListItem={false}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={
|
||||
proposal.terms.change.source.withdrawThreshold
|
||||
<div id="details">
|
||||
<div className="my-10">
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalDescription description={proposal.rationale.description} />
|
||||
</div>
|
||||
|
||||
{newMarketData && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData marketData={newMarketData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateMarket' && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketChanges
|
||||
originalProposal={
|
||||
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
|
||||
?.changes || {}
|
||||
}
|
||||
latestEnactedProposal={
|
||||
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
|
||||
?.terms?.updateMarket?.changes || {}
|
||||
}
|
||||
updatedProposal={
|
||||
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
|
||||
}
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalDescription description={proposal.rationale.description} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{newMarketData && (
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData marketData={newMarketData} />
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={restData?.data?.proposal} />
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<ProposalJson proposal={restData?.data?.proposal} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="voting">
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
params?.spam_protection_voting_min_tokens
|
||||
}
|
||||
/>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable
|
||||
<div id="voting">
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<VoteDetails
|
||||
proposal={proposal}
|
||||
proposalType={proposalType}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams?.spam_protection_voting_min_tokens
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</RoundedWrapper>
|
||||
</div>
|
||||
</section>
|
||||
</AsyncRenderer>
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-127
@@ -5,11 +5,7 @@ import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { format } from 'date-fns';
|
||||
import {
|
||||
ProposalRejectionReason,
|
||||
ProposalState,
|
||||
VoteValue,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
@@ -18,7 +14,6 @@ import {
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
mockPubkey,
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
fiveMinutes,
|
||||
@@ -28,39 +23,6 @@ import {
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { UserVoteDocument } from '../vote-details/__generated__/Vote';
|
||||
import faker from 'faker';
|
||||
|
||||
const createUserVoteQueryMock = (
|
||||
proposalId: string | undefined | null,
|
||||
value: VoteValue
|
||||
) => ({
|
||||
request: {
|
||||
query: UserVoteDocument,
|
||||
variables: {
|
||||
partyId: mockPubkey.publicKey,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
party: {
|
||||
votesConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
proposalId,
|
||||
vote: {
|
||||
value,
|
||||
datetime: faker.date.past().toISOString(),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
@@ -131,7 +93,7 @@ describe('Proposals list item details', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - set to pass by LP vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -153,11 +115,11 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Set to pass by LP vote'
|
||||
'Currently expected to pass by LP vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - set to pass by token vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -179,11 +141,11 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Set to pass by token vote'
|
||||
'Currently expected to pass by token vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - set to fail', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -204,7 +166,9 @@ describe('Proposals list item details', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to fail');
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to fail'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||
@@ -249,52 +213,6 @@ describe('Proposals list item details', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted for', async () => {
|
||||
const proposal = generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
renderComponent(proposal, [
|
||||
networkParamsQueryMock,
|
||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_YES),
|
||||
]);
|
||||
expect(await screen.findByText('You voted For')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - user voted against', async () => {
|
||||
const proposal = generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: nextWeek.toString(),
|
||||
},
|
||||
});
|
||||
renderComponent(proposal, [
|
||||
networkParamsQueryMock,
|
||||
createUserVoteQueryMock(proposal?.id, VoteValue.VALUE_NO),
|
||||
]);
|
||||
expect(await screen.findByText('You voted Against')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - participation not reached', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Participation not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - majority not reached', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
@@ -309,7 +227,7 @@ describe('Proposals list item details', () => {
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Majority not reached'
|
||||
'Currently expected to fail'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -327,42 +245,8 @@ describe('Proposals list item details', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent('Set to pass');
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - participation not reached', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(0),
|
||||
yes: generateYesVotes(0),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Participation not reached'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Declined - majority not reached', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_DECLINED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
votes: {
|
||||
no: generateNoVotes(1, 1000000000000000000),
|
||||
yes: generateYesVotes(1, 1000000000000000000),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Majority not reached'
|
||||
'Currently expected to pass'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+53
-106
@@ -1,9 +1,6 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import { StatusPass } from '../current-proposal-status/current-proposal-status';
|
||||
import { format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -14,138 +11,100 @@ import {
|
||||
import Routes from '../../../routes';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
const MajorityNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Majority')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const ParticipationNotReached = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
{t('Participation')} {t('not reached')}
|
||||
</>
|
||||
);
|
||||
};
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const state = proposal?.state;
|
||||
const {
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
majorityMet,
|
||||
participationMet,
|
||||
} = useVoteInformation({
|
||||
const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({
|
||||
proposal,
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { voteState } = useUserVote(proposal?.id);
|
||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
||||
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
|
||||
const updateMarketVotePassMethod = willPassByTokenVote
|
||||
? t('byTokenVote')
|
||||
: t('byLPVote');
|
||||
const nowToEnactmentInHours = differenceInHours(
|
||||
new Date(proposal?.terms.closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
||||
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
voteDetails = proposal?.terms.enactmentDatetime && (
|
||||
<>
|
||||
{format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
voteDetails =
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{proposal?.terms.enactmentDatetime &&
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
voteDetails = proposal?.terms.change.__typename !== 'NewFreeform' && (
|
||||
<>
|
||||
{t('toEnactOn')}{' '}
|
||||
{proposal?.terms.enactmentDatetime &&
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
)}
|
||||
</>
|
||||
);
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
voteDetails = (voteState === 'Yes' && (
|
||||
<>
|
||||
{t('youVoted')} {t('voteState_Yes')}
|
||||
</>
|
||||
)) ||
|
||||
(voteState === 'No' && (
|
||||
<>
|
||||
{t('youVoted')} {t('voteState_No')}
|
||||
</>
|
||||
)) || (
|
||||
<>
|
||||
{formatDistanceToNowStrict(
|
||||
new Date(proposal?.terms.closingDatetime)
|
||||
)}{' '}
|
||||
{t('left to vote')}
|
||||
</>
|
||||
);
|
||||
voteDetails = (
|
||||
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
|
||||
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
||||
{t('left to vote')}
|
||||
</span>
|
||||
);
|
||||
voteStatus =
|
||||
(isUpdateMarket &&
|
||||
(updateMarketWillPass ? (
|
||||
<>
|
||||
{t('Set to')}{' '}
|
||||
<StatusPass>
|
||||
{t('pass')} {updateMarketVotePassMethod}
|
||||
</StatusPass>
|
||||
{t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Set to')} {t('fail')}
|
||||
{t('currentlySetTo')} {t('fail')}
|
||||
</>
|
||||
))) ||
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />) ||
|
||||
(willPassByTokenVote ? (
|
||||
<>
|
||||
{t('Set to')} {t('pass')}
|
||||
{t('currentlySetTo')} {t('pass')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Set to')} {t('fail')}
|
||||
{t('currentlySetTo')} {t('fail')}
|
||||
</>
|
||||
));
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
voteStatus =
|
||||
(!participationMet && <ParticipationNotReached />) ||
|
||||
(!majorityMet && <MajorityNotReached />);
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
voteStatus = proposal?.rejectionReason && (
|
||||
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
|
||||
@@ -155,29 +114,17 @@ export const ProposalsListItemDetails = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[1fr_auto] mt-4 items-start gap-2 text-sm">
|
||||
{voteDetails && (
|
||||
<div
|
||||
className="col-start-1 row-start-2 text-vega-light-300"
|
||||
data-testid="vote-details"
|
||||
>
|
||||
{voteDetails}
|
||||
</div>
|
||||
)}
|
||||
{voteStatus && (
|
||||
<div
|
||||
className="col-start-2 row-start-1 justify-self-end"
|
||||
data-testid="vote-status"
|
||||
>
|
||||
{voteStatus}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 items-start text-sm">
|
||||
<div className="flex items-center gap-2 text-vega-light-300 mb-2">
|
||||
{voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
|
||||
{voteDetails && voteStatus && <span>·</span>}
|
||||
{voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
|
||||
</div>
|
||||
|
||||
{proposal?.id && (
|
||||
<div className="col-start-2 row-start-2 justify-self-end">
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('View')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+8
-3
@@ -3,18 +3,23 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
|
||||
networkParams: Partial<NetworkParamsResult> | null;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id) return null;
|
||||
export const ProposalsListItem = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
}: ProposalsListItemProps) => {
|
||||
if (!proposal || !proposal.id || !networkParams) return null;
|
||||
|
||||
return (
|
||||
<li id={proposal.id} data-testid="proposals-list-item">
|
||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||
<ProposalHeader proposal={proposal} />
|
||||
<ProposalHeader proposal={proposal} networkParams={networkParams} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
|
||||
+45
-21
@@ -88,31 +88,39 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
describe('Proposals list', () => {
|
||||
it('Render a page title and link to the make proposal form', () => {
|
||||
it('Render a page title and link to the make proposal form', async () => {
|
||||
render(renderComponent([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByText('Proposals')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('new-proposal-link')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will hide filter if no proposals', () => {
|
||||
it('Will hide filter if no proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('proposals-list-filter')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will show filter if there are proposals', () => {
|
||||
it('Will show filter if there are proposals', async () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.queryByTestId('proposals-list-filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Will render a link to rejected proposals', () => {
|
||||
it('Will render a link to rejected proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByText('See rejected proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Places proposals correctly in open or closed lists', () => {
|
||||
it('Places proposals correctly in open or closed lists', async () => {
|
||||
render(
|
||||
renderComponent([
|
||||
openProposalClosesNextWeek,
|
||||
@@ -121,6 +129,7 @@ describe('Proposals list', () => {
|
||||
failedProposalClosedLastMonth,
|
||||
])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
const openProposals = within(screen.getByTestId('open-proposals'));
|
||||
const closedProposals = within(screen.getByTestId('closed-proposals'));
|
||||
expect(openProposals.getAllByTestId('proposals-list-item').length).toBe(2);
|
||||
@@ -129,42 +138,47 @@ describe('Proposals list', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Displays info on no proposals', () => {
|
||||
it('Displays info on no proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays info on no open proposals if only closed are present', () => {
|
||||
it('Displays info on no open proposals if only closed are present', async () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.queryByTestId('open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-open-proposals')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('closed-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('no-closed-proposals')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays info on no closed proposals if only open are present', () => {
|
||||
it('Displays info on no closed proposals if only open are present', async () => {
|
||||
render(renderComponent([openProposalClosesNextWeek]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByTestId('open-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('no-open-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('closed-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Opens filter form when button is clicked', () => {
|
||||
it('Opens filter form when button is clicked', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
expect(screen.getByTestId('proposals-list-filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - party id', () => {
|
||||
it('Filters list by text - party id', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'bvcx' },
|
||||
@@ -174,10 +188,11 @@ describe('Proposals list', () => {
|
||||
expect(container.querySelector('#proposal1')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - proposal id', () => {
|
||||
it('Filters list by text - proposal id', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'proposal1' },
|
||||
@@ -187,10 +202,11 @@ describe('Proposals list', () => {
|
||||
expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Filters list by text - check for substring matching', () => {
|
||||
it('Filters list by text - check for substring matching', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'osal1' },
|
||||
@@ -200,10 +216,11 @@ describe('Proposals list', () => {
|
||||
expect(container.querySelector('#proposal2')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('When filter is used, clear button is visible', () => {
|
||||
it('When filter is used, clear button is visible', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'test' },
|
||||
@@ -211,10 +228,11 @@ describe('Proposals list', () => {
|
||||
expect(screen.getByTestId('clear-filter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('When clear filter button is used, input is cleared', () => {
|
||||
it('When clear filter button is used, input is cleared', async () => {
|
||||
render(
|
||||
renderComponent([openProposalClosesNextMonth, openProposalClosesNextWeek])
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'test' },
|
||||
@@ -225,37 +243,41 @@ describe('Proposals list', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', () => {
|
||||
it('Displays a toggle for closed proposals if there are both closed governance proposals and closed upgrade proposals', async () => {
|
||||
render(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(screen.getByTestId('toggle-closed-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', () => {
|
||||
it('Does not display a toggle for closed proposals if there are only closed upgrade proposals', async () => {
|
||||
render(renderComponent([], [closedProtocolUpgradeProposal]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('toggle-closed-proposals')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Does not display a toggle for closed proposals if there are only closed governance proposals', () => {
|
||||
it('Does not display a toggle for closed proposals if there are only closed governance proposals', async () => {
|
||||
render(renderComponent([enactedProposalClosedLastWeek]));
|
||||
await screen.findByTestId('proposals-list');
|
||||
expect(
|
||||
screen.queryByTestId('toggle-closed-proposals')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Does not display a toggle for closed proposals if the proposal filter is engaged', () => {
|
||||
it('Does not display a toggle for closed proposals if the proposal filter is engaged', async () => {
|
||||
render(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
await screen.findByTestId('proposal-filter-toggle');
|
||||
fireEvent.click(screen.getByTestId('proposal-filter-toggle'));
|
||||
fireEvent.change(screen.getByTestId('filter-input'), {
|
||||
target: { value: 'test' },
|
||||
@@ -265,7 +287,7 @@ describe('Proposals list', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays closed governance proposals by default due to default for the toggle', () => {
|
||||
it('Displays closed governance proposals by default due to default for the toggle', async () => {
|
||||
render(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
@@ -273,17 +295,19 @@ describe('Proposals list', () => {
|
||||
)
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('closed-governance-proposals')
|
||||
await screen.findByTestId('closed-governance-proposals')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Displays closed upgrade proposals when the toggle is clicked', () => {
|
||||
it('Displays closed upgrade proposals when the toggle is clicked', async () => {
|
||||
render(
|
||||
renderComponent(
|
||||
[enactedProposalClosedLastWeek],
|
||||
[closedProtocolUpgradeProposal]
|
||||
)
|
||||
);
|
||||
|
||||
await screen.findByTestId('toggle-closed-proposals');
|
||||
fireEvent.click(screen.getByText('Network upgrades'));
|
||||
expect(screen.getByTestId('closed-upgrade-proposals')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
+180
-146
@@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import Routes from '../../../routes';
|
||||
import {
|
||||
AsyncRenderer,
|
||||
Button,
|
||||
Toggle,
|
||||
VegaIcon,
|
||||
@@ -19,6 +20,10 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||
@@ -70,6 +75,20 @@ export const ProposalsList = ({
|
||||
lastBlockHeight,
|
||||
}: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
params: networkParams,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
]);
|
||||
|
||||
const [filterString, setFilterString] = useState('');
|
||||
const [closedProposalsView, setClosedProposalsView] =
|
||||
useState<ClosedProposalsViewOptions>(
|
||||
@@ -134,166 +153,181 @@ export const ProposalsList = ({
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid xs:grid-cols-2 items-center">
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<div data-testid="proposals-list">
|
||||
<div className="grid xs:grid-cols-2 items-center">
|
||||
<Heading
|
||||
centerContent={false}
|
||||
marginBottom={false}
|
||||
title={t('pageTitleProposals')}
|
||||
/>
|
||||
|
||||
{DocsLinks && (
|
||||
<div className="xs:justify-self-end" data-testid="new-proposal-link">
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||
<Button variant="primary" size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{t('NewProposal')}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||
</div>
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
{DocsLinks && (
|
||||
<div
|
||||
className="xs:justify-self-end"
|
||||
data-testid="new-proposal-link"
|
||||
>
|
||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||
<Button variant="primary" size="sm">
|
||||
<div className="flex items-center gap-1">
|
||||
{t('NewProposal')}
|
||||
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
|
||||
</div>
|
||||
</Button>
|
||||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-8">
|
||||
{t(
|
||||
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
|
||||
)}{' '}
|
||||
<ExternalLink
|
||||
data-testid="proposal-documentation-link"
|
||||
href={ExternalLinks.GOVERNANCE_PAGE}
|
||||
className="text-white"
|
||||
>
|
||||
{t(`Find out more about Vega governance`)}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
|
||||
{proposals.length > 0 && (
|
||||
<ProposalsListFilter
|
||||
filterString={filterString}
|
||||
setFilterString={(value) => {
|
||||
setFilterString(value);
|
||||
if (value.length > 0) {
|
||||
// If the filter is engaged, ensure the user is viewing governance proposals,
|
||||
// as network upgrades do not have IDs to filter by and will be excluded.
|
||||
setClosedProposalsView(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mb-8">
|
||||
{t(
|
||||
`The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.`
|
||||
)}{' '}
|
||||
<ExternalLink
|
||||
data-testid="proposal-documentation-link"
|
||||
href={ExternalLinks.GOVERNANCE_PAGE}
|
||||
className="text-white"
|
||||
>
|
||||
{t(`Find out more about Vega governance`)}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
|
||||
<SubHeading title={t('openProposals')} />
|
||||
|
||||
{proposals.length > 0 && (
|
||||
<ProposalsListFilter
|
||||
filterString={filterString}
|
||||
setFilterString={(value) => {
|
||||
setFilterString(value);
|
||||
if (value.length > 0) {
|
||||
// If the filter is engaged, ensure the user is viewing governance proposals,
|
||||
// as network upgrades do not have IDs to filter by and will be excluded.
|
||||
setClosedProposalsView(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{sortedProposals.open.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.open.length > 0 ? (
|
||||
<ul data-testid="open-proposals">
|
||||
{filterString.length < 1 &&
|
||||
sortedProtocolUpgradeProposals.open.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
proposal={proposal}
|
||||
/>
|
||||
))}
|
||||
|
||||
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
|
||||
<SubHeading title={t('openProposals')} />
|
||||
|
||||
{sortedProposals.open.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.open.length > 0 ? (
|
||||
<ul data-testid="open-proposals">
|
||||
{filterString.length < 1 &&
|
||||
sortedProtocolUpgradeProposals.open.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-open-proposals">
|
||||
{t('noOpenProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-open-proposals">
|
||||
{t('noOpenProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="relative">
|
||||
<SubHeading title={t('closedProposals')} />
|
||||
{sortedProposals.closed.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
|
||||
<>
|
||||
{
|
||||
// We need both the closed proposals and closed protocol upgrade
|
||||
// proposals to be present for there to be a toggle. It also gets
|
||||
// hidden if the user has filtered the list, as the upgrade proposals
|
||||
// do not have the necessary fields for filtering.
|
||||
sortedProposals.closed.length > 0 &&
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 &&
|
||||
filterString.length < 1 && (
|
||||
<div
|
||||
className="grid w-full justify-end xl:-mt-12 pb-6"
|
||||
data-testid="toggle-closed-proposals"
|
||||
>
|
||||
<div className="w-[440px]">
|
||||
<Toggle
|
||||
name="closed-proposals-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkGovernance,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkUpgrades
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkUpgrades,
|
||||
},
|
||||
]}
|
||||
checkedValue={closedProposalsView}
|
||||
onChange={(e) =>
|
||||
setClosedProposalsView(
|
||||
e.target.value as ClosedProposalsViewOptions
|
||||
)
|
||||
}
|
||||
/>
|
||||
<section className="relative">
|
||||
<SubHeading title={t('closedProposals')} />
|
||||
{sortedProposals.closed.length > 0 ||
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 ? (
|
||||
<>
|
||||
{
|
||||
// We need both the closed proposals and closed protocol upgrade
|
||||
// proposals to be present for there to be a toggle. It also gets
|
||||
// hidden if the user has filtered the list, as the upgrade proposals
|
||||
// do not have the necessary fields for filtering.
|
||||
sortedProposals.closed.length > 0 &&
|
||||
sortedProtocolUpgradeProposals.closed.length > 0 &&
|
||||
filterString.length < 1 && (
|
||||
<div
|
||||
className="grid w-full justify-end xl:-mt-12 pb-6"
|
||||
data-testid="toggle-closed-proposals"
|
||||
>
|
||||
<div className="w-[440px]">
|
||||
<Toggle
|
||||
name="closed-proposals-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkGovernance
|
||||
),
|
||||
value:
|
||||
ClosedProposalsViewOptions.NetworkGovernance,
|
||||
},
|
||||
{
|
||||
label: t(
|
||||
ClosedProposalsViewOptions.NetworkUpgrades
|
||||
),
|
||||
value: ClosedProposalsViewOptions.NetworkUpgrades,
|
||||
},
|
||||
]}
|
||||
checkedValue={closedProposalsView}
|
||||
onChange={(e) =>
|
||||
setClosedProposalsView(
|
||||
e.target.value as ClosedProposalsViewOptions
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
<ul data-testid="closed-proposals">
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkUpgrades && (
|
||||
<div data-testid="closed-upgrade-proposals">
|
||||
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
proposal={proposal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkGovernance && (
|
||||
<div data-testid="closed-governance-proposals">
|
||||
{sortedProposals.closed
|
||||
.filter(filterPredicate)
|
||||
.map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
<ul data-testid="closed-proposals">
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkUpgrades && (
|
||||
<div data-testid="closed-upgrade-proposals">
|
||||
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
|
||||
<ProtocolUpgradeProposalsListItem
|
||||
key={proposal.upgradeBlockHeight}
|
||||
proposal={proposal}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-closed-proposals">
|
||||
{t('noClosedProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('seeRejectedProposals')}
|
||||
</Link>
|
||||
</>
|
||||
{closedProposalsView ===
|
||||
ClosedProposalsViewOptions.NetworkGovernance && (
|
||||
<div data-testid="closed-governance-proposals">
|
||||
{sortedProposals.closed
|
||||
.filter(filterPredicate)
|
||||
.map((proposal) => (
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p className="mb-0" data-testid="no-closed-proposals">
|
||||
{t('noClosedProposals')}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
|
||||
{t('seeRejectedProposals')}
|
||||
</Link>
|
||||
</div>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
+24
-10
@@ -5,7 +5,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { RejectedProposalsList } from './rejected-proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
@@ -55,24 +55,38 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
describe('Rejected proposals list', () => {
|
||||
it('Renders a list of proposals', () => {
|
||||
it('Renders a list of proposals', async () => {
|
||||
render(
|
||||
renderComponent([
|
||||
rejectedProposalClosedLastMonth,
|
||||
rejectedProposalClosesNextWeek,
|
||||
])
|
||||
);
|
||||
const rejectedProposals = within(screen.getByTestId('rejected-proposals'));
|
||||
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(rejectedProposalsItems).toHaveLength(2);
|
||||
|
||||
await waitFor(() => {
|
||||
const rejectedProposals = within(
|
||||
screen.getByTestId('rejected-proposals')
|
||||
);
|
||||
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(rejectedProposalsItems).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('Displays text when there are no proposals', () => {
|
||||
it('Displays text when there are no proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('rejected-proposals')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+29
-3
@@ -1,10 +1,15 @@
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { ProposalsListItem } from '../proposals-list-item';
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
|
||||
@@ -12,6 +17,19 @@ interface ProposalsListProps {
|
||||
|
||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
params: networkParams,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
]);
|
||||
const [filterString, setFilterString] = useState('');
|
||||
|
||||
const filterPredicate = (
|
||||
@@ -21,7 +39,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AsyncRenderer
|
||||
loading={networkParamsLoading}
|
||||
error={networkParamsError}
|
||||
data={networkParams}
|
||||
>
|
||||
<Heading title={t('pageTitleRejectedProposals')} />
|
||||
<ProposalsListFilter
|
||||
filterString={filterString}
|
||||
@@ -31,7 +53,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
{proposals.length > 0 ? (
|
||||
<ul data-testid="rejected-proposals">
|
||||
{proposals.filter(filterPredicate).map((proposal) => (
|
||||
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||
<ProposalsListItem
|
||||
key={proposal?.id}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
@@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
+36
-8
@@ -4,16 +4,20 @@ import { render } from '@testing-library/react';
|
||||
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||
import { ProtocolUpgradeProposalDetailInfo } from './protocol-upgrade-proposal-detail-info';
|
||||
|
||||
const renderComponent = () =>
|
||||
const PROPOSAL = {
|
||||
vegaReleaseTag: 'v0.1.234',
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||
upgradeBlockHeight: '12345',
|
||||
approvers: [],
|
||||
};
|
||||
|
||||
const renderComponent = (lastBlockHeight?: string, time?: string) =>
|
||||
render(
|
||||
<ProtocolUpgradeProposalDetailInfo
|
||||
proposal={{
|
||||
vegaReleaseTag: 'v0.1.234',
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||
upgradeBlockHeight: '12345',
|
||||
approvers: [],
|
||||
}}
|
||||
proposal={PROPOSAL}
|
||||
lastBlockHeight={lastBlockHeight}
|
||||
time={time}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -43,4 +47,28 @@ describe('ProtocolUpgradeProposalDetailInfo', () => {
|
||||
'v0.1.234'
|
||||
);
|
||||
});
|
||||
|
||||
it('should display estimated time if proposal is still pending', () => {
|
||||
const { getByTestId } = renderComponent('12', 'time');
|
||||
expect(getByTestId('protocol-upgrade-time-label')).toHaveTextContent(
|
||||
'Estimated time to upgrade'
|
||||
);
|
||||
expect(getByTestId('protocol-upgrade-time')).toHaveTextContent('time');
|
||||
});
|
||||
|
||||
it('should display upgraded at if proposal is done', () => {
|
||||
const { getByTestId } = renderComponent('123456', 'time');
|
||||
expect(getByTestId('protocol-upgrade-time-label')).toHaveTextContent(
|
||||
'Upgraded at'
|
||||
);
|
||||
expect(getByTestId('protocol-upgrade-time')).toHaveTextContent('time');
|
||||
});
|
||||
|
||||
it('should not display time if none provided', () => {
|
||||
const { queryByTestId } = renderComponent('123456', undefined);
|
||||
expect(
|
||||
queryByTestId('protocol-upgrade-time-label')
|
||||
).not.toBeInTheDocument();
|
||||
expect(queryByTestId('protocol-upgrade-time')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+30
-3
@@ -5,19 +5,33 @@ import {
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface ProtocolUpgradeProposalDetailInfoProps {
|
||||
proposal: ProtocolUpgradeProposalFieldsFragment;
|
||||
lastBlockHeight?: string;
|
||||
time?: string | ReactNode;
|
||||
}
|
||||
|
||||
export const ProtocolUpgradeProposalDetailInfo = ({
|
||||
proposal,
|
||||
lastBlockHeight,
|
||||
time,
|
||||
}: ProtocolUpgradeProposalDetailInfoProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const shouldShowUpgradeTime =
|
||||
![
|
||||
Types.ProtocolUpgradeProposalStatus
|
||||
.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
|
||||
Types.ProtocolUpgradeProposalStatus
|
||||
.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
|
||||
].includes(proposal.status) && time;
|
||||
|
||||
const pending = Number(proposal.upgradeBlockHeight) > Number(lastBlockHeight);
|
||||
|
||||
return (
|
||||
<div className="mb-10">
|
||||
<SubHeading title={t('proposal')} />
|
||||
@@ -29,13 +43,26 @@ export const ProtocolUpgradeProposalDetailInfo = ({
|
||||
<span data-testid="protocol-upgrade-block-height">
|
||||
{proposal.upgradeBlockHeight}{' '}
|
||||
{lastBlockHeight && (
|
||||
<>
|
||||
<span className="text-vega-light-300 dark:text-vega-dark-300">
|
||||
({t('currently')} {lastBlockHeight})
|
||||
</>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
|
||||
{shouldShowUpgradeTime && (
|
||||
<KeyValueTableRow>
|
||||
<span
|
||||
data-testid="protocol-upgrade-time-label"
|
||||
className="uppercase"
|
||||
>
|
||||
{pending ? t('Estimated time to upgrade') : t('Upgraded at')}
|
||||
</span>
|
||||
|
||||
<span data-testid="protocol-upgrade-time">{time}</span>
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
|
||||
<KeyValueTableRow>
|
||||
<span className="uppercase">{t('state')}</span>
|
||||
|
||||
|
||||
-37
@@ -20,43 +20,6 @@ const renderComponent = (proposal: ProtocolUpgradeProposalFieldsFragment) =>
|
||||
);
|
||||
|
||||
describe('ProtocolUpgradeProposalsListItem', () => {
|
||||
it('renders the correct status icon for each proposal status', () => {
|
||||
const statuses = [
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-rejected',
|
||||
text: 'Declined by validators',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-pending',
|
||||
text: 'Waiting for validator votes',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-approved',
|
||||
text: 'Approved by validators',
|
||||
},
|
||||
{
|
||||
status:
|
||||
ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED,
|
||||
icon: 'protocol-upgrade-proposal-status-icon-unspecified',
|
||||
text: 'Unspecified',
|
||||
},
|
||||
];
|
||||
|
||||
statuses.forEach(({ status, icon, text }) => {
|
||||
renderComponent({ ...proposal, status });
|
||||
const statusIcon = screen.getByTestId(icon);
|
||||
const textContent = screen.getByText(text);
|
||||
expect(statusIcon).toBeInTheDocument();
|
||||
expect(textContent).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the correct Vega release tag', () => {
|
||||
renderComponent(proposal);
|
||||
const releaseTag = screen.getByTestId(
|
||||
|
||||
+28
-73
@@ -1,17 +1,10 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Lozenge,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { stripFullStops } from '@vegaprotocol/utils';
|
||||
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import Routes from '../../../routes';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
|
||||
interface ProtocolProposalsListItemProps {
|
||||
@@ -24,45 +17,21 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
const { t } = useTranslation();
|
||||
if (!proposal || !proposal.upgradeBlockHeight) return null;
|
||||
|
||||
let proposalStatusIcon: ReactNode;
|
||||
|
||||
switch (proposal.status) {
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_REJECTED:
|
||||
proposalStatusIcon = (
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-rejected">
|
||||
<Icon name={'cross'} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_PENDING:
|
||||
proposalStatusIcon = (
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-pending">
|
||||
<Icon name={'time'} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED:
|
||||
proposalStatusIcon = (
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-approved">
|
||||
<Icon name={'tick'} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ProtocolUpgradeProposalStatus.PROTOCOL_UPGRADE_PROPOSAL_STATUS_UNSPECIFIED:
|
||||
proposalStatusIcon = (
|
||||
<span data-testid="protocol-upgrade-proposal-status-icon-unspecified">
|
||||
<Icon name={'disable'} />
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
id={proposal.upgradeBlockHeight}
|
||||
data-testid="protocol-upgrade-proposals-list-item"
|
||||
>
|
||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||
<div
|
||||
data-testid="protocol-upgrade-proposal-type"
|
||||
className="flex items-center gap-2 mb-4 text-sm"
|
||||
>
|
||||
<ProposalInfoLabel variant="highlight">
|
||||
{t('networkUpgrade')}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="protocol-upgrade-proposal-title"
|
||||
className="text-sm mb-2"
|
||||
@@ -70,30 +39,13 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
<SubHeading title={`Vega release ${proposal.vegaReleaseTag}`} />
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<div className="flex gap-2">
|
||||
<div
|
||||
data-testid="protocol-upgrade-proposal-type"
|
||||
className="flex items-center gap-2 mb-4"
|
||||
>
|
||||
<ProposalInfoLabel variant="highlight">
|
||||
{t('networkUpgrade')}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
|
||||
<div data-testid="protocol-upgrade-proposal-status">
|
||||
<ProposalInfoLabel>
|
||||
{t(`${proposal.status}`)} {proposalStatusIcon}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-vega-light-200">
|
||||
<div
|
||||
data-testid="protocol-upgrade-proposal-release-tag"
|
||||
className="mb-2"
|
||||
>
|
||||
<span>{t('vegaReleaseTag')}:</span>{' '}
|
||||
<Lozenge>{proposal.vegaReleaseTag}</Lozenge>
|
||||
<span>{proposal.vegaReleaseTag}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -101,21 +53,24 @@ export const ProtocolUpgradeProposalsListItem = ({
|
||||
className="mb-2"
|
||||
>
|
||||
<span>{t('upgradeBlockHeight')}:</span>{' '}
|
||||
<Lozenge>{proposal.upgradeBlockHeight}</Lozenge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 mt-3">
|
||||
<div className="justify-self-end">
|
||||
<Link
|
||||
to={`${Routes.PROTOCOL_UPGRADES}/${stripFullStops(
|
||||
proposal.vegaReleaseTag
|
||||
)}`}
|
||||
>
|
||||
<Button data-testid="view-proposal-btn">{t('View')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<span>{proposal.upgradeBlockHeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-sm text-vega-light-300 mb-4"
|
||||
data-testid="protocol-upgrade-proposal-status"
|
||||
>
|
||||
{t(`${proposal.status}`)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`${Routes.PROTOCOL_UPGRADES}/${stripFullStops(
|
||||
proposal.vegaReleaseTag
|
||||
)}`}
|
||||
>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -24,8 +24,8 @@ interface VoteButtonsContainerProps {
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
className?: string;
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
|
||||
interface VoteDetailsProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
proposalType: ProposalType | null;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user