Compare commits

..
1321 changed files with 175592 additions and 43540 deletions
-1
View File
@@ -1 +0,0 @@
node_modules
+6 -11
View File
@@ -1,7 +1,7 @@
{ {
"root": true, "root": true,
"ignorePatterns": ["**/*"], "ignorePatterns": ["**/*"],
"plugins": ["@nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"], "plugins": ["@nrwl/nx", "eslint-plugin-unicorn", "jsx-a11y", "jest"],
"settings": { "settings": {
"jsx-a11y": { "jsx-a11y": {
"components": { "components": {
@@ -18,7 +18,7 @@
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:jsx-a11y/strict"], "extends": ["plugin:jsx-a11y/strict"],
"rules": { "rules": {
"@nx/enforce-module-boundaries": [ "@nrwl/nx/enforce-module-boundaries": [
"error", "error",
{ {
"enforceBuildableLibDependency": true, "enforceBuildableLibDependency": true,
@@ -56,7 +56,7 @@
}, },
{ {
"files": ["*.ts", "*.tsx"], "files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"], "extends": ["plugin:@nrwl/nx/typescript"],
"rules": { "rules": {
"@typescript-eslint/ban-ts-comment": [ "@typescript-eslint/ban-ts-comment": [
"error", "error",
@@ -80,19 +80,14 @@
}, },
{ {
"files": ["*.spec.ts", "*.spec.tsx"], "files": ["*.spec.ts", "*.spec.tsx"],
"extends": ["plugin:@nx/typescript", "plugin:jest/recommended"], "extends": ["plugin:@nrwl/nx/typescript", "plugin:jest/recommended"],
"rules": { "rules": {
"jest/consistent-test-it": [ "jest/consistent-test-it": ["error", { "fn": "it" }]
"error",
{
"fn": "it"
}
]
} }
}, },
{ {
"files": ["*.js", "*.jsx"], "files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"], "extends": ["plugin:@nrwl/nx/javascript"],
"rules": {} "rules": {}
} }
] ]
+3 -2
View File
@@ -14,7 +14,8 @@ What we need to achieve and who for
## Tasks ## Tasks
- [ ] - [ ] What do we need to do first
- [ ] - [ ] and then what?
- [ ] Etc.
## Additional details / background info ## Additional details / background info
+4 -7
View File
@@ -22,14 +22,11 @@ So that
## Tasks ## Tasks
- [ ] UX (if needed) - [ ] Explore and sketch
- [ ] Design (if needed)
- [ ] Team and stakeholder review - [ ] Team and stakeholder review
- [ ] Specs reviewed and created or adjusted - [ ] Visual Design
- [ ] Implementation - [ ] Team review
- [ ] Testing (unit and/or e2e) - [ ] Etc.
- [ ] Code review
- [ ] QA review
## Sketch ## Sketch
@@ -1,82 +0,0 @@
name: After Release
on:
release:
types: [published]
jobs:
after-release:
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: resolve ipfs hashes for release
run: |
echo "Name: ${{ github.event.release.name }}"
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
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
echo IPFS_V0=$(cat ipfs-hash) >> $GITHUB_ENV
echo IPFS_V1=$(ipfs cid format -v 1 -b base32 $(cat ipfs-hash)) >> $GITHUB_ENV
- name: Edit Release
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
body: |
___
# Deployments
* https://explorer.vega.xyz
* https://governance.vega.xyz
# IPFS releases
The IPFS hash of this release of the Trading app is:
CIDv0: ${{ env.IPFS_V0 }}
CIDv1: ${{ env.IPFS_V1 }}
You can always access the latest IPFS release by visiting [console.vega.xyz](https://console.vega.xyz).
You can also access Trading directly from an IPFS gateway.
BEWARE: The Trading interface uses [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. You should always use an IPFS gateway that enforces [origin separation](https://ipfs.github.io/public-gateway-checker/).
Your settings are not remembered across different URLs.
IPFS gateways:
https://${{ env.IPFS_V1 }}.ipfs.dweb.link/
https://${{ env.IPFS_V1 }}.ipfs.cf-ipfs.com/
ipfs://${{ env.IPFS_V0 }}/
+1 -1
View File
@@ -6,7 +6,7 @@ name: 'Add Issues To Project Board'
types: types:
- opened - opened
env: env:
GH_TOKEN: ${{ secrets.PROJECT_MANAGE_ACTION }} GH_TOKEN: ${{ secrets.GH_NEW_CARD_TO_PROJECT }}
PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }} PROJECT_ID: ${{ secrets.FRONT_END_PROJECT_ID }}
ISSUE_ID: ${{ github.event.issue.node_id }} ISSUE_ID: ${{ github.event.issue.node_id }}
USER: ${{ github.actor }} USER: ${{ github.actor }}
+32 -137
View File
@@ -19,8 +19,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Cache node modules - name: Cache node modules
id: cache id: cache
@@ -61,7 +59,6 @@ jobs:
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
@@ -99,122 +96,46 @@ jobs:
# See affected apps # See affected apps
- name: See affected apps - name: See affected apps
run: | 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 ">>>> debug"
echo "NX Version: $nx_version"
echo "NX_BASE: ${{ env.NX_BASE }}" echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}" echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo "Affected: ${affected}"
echo "Branch slug: ${branch_slug}"
echo "Current ref: ${{ github.ref }}"
echo ">>>> eof debug" echo ">>>> eof debug"
projects_array=() affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
echo -n "Affected projects: $affected"
branch_slug="$(echo ${{ github.head_ref || github.ref_name }} | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
projects_e2e=""
preview_governance="not deployed" preview_governance="not deployed"
preview_trading="not deployed" preview_trading="not deployed"
preview_explorer="not deployed" preview_explorer="not deployed"
preview_tools="not deployed" if [[ -z "$projects_e2e" ]]; then
projects_e2e+='"governance-e2e" "trading-e2e" "explorer-e2e" '
# 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_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug") preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug") preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi else
if [[ $affected == *"governance"* ]]; then
# applications parsed before this loop are applicable for running e2e-tests projects_e2e+='"governance-e2e" '
projects_e2e_array=() preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
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 fi
# those apps deploy only from develop to mainnet if [[ $affected == *"trading"* ]]; then
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then projects_e2e+='"trading-e2e" '
if echo "$affected" | grep -q multisig-signer; then preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
echo "Tools are affected"
echo "Deploying tools on s3"
projects_array+=("multisig-signer")
fi fi
if echo "$affected" | grep -q static; then if [[ $affected == *"explorer"* ]]; then
echo "static is affected" projects_e2e+='"explorer-e2e" '
echo "Deploying static on s3" preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
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
fi fi
projects_e2e=${projects_e2e%?}
# if branch starts with release/ and ends with trading / governance or explorer - overwrite the array of affected projects with fixed single application projects_e2e=[${projects_e2e// /,}]
if [[ "${{ github.ref }}" == *release* ]]; then echo PROJECTS_E2E=$projects_e2e >> $GITHUB_ENV
echo ">> This is a relase branch" echo PROJECTS=$(echo $projects_e2e | sed 's|-e2e||g') >> $GITHUB_ENV
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_GOVERNANCE=$preview_governance >> $GITHUB_ENV
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
outputs: outputs:
projects: ${{ env.PROJECTS }} projects: ${{ env.PROJECTS }}
@@ -222,12 +143,11 @@ jobs:
preview_governance: ${{ env.PREVIEW_GOVERNANCE }} preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
preview_trading: ${{ env.PREVIEW_TRADING }} preview_trading: ${{ env.PREVIEW_TRADING }}
preview_explorer: ${{ env.PREVIEW_EXPLORER }} preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }}
cypress: cypress:
needs: lint-test-build needs: lint-test-build
name: '(CI) cypress' name: '(CI) cypress'
# if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }} if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml uses: ./.github/workflows/cypress-run.yml
secrets: inherit secrets: inherit
with: with:
@@ -237,7 +157,7 @@ jobs:
publish-dist: publish-dist:
needs: lint-test-build needs: lint-test-build
name: '(CD) publish dist' name: '(CD) publish dist'
if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'vegaprotocol/frontend-monorepo') || github.event_name == 'push' }} if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-dist.yml uses: ./.github/workflows/publish-dist.yml
secrets: inherit secrets: inherit
with: with:
@@ -248,8 +168,7 @@ jobs:
needs: needs:
- publish-dist - publish-dist
- lint-test-build - lint-test-build
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'vegaprotocol/frontend-monorepo' }} if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 60
name: '(CD) comment preview links' name: '(CD) comment preview links'
steps: steps:
- name: Find Comment - name: Find Comment
@@ -259,50 +178,26 @@ jobs:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
body-includes: Previews body-includes: Previews
- name: Wait for deployments
run: |
# https://stackoverflow.com/questions/3183444/check-for-valid-link-url
regex='(https?|ftp|file)://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]'
if [[ "${{ needs.lint-test-build.outputs.preview_governance }}" =~ $regex ]]; then
until curl --insecure --location --fail "${{ needs.lint-test-build.outputs.preview_governance }}"; do
echo "waiting for governance preview: ${{ needs.lint-test-build.outputs.preview_governance }}"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_explorer }}" =~ $regex ]]; then
until curl --insecure --location --fail "${{ needs.lint-test-build.outputs.preview_explorer }}"; do
echo "waiting for explorer preview: ${{ needs.lint-test-build.outputs.preview_explorer }}"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_trading }}" =~ $regex ]]; then
until curl --insecure --location --fail "${{ needs.lint-test-build.outputs.preview_trading }}"; do
echo "waiting for trading preview: ${{ needs.lint-test-build.outputs.preview_trading }}"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then
until curl --insecure --location --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do
echo "waiting for tools preview: ${{ needs.lint-test-build.outputs.preview_tools }}"
sleep 5
done
fi
- name: Create comment - name: Create comment
uses: peter-evans/create-or-update-comment@v3 uses: peter-evans/create-or-update-comment@v3
if: ${{ steps.fc.outputs.comment-id == 0 }} if: ${{ steps.fc.outputs.comment-id == 0 }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
body: | body: |
Previews Previews:
* governance: ${{ needs.lint-test-build.outputs.preview_governance }} * governance: ${{ needs.lint-test-build.outputs.preview_governance }}
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }} * explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
* trading: ${{ needs.lint-test-build.outputs.preview_trading }} * trading: ${{ needs.lint-test-build.outputs.preview_trading }}
* tools: ${{ needs.lint-test-build.outputs.preview_tools }}
# Report single result at the end, to avoid mess with required checks in PR
cypress-check: cypress-check:
name: '(CI) cypress - check' name: '(CI) cypress - check'
runs-on: ubuntu-latest
needs: cypress
steps:
- run: echo Done!
# Report single result at the end, to avoid mess with required checks in PR
cypress-result:
if: ${{ always() }} if: ${{ always() }}
needs: cypress needs: cypress
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
-59
View File
@@ -1,59 +0,0 @@
name: console-test-run
on:
workflow_call:
inputs:
github-sha:
required: true
type: string
jobs:
console-test:
timeout-minutes: 5
runs-on: self-hosted-runner
steps:
- name: Checkout console test repo
uses: actions/checkout@v3
with:
repository: vegaprotocol/console-test
path: './console-test'
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10.11'
- name: Install Poetry
uses: snok/install-poetry@v1
- 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
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
- name: install playwright
run: poetry run playwright install
working-directory: ./console-test
- name: run tests
run: poetry run pytest --numprocesses auto
working-directory: ./console-test
- name: Upload Playwright Trace
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-trace
path: ./traces/
retention-days: 15
+1
View File
@@ -33,4 +33,5 @@ jobs:
config: baseUrl=${{ github.event.inputs.url }} config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live env: grepTags=@live
env: env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -4
View File
@@ -20,7 +20,7 @@ jobs:
project: ${{ fromJSON(inputs.projects) }} project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }} name: ${{ matrix.project }}
runs-on: self-hosted-runner runs-on: self-hosted-runner
timeout-minutes: 100 timeout-minutes: 40
steps: steps:
# Checks if skip cache was requested # Checks if skip cache was requested
- name: Set skip-nx-cache flag - name: Set skip-nx-cache flag
@@ -33,7 +33,6 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
path: './frontend-monorepo' path: './frontend-monorepo'
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# Restore node_modules from cache if possible # Restore node_modules from cache if possible
- name: Restore node_modules from cache - name: Restore node_modules from cache
@@ -67,7 +66,7 @@ jobs:
###### ######
- name: Run Cypress tests - name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}" run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
working-directory: frontend-monorepo working-directory: frontend-monorepo
env: env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }} CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
@@ -99,4 +98,4 @@ jobs:
if: ${{ failure() }} if: ${{ failure() }}
with: with:
name: test-report-${{ matrix.project }} name: test-report-${{ matrix.project }}
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports path: frontend-monorepo/apps/trading-e2e/cypress/reports
-2
View File
@@ -11,8 +11,6 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
+32
View File
@@ -0,0 +1,32 @@
name: Generate tranches
on:
schedule:
- cron: '0 */6 * * *'
jobs:
master:
name: Generate Queries
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Install root dependencies
run: yarn install
- name: Generate queries
run: node ./scripts/get-tranches.js
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: 'chore: update tranches'
commit_options: '--no-verify --signoff'
skip_fetch: true
skip_checkout: true
+52 -245
View File
@@ -19,41 +19,6 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Init variables
run: |
echo IS_PR=false >> $GITHUB_ENV
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
- name: Is PR
if: ${{ github.event_name == 'pull_request' }}
run: |
echo IS_PR=true >> $GITHUB_ENV
- name: Is mainnet release
if: ${{ contains(github.ref, 'release/mainnnet') && !contains(github.ref, 'mirror') }}
run: |
echo IS_MAINNET_RELEASE=true >> $GITHUB_ENV
- name: Is testnet release
if: ${{ contains(github.ref, 'release/testnet') }}
run: |
echo IS_TESTNET_RELEASE=true >> $GITHUB_ENV
- name: Is IPFS Release
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( env.IS_MAINNET_RELEASE == 'true' || env.IS_TESTNET_RELEASE == 'true' ) }}
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 - name: Set up QEMU
id: quemu id: quemu
@@ -65,21 +30,13 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr) - name: Log in to the Container registry
if: ${{ env.IS_PR == 'true' }}
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node - name: Setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
@@ -94,71 +51,46 @@ jobs:
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }} key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# https://docs.github.com/en/actions/learn-github-actions/contexts # https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Define dist variables - name: Define variables
if: ${{ github.event_name == 'push' }}
run: | run: |
envName='' envName=''
domain="vega.rocks" dockerfile="dist.Dockerfile"
bucketName='' if [[ "${{ github.event_name }}" = "push" ]]; then
domain="vega.rocks"
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then 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 }} | rev | cut -d '/' -f 1 | rev)"
envName="$(echo ${{ github.ref }} | sed -e "s|refs/heads/release/||" | cut -d '-' -f 1 )" if [[ "${{ github.ref }}" =~ .*mainnet.* ]]; then
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then domain="vega.community"
envName="stagnet1" if [[ "${{ matrix.app }}" = "trading" ]]; then
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then dockerfile="ipfs.Dockerfile"
envName="mainnet" fi
bucketName="tools.vega.xyz" fi
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
fi 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}" bucketName="${{ matrix.app }}.${envName}.${domain}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
fi fi
nodeVersion=$(cat .nvmrc | head -n 1)
echo "bucket name: ${bucketName}"
echo "env name: ${envName}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
echo ENV_NAME=${envName} >> $GITHUB_ENV echo ENV_NAME=${envName} >> $GITHUB_ENV
echo NODE_VERSION=${nodeVersion} >> $GITHUB_ENV
echo DOCKERFILE=docker/${dockerfile} >> $GITHUB_ENV
- name: Build local dist - name: Build local dist
if: ${{ env.DOCKERFILE != 'docker/ipfs.Dockerfile' }}
run: | run: |
envCmd="" flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
envCmd="yarn env-cmd -f ./apps/${{ matrix.app }}/.env.${{ env.ENV_NAME }}" if [[ "${{ env.ENV_NAME }}" != "ops-vega" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
fi fi
if [ "${{ matrix.app }}" = "trading" ]; then if [ "${{ matrix.app }}" = "trading" ]; then
$envCmd yarn nx export trading || (yarn install && $envCmd yarn nx export trading) yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
DIST_LOCATION=dist/apps/trading/exported DIST_LOCATION=dist/apps/trading/exported
elif [ "${{ matrix.app }}" = "ui-toolkit" ]; then
NODE_ENV=production yarn nx run ui-toolkit:build-storybook
DIST_LOCATION=dist/storybook/ui-toolkit
else else
$envCmd yarn nx build ${{ matrix.app }} || (yarn install && $envCmd yarn nx build ${{ matrix.app }}) yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }} DIST_LOCATION=dist/apps/${{ matrix.app }}
fi fi
mv $DIST_LOCATION dist-result mv $DIST_LOCATION dist-result
@@ -166,92 +98,62 @@ jobs:
- name: Build and export to local Docker - name: Build and export to local Docker
id: docker_build id: docker_build
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: ${{ env.DOCKERFILE }}
load: true load: true
build-args: | build-args: |
APP=${{ matrix.app }} APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest - name: Image digest
if: ${{ env.IS_PR == 'true' }} if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image - name: Sanity check docker image
if: ${{ github.event_name == 'pull_request' || ( env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' ) }}
run: | run: |
echo "Check ipfs-hash" echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash if [[ "${{ env.DOCKERFILE }}" = "docker/ipfs.Dockerfile" ]]; then
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
fi
echo "List html directory" echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html' docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree .'
- name: Publish dist as docker image (ghcr) - name: Copy dist to local filesystem
if: ${{ env.DOCKERFILE == 'docker/ipfs.Dockerfile' && github.event_name == 'push' }}
run: |
docker create --name=dist ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
docker cp dist:/usr/share/nginx/html dist
echo "check local dist files"
tree dist/html
mv dist/html dist-result
- name: Publish dist as docker image
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
continue-on-error: true if: ${{ github.event_name == 'pull_request' }}
id: ghcr-push
if: ${{ env.IS_PR == 'true' }}
with: with:
context: . context: .
file: docker/node-outside-docker.Dockerfile file: ${{ env.DOCKERFILE }}
push: true push: true
build-args: | build-args: |
APP=${{ matrix.app }} APP=${{ matrix.app }}
NODE_VERSION=${{ env.NODE_VERSION }}
ENV_NAME=${{ env.ENV_NAME }} ENV_NAME=${{ env.ENV_NAME }}
tags: | tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }} ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub)
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
- name: Publish dist as docker image (ghcr - retry)
uses: docker/build-push-action@v3
if: ${{ steps.ghcr-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ github.event.pull_request.head.sha || github.sha }}
- name: Publish dist as docker image (docker hub - retry)
uses: docker/build-push-action@v3
if: ${{ steps.dockerhub-push.outcome == 'failure' }}
with:
context: .
file: docker/node-outside-docker.Dockerfile
push: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
vegaprotocol/${{ matrix.app }}:${{ github.sha }}
vegaprotocol/${{ matrix.app }}:${{ env.IS_MAINNET_RELEASE == 'true' && 'mainnet' || env.IS_TESTNET_RELEASE == 'true' && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend # bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3 - name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS if: ${{ github.event_name == 'push' }}
if: ${{ env.IS_S3_RELEASE == 'true' }}
with: with:
args: --acl private --follow-symlinks --delete args: --acl private --follow-symlinks --delete
env: env:
@@ -261,104 +163,9 @@ jobs:
AWS_REGION: 'eu-west-1' AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result' SOURCE_DIR: 'dist-result'
- name: Install aws CLI
if: ${{ env.IS_S3_RELEASE == 'true' }}
uses: unfor19/install-aws-cli-action@master
- name: Perform cache invalidation
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 }}
AWS_REGION: 'eu-west-1'
run: |
echo "Looking for distribution for bucket: ${{ env.BUCKET_NAME }}"
id=$(aws cloudfront list-distributions | jq -Mrc '.DistributionList.Items | .[] | select(.DefaultCacheBehavior.TargetOriginId == "${{ env.BUCKET_NAME }}") | .Id')
echo "Found id is: ${id}"
aws cloudfront create-invalidation --distribution-id $id --paths "/*"
- name: Add preview label - name: Add preview label
uses: actions-ecosystem/action-add-labels@v1 uses: actions-ecosystem/action-add-labels@v1
if: ${{ env.IS_PR == 'true' }} if: ${{ github.event_name == 'pull_request' }}
with: with:
labels: ${{ matrix.app }}-preview labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }} number: ${{ github.event.number }}
- name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
run: |
if [[ "${{ env.IS_MAINNET_RELEASE }}" = "true" ]]; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
elif [[ "${{ env.IS_TESTNET_RELEASE }}" = "true" ]]; then
# display info about app
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl --fail -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"79baaeca-1952-4ae7-a256-f668cfc1d68e\"){id status}}"}' \
https://api.fleek.co/graphql
fi
- name: Check out ipfs-redirect
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update interstitial page to point to the new console
if: ${{ env.IS_IPFS_RELEASE == 'true' }}
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
# set CID
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ${{ matrix.app }}-ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
(
cd ipfs-redirect
# configure git
git status
cat .git/config
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
# update CID files
if [[ "${{ env.IS_MAINNET_RELEASE }}" = "true" ]]; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif [[ "${{ env.IS_TESTNET_RELEASE }}" = "true" ]]; then
echo $new_hash > cidv0-fairground.txt
echo $new_cid > cidv1-fairground.txt
git add cidv0-fairground.txt cidv1-fairground.txt
fi
# create commit
commit_msg="Automated hash update from ${{ github.ref }}"
git commit -m "$commit_msg"
git push -u origin "main"
)
-84
View File
@@ -1,84 +0,0 @@
name: 'Rollback console'
on:
workflow_dispatch:
inputs:
version:
description: 'Version that should be set on rollback'
required: true
type: string
jobs:
rollback:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Retag mainnet
run: |
docker pull vegaprotocol/trading:${{ inputs.version }}
docker tag vegaprotocol/trading:${{ inputs.version }} vegaprotocol/trading:mainnet
docker push vegaprotocol/trading:mainnet
docker run --rm vegaprotocol/trading:mainnet cat /ipfs-hash > ipfs-hash
- name: Trigger fleek deployment
run: |
# display info about app
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "query{getSiteById(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id latestDeploy{id status}}}"}' \
https://api.fleek.co/graphql
# trigger new deployment as base image is always set to vegaprotocol/trading:mainnet
curl -H "Authorization: ${{ secrets.FLEEK_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"query": "mutation{triggerDeploy(siteId:\"f8f2e051-f18e-49e6-b876-0a39369dc0d8\"){id status}}"}' \
https://api.fleek.co/graphql
- name: Check out ipfs-redirect
uses: actions/checkout@v3
with:
repository: 'vegaprotocol/ipfs-redirect'
path: 'ipfs-redirect'
fetch-depth: '0'
token: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
- name: Update console.vega.xyz DNS to redirect to the new console
env:
GH_TOKEN: ${{ secrets.VEGA_CI_BOT_GITHUB_TOKEN }}
run: |
curl -L https://dist.ipfs.tech/kubo/v0.20.0/kubo_v0.20.0_linux-amd64.tar.gz -o kubo.tgz
tar -xzf kubo.tgz
export PATH="$PATH:$PWD/kubo"
which ipfs
new_hash=$(cat ipfs-hash)
new_cid=$(ipfs cid format -v 1 -b base32 $new_hash)
git config --global user.email "vega-ci-bot@vega.xyz"
git config --global user.name "vega-ci-bot"
echo $new_hash > ipfs-redirect/cidv0.txt
echo $new_cid > ipfs-redirect/cidv1.txt
(
cd ipfs-redirect
git status
branch_name="rollback-to-$new_hash"
git checkout -b "$branch_name"
commit_msg="hash rollback to $new_hash"
git add cidv0.txt cidv1.txt
git commit -m "$commit_msg"
git push -u origin "$branch_name" --force-with-lease
pr_url="$(gh pr create --title "${commit_msg}" --body 'automated pull request to update CIDs')"
echo $pr_url
# once auto merge get's enabled on documentation repo let's do follow up
sleep 5
gh pr merge "${pr_url}" --delete-branch --squash --admin
)
+1 -4
View File
@@ -2,7 +2,6 @@
# compiled output # compiled output
/dist /dist
/dist-result
/tmp /tmp
/out-tsc /out-tsc
/tools/executors/**/*.js /tools/executors/**/*.js
@@ -49,6 +48,4 @@ cypress.env.json
.next .next
#cypress #cypress
/apps/**/cypress/reports/ /apps/trading-e2e/cypress/reports/
/apps/**/cypress/downloads/
/apps/**/fixtures/wallet/node**
+29
View File
@@ -0,0 +1,29 @@
module.exports = {
stories: [],
addons: [
'@storybook/addon-actions',
'@storybook/addon-viewport',
{
name: '@storybook/addon-docs',
options: {
configureJSX: true,
babelOptions: {},
sourceLoaderOptions: null,
transcludeMarkdown: true,
},
},
'@storybook/addon-controls',
'@storybook/addon-backgrounds',
'@storybook/addon-toolbars',
'@storybook/addon-measure',
'@storybook/addon-outline',
'@storybook/addon-a11y',
],
// uncomment the property below if you want to apply some webpack config globally
// webpackFinal: async (config, { configType }) => {
// // Make whatever fine-grained changes you need that should apply to all storybook configs
// // Return the altered config
// return config;
// },
};
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.base.json",
"exclude": [
"../**/*.spec.js",
"../**/*.test.js",
"../**/*.spec.ts",
"../**/*.test.ts",
"../**/*.spec.tsx",
"../**/*.test.tsx",
"../**/*.spec.jsx",
"../**/*.test.jsx"
],
"include": ["../**/*"]
}
-20
View File
@@ -1,20 +0,0 @@
.PHONY: latest-release
latest-release:
gh release list | head -n 1 | awk '{print $1}'
.PHONY: show-latest-release
show-latest-release:
gh release view `gh release list | head -n 1 | awk '{print $1}'`
.PHONY: recalculate-ipfs
recalculate-ipfs:
echo "ipfs hash inside the image"
docker run --rm ${TAG} cat /ipfs-hash
echo "recalculating ipfs hash"
docker run --rm ${TAG} ipfs add -r /usr/share/nginx/html
.PHONY: eject-ipfs-hash
unpack:
docker create --name=dist ${TAG}
docker cp dist:/usr/share/nginx/html dist
docker rm dist
+12 -55
View File
@@ -103,68 +103,25 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console # Docker & Vegacapsule
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). ## Docker
In order to run a container on port 3000: The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
```
In order to run a container:
```bash ```bash
docker run -p 3000:80 [TAG] docker run -p 3000:80 [TAG]
``` ```
## Build instructions Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
### nx build outside the docker
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
```bash
./docker/prepare-dist.sh
```
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
```
### nx build inside the docker
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build) example.
```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
```
### Computing ipfs-hash of the build
At the moment this feature is important only for `trading` (console) releases.
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
```bash
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
```
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
### Verifying ipfs-hash of existing current application version
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
4. Download docker image with the desired release `docker pull $TAG`.
5. Recalculate hash: `make recalculate-ipfs`
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
## Config ## Config
+1
View File
@@ -1,3 +1,4 @@
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=http://localhost:26617 NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_URL=http://localhost:3008/graphql NX_VEGA_URL=http://localhost:3008/graphql
+1
View File
@@ -1,4 +1,5 @@
# App configuration variables # App configuration variables
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
+1
View File
@@ -1,4 +1,5 @@
# App configuration variables # App configuration variables
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/ NX_TENDERMINT_URL=https://mainnet-observer-proxy01.ops.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
NX_VEGA_URL=https://api.vega.community/graphql NX_VEGA_URL=https://api.vega.community/graphql
+1
View File
@@ -1,4 +1,5 @@
# App configuration variables # App configuration variables
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz/tm NX_TENDERMINT_URL=https://tm.n07.testnet.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
+1 -2
View File
@@ -1,11 +1,10 @@
const { defineConfig } = require('cypress'); const { defineConfig } = require('cypress');
module.exports = defineConfig({ module.exports = defineConfig({
reporter: '../../node_modules/cypress-mochawesome-reporter', projectId: 'et4snf',
e2e: { e2e: {
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('@cypress/grep/src/plugin')(config); require('@cypress/grep/src/plugin')(config);
return config; return config;
}, },
+3 -4
View File
@@ -1,11 +1,10 @@
{ {
"name": "explorer-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/explorer-e2e/src", "sourceRoot": "apps/explorer-e2e/src",
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"e2e": { "e2e": {
"executor": "@nx/cypress:cypress", "executor": "@nrwl/cypress:cypress",
"options": { "options": {
"cypressConfig": "apps/explorer-e2e/cypress.config.js", "cypressConfig": "apps/explorer-e2e/cypress.config.js",
"devServerTarget": "explorer:serve" "devServerTarget": "explorer:serve"
@@ -17,14 +16,14 @@
} }
}, },
"lint": { "lint": {
"executor": "@nx/linter:eslint", "executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],
"options": { "options": {
"lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"] "lintFilePatterns": ["apps/explorer-e2e/**/*.{js,ts}"]
} }
}, },
"build": { "build": {
"executor": "nx:run-commands", "executor": "@nrwl/workspace:run-commands",
"outputs": [], "outputs": [],
"options": { "options": {
"command": "yarn tsc --project ./apps/explorer-e2e/" "command": "yarn tsc --project ./apps/explorer-e2e/"
@@ -31,7 +31,7 @@ context('Asset page', { tags: '@regression' }, () => {
}); });
}); });
it.skip('should open details page when clicked on "View details"', () => { it('should open details page when clicked on "View details"', () => {
cy.getAssets().then((assets) => { cy.getAssets().then((assets) => {
assets.forEach((asset) => { assets.forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`) cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
@@ -169,6 +169,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
const jsonFields = '.hljs'; const jsonFields = '.hljs';
const sideMenuBackground = '.absolute'; const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground) cy.get(sideMenuBackground)
.should('have.css', 'background-color') .should('have.css', 'background-color')
.then((background_color) => { .then((background_color) => {
@@ -40,7 +40,7 @@ context.skip('Transactions page', function () {
.first() .first()
.click({ force: true }); .click({ force: true });
} else { } else {
cy.log('Unable to find any transactions on page'); cy.slack('Unable to find any transactions on page');
cy.screenshot(); cy.screenshot();
} }
}); });
@@ -78,7 +78,7 @@ context.skip('Transactions page', function () {
} }
}); });
} else { } else {
cy.log('Unable to find any transactions on page'); cy.slack('Unable to find any transactions on page');
cy.screenshot(); cy.screenshot();
} }
}); });
-1
View File
@@ -15,6 +15,5 @@
import '@vegaprotocol/cypress'; import '@vegaprotocol/cypress';
import './common.functions.js'; import './common.functions.js';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep'; import registerCypressGrep from '@cypress/grep';
registerCypressGrep(); registerCypressGrep();
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"presets": [ "presets": [
[ [
"@nx/react/babel", "@nrwl/react/babel",
{ {
"runtime": "automatic" "runtime": "automatic"
} }
+7 -10
View File
@@ -1,21 +1,18 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.rocks NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_ENV=STAGNET1 NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789 NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.jsoo
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases/tag/
# App flags # App flags
NX_EXPLORER_ASSETS=1 NX_EXPLORER_ASSETS=1
+3 -3
View File
@@ -1,12 +1,12 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_VEGA_ENV=DEVNET NX_VEGA_ENV=DEVNET
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.governance.vega.xyz NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=/ NX_VEGA_EXPLORER_URL=/
+3 -6
View File
@@ -1,13 +1,10 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://be.vega.community NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_SENTRY_DSN=https://b3a56b03eda842faad731f3ea9dfd1bc@o286262.ingest.sentry.io/6242427
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_ENV=MAINNET NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest NX_BLOCK_EXPLORER=https://be.vega.community/rest/
NX_ETHERSCAN_URL=https://etherscan.io NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/ NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
-13
View File
@@ -1,13 +0,0 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
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_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
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks/
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
+1 -2
View File
@@ -1,2 +1 @@
# .env is stagnet1, so there are no overrides required # .env is stagnet1, so there are no overrides required
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz","STAGNET1":"https://stagnet1.explorer.vega.xyz"}'
+2 -3
View File
@@ -7,7 +7,6 @@ NX_VEGA_ENV=TESTNET
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
@@ -7,9 +7,8 @@ NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks NX_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
NX_VEGA_GOVERNANCE_URL=https://governance.validators-testnet.vega.rocks NX_VEGA_EXPLORER_URL=https://validator-testnet.explorer.vega.xyz/
NX_VEGA_EXPLORER_URL=https://explorer.validators-testnet.vega.rocks/
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks/
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"extends": ["plugin:@nx/react", "../../.eslintrc.json"], "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"], "ignorePatterns": ["!**/*", "__generated__", "__generated___"],
"overrides": [ "overrides": [
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+1 -2
View File
@@ -32,7 +32,6 @@ yarn nx serve explorer
Example configurations are provided here: Example configurations are provided here:
- [Mainnet](./.env.mainnet) - [Mainnet](./.env.mainnet)
- [Mainnet-mirror](./.env.mainnet-mirror)
- [Devnet](./.env.devnet) - [Devnet](./.env.devnet)
- [Capsule](./.env.capsule) - [Capsule](./.env.capsule)
- [Testnet](./.env.testnet) - [Testnet](./.env.testnet)
@@ -40,7 +39,7 @@ Example configurations are provided here:
For convenience, you can boot the app injecting one of the configurations above by running: For convenience, you can boot the app injecting one of the configurations above by running:
```bash ```bash
yarn env-cmd -f .\apps\explorer\.env.{env} yarn nx run explorer:serve # e.g. stagnet1 yarn nx run explorer:serve --env={env} # e.g. stagnet1
``` ```
There are a few different configuration options offered for this app: There are a few different configuration options offered for this app:
+2 -2
View File
@@ -4,8 +4,8 @@ export default {
displayName: 'explorer', displayName: 'explorer',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/react/babel'] }], '^.+\\.[tj]sx?$': 'babel-jest',
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/explorer', coverageDirectory: '../../coverage/apps/explorer',
+9 -16
View File
@@ -1,11 +1,10 @@
{ {
"name": "explorer",
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/explorer/src", "sourceRoot": "apps/explorer/src",
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"build": { "build": {
"executor": "@nx/webpack:webpack", "executor": "./tools/executors/webpack:build",
"outputs": ["{options.outputPath}"], "outputs": ["{options.outputPath}"],
"defaultConfiguration": "production", "defaultConfiguration": "production",
"options": { "options": {
@@ -39,7 +38,7 @@
} }
}, },
"serve": { "serve": {
"executor": "@nx/webpack:dev-server", "executor": "./tools/executors/webpack:serve",
"options": { "options": {
"port": 3000, "port": 3000,
"buildTarget": "explorer:build:development", "buildTarget": "explorer:build:development",
@@ -53,36 +52,30 @@
} }
}, },
"lint": { "lint": {
"executor": "@nx/linter:eslint", "executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],
"options": { "options": {
"lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"] "lintFilePatterns": ["apps/explorer/**/*.{ts,tsx,js,jsx}"]
} }
}, },
"test": { "test": {
"executor": "@nx/jest:jest", "executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/explorer"], "outputs": ["coverage/apps/explorer"],
"options": { "options": {
"jestConfig": "apps/explorer/jest.config.ts", "jestConfig": "apps/explorer/jest.config.ts",
"passWithNoTests": true "passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
} }
}, },
"generate-types": { "generate-types": {
"executor": "nx:run-commands", "executor": "@nrwl/workspace:run-commands",
"options": { "options": {
"commands": [ "commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/spec-update-v0.72.0-preview.2/specs/v0.72.0-preview.2/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types" "npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.67.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
] ]
} }
}, },
"build-netlify": { "build-netlify": {
"executor": "nx:run-commands", "executor": "@nrwl/workspace:run-commands",
"options": { "options": {
"commands": [ "commands": [
"cp apps/explorer/netlify.toml netlify.toml", "cp apps/explorer/netlify.toml netlify.toml",
@@ -91,7 +84,7 @@
} }
}, },
"build-spec": { "build-spec": {
"executor": "nx:run-commands", "executor": "@nrwl/workspace:run-commands",
"outputs": [], "outputs": [],
"options": { "options": {
"command": "yarn tsc --project ./apps/explorer/tsconfig.spec.json" "command": "yarn tsc --project ./apps/explorer/tsconfig.spec.json"
+2 -27
View File
@@ -1,19 +1,9 @@
import { import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
AppFailure,
NetworkLoader,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider'; import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client'; import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config'; import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
import { Suspense } from 'react';
const splashLoading = ( const splashLoading = (
<Splash> <Splash>
@@ -22,25 +12,10 @@ const splashLoading = (
); );
function App() { function App() {
const { VEGA_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
(store) => [store.dialogOpen, store.setDialogOpen]
);
return ( return (
<TendermintWebsocketProvider> <TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}> <NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard <RouterProvider router={router} fallbackElement={splashLoading} />
skeleton={<div>{t('Loading')}</div>}
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
>
<Suspense fallback={splashLoading}>
<RouterProvider router={router} fallbackElement={splashLoading} />
</Suspense>
</NodeGuard>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</NetworkLoader> </NetworkLoader>
</TendermintWebsocketProvider> </TendermintWebsocketProvider>
); );
@@ -1,15 +1,15 @@
import { useMemo } from 'react';
import type { AssetFieldsFragment } from '@vegaprotocol/assets'; import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets'; import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid'; import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react'; import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import type { RowClickedEvent, ColDef } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = { type AssetsTableProps = {
data: AssetFieldsFragment[] | null; data: AssetFieldsFragment[] | null;
@@ -31,58 +31,6 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
}; };
}, []); }, []);
const columnDefs = useMemo<ColDef[]>(
() => [
{ headerName: t('Symbol'), field: 'symbol' },
{ headerName: t('Name'), field: 'name' },
{
flex: 2,
headerName: t('ID'),
field: 'id',
hide: window.innerWidth < BREAKPOINT_MD,
},
{
colId: 'type',
headerName: t('Type'),
field: 'source.__typename',
hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) =>
value ? AssetTypeMapping[value].value : '',
},
{
headerName: t('Status'),
field: 'status',
hide: window.innerWidth < BREAKPOINT_MD,
valueFormatter: ({ value }: { value?: string }) =>
value ? AssetStatusMapping[value].value : '',
},
{
colId: 'actions',
headerName: '',
sortable: false,
filter: false,
resizable: false,
wrapText: true,
field: 'id',
cellRenderer: ({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
) : (
''
),
},
],
[navigate]
);
return ( return (
<AgGrid <AgGrid
ref={ref} ref={ref}
@@ -98,11 +46,60 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
autoHeight: true, autoHeight: true,
}} }}
columnDefs={columnDefs}
suppressCellFocus={true} suppressCellFocus={true}
onRowClicked={({ data }: RowClickedEvent) => { onRowClicked={({ data }: RowClickedEvent) => {
navigate(data.id); navigate(data.id);
}} }}
/> >
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn
flex="2"
headerName={t('ID')}
field="id"
hide={window.innerWidth < BREAKPOINT_MD}
/>
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
/>
<AgGridColumn
colId="actions"
headerName=""
sortable={false}
filter={false}
resizable={false}
wrapText={true}
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
) : (
''
)
}
/>
</AgGrid>
); );
}; };
@@ -1,13 +1,29 @@
import WS from 'jest-websocket-mock'; import WS from 'jest-websocket-mock';
import { render, screen } from '@testing-library/react'; import useWebSocket from 'react-use-websocket';
import {
render,
screen,
fireEvent,
act,
waitFor,
} from '@testing-library/react';
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
import { BlocksRefetch } from './blocks-refetch'; import { BlocksRefetch } from './blocks-refetch';
const BlocksRefetchInWebsocketProvider = ({ const BlocksRefetchInWebsocketProvider = ({
callback, callback,
mocketLocation,
}: { }: {
callback: () => null; callback: () => null;
mocketLocation: string;
}) => { }) => {
return <BlocksRefetch refetch={callback} />; const contextShape = useWebSocket(mocketLocation);
return (
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
<BlocksRefetch refetch={callback} />
</TendermintWebsocketContext.Provider>
);
}; };
describe('Blocks refetch', () => { describe('Blocks refetch', () => {
@@ -16,8 +32,111 @@ describe('Blocks refetch', () => {
const mocket = new WS(mocketLocation, { jsonProtocol: true }); const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation); new WebSocket(mocketLocation);
render(<BlocksRefetchInWebsocketProvider callback={() => null} />); render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
expect(screen.getByTestId('refresh')).toBeInTheDocument(); expect(screen.getByTestId('refresh')).toBeInTheDocument();
mocket.close(); mocket.close();
}); });
it('should initiate callback when the button is clicked', async () => {
const mocketLocation = 'wss:localhost:3003';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
const callback = jest.fn();
render(
<BlocksRefetchInWebsocketProvider
callback={callback}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
const button = screen.getByTestId('refresh');
act(() => {
fireEvent.click(button);
});
expect(callback.mock.calls.length).toEqual(1);
mocket.close();
});
it('should show new blocks as websocket is correctly updated', async () => {
const mocketLocation = 'wss:localhost:3004';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
// @ts-ignore id on messages
const id = mocket.messages[0].id;
const newBlockMessage = {
id,
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
act(() => {
mocket.send(newBlockMessage);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
mocket.close();
});
it('will not show new blocks if websocket has wrong ID', async () => {
const mocketLocation = 'wss:localhost:3005';
const mocket = new WS(mocketLocation, { jsonProtocol: true });
new WebSocket(mocketLocation);
render(
<BlocksRefetchInWebsocketProvider
callback={() => null}
mocketLocation={mocketLocation}
/>
);
await mocket.connected;
// Ensuring we send an ID equal to the one the client subscribed with.
await waitFor(() => expect(mocket.messages.length).toEqual(1));
const newBlockMessageBadId = {
id: 'blahblahblah',
result: {
query: "tm.event = 'NewBlock'",
},
};
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
act(() => {
mocket.send(newBlockMessageBadId);
});
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
mocket.close();
});
}); });
@@ -1,19 +1,36 @@
import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Button, Icon } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
interface BlocksRefetchProps { interface BlocksRefetchProps {
refetch: () => void; refetch: () => void;
} }
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => { export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
const { messages } = useTendermintWebsocket({
query: "tm.event = 'NewBlock'",
});
useEffect(() => {
if (messages.length > 0) {
setBlocksToLoad((prev) => prev + 1);
}
}, [messages]);
const refresh = () => { const refresh = () => {
refetch(); refetch();
setBlocksToLoad(0);
}; };
return ( return (
<Button onClick={refresh} data-testid="refresh" size="xs"> <div className="mb-4">
<Icon name="refresh" className="!align-baseline mr-2" size={3} /> <span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
{t('Load new')} <ButtonLink onClick={refresh} data-testid="refresh">
</Button> {t('refresh to see latest')}
</ButtonLink>
</div>
); );
}; };
@@ -1,21 +1,13 @@
import { import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { Link, ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { ENV } from '../../config/env'; import { ENV } from '../../config/env';
import { Routes } from '../../routes/route-names';
import { Link as RouteLink } from 'react-router-dom';
export const Footer = () => { export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment(); const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const setNodeSwitcherOpen = useNodeSwitcherStore( const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
(store) => store.setDialogOpen
);
const { screenSize } = useScreenDimensions(); const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo( const showFullFeedbackLabel = useMemo(
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize), () => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
@@ -23,53 +15,56 @@ export const Footer = () => {
); );
return ( return (
<footer className="grid grid-rows-2 lg:grid-cols-[1fr_auto] text-xs md:text-md lg:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200"> <>
<div className="flex justify-between gap-2 align-middle"> <footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
{GIT_COMMIT_HASH && ( <div className="flex justify-between gap-2 align-middle">
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4"> {GIT_COMMIT_HASH && (
<p data-testid="git-commit-hash"> <div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
{t('Version')}:{' '} <p data-testid="git-commit-hash">
<Link {t('Version')}:{' '}
href={ <Link
GIT_ORIGIN_URL href={
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}` GIT_ORIGIN_URL
: undefined ? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
} : undefined
target={GIT_ORIGIN_URL ? '_blank' : undefined} }
> target={GIT_ORIGIN_URL ? '_blank' : undefined}
{GIT_COMMIT_HASH} >
</Link> {GIT_COMMIT_HASH}
</p> </Link>
</p>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div> </div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
{ENV.addresses.feedback ? (
<div className="flex pl-2 content-center"> <div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}> <ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')} {showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink> </ExternalLink>
</div> </div>
) : null} </div>
</div> </footer>
<div className="pl-2 align-center lg:align-right lg:flex lg:justify-end gap-2 align-middle lg:max-w-xs lg:ml-auto"> <NodeSwitcherDialog
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline"> open={nodeSwitcherOpen}
Disclaimer setOpen={setNodeSwitcherOpen}
</RouteLink> />
</div> </>
</footer>
); );
}; };
export const NodeUrl = ({ url }: { url: string }) => { const NodeUrl = ({ url }: { url: string }) => {
// get base url from api url, api sub domain // get base url from api url, api sub domain
const urlObj = new URL(url); const urlObj = new URL(url);
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, ''); const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
return <span className="cursor-default">{nodeUrl}</span>; return (
<Link href={'https://' + nodeUrl} target="_blank">
{nodeUrl}
</Link>
);
}; };
@@ -3,7 +3,6 @@ import { Header } from './header';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
jest.mock('@vegaprotocol/environment', () => ({ jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => ( NetworkSwitcher: () => (
<div data-testid="network-switcher">NetworkSwitcher</div> <div data-testid="network-switcher">NetworkSwitcher</div>
), ),
@@ -1,4 +1,5 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit'; import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import React from 'react';
export interface InfoBlockProps { export interface InfoBlockProps {
title: string; title: string;
@@ -1,98 +1,33 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link'; import PartyLink from './party-link';
import { MockedProvider } from '@apollo/client/testing';
import { ExplorerNodeNamesDocument } from '../../../routes/validators/__generated__/NodeNames';
import { act } from 'react-dom/test-utils';
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const mocks = [
{
request: {
query: ExplorerNodeNamesDocument,
},
result: {
data: {
nodesConnection: {
edges: [
{
node: {
id: '1',
name: 'Validator Node',
pubkey:
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e',
tmPubkey: 'tmPubkey1',
ethereumAddress: '0x123456789',
},
},
{
node: {
id: '2',
name: 'Node 2',
pubkey: 'pubkey2',
tmPubkey: 'tmPubkey2',
ethereumAddress: '0xabcdef123',
},
},
],
},
},
},
},
];
describe('PartyLink', () => { describe('PartyLink', () => {
it('renders Network for 000.000 party', () => { it('renders Network for 000.000 party', () => {
const screen = render( const zeroes =
<MockedProvider> '0000000000000000000000000000000000000000000000000000000000000000';
<PartyLink id={zeroes} /> const screen = render(<PartyLink id={zeroes} />);
</MockedProvider>
);
expect(screen.getByText('Network')).toBeInTheDocument(); expect(screen.getByText('Network')).toBeInTheDocument();
}); });
it('renders Network for network party', () => { it('renders Network for network party', () => {
const screen = render( const screen = render(<PartyLink id="network" />);
<MockedProvider>
<PartyLink id="network" />
</MockedProvider>
);
expect(screen.getByText('Network')).toBeInTheDocument(); expect(screen.getByText('Network')).toBeInTheDocument();
}); });
it('renders ID with no link for invalid party', () => { it('renders ID with no link for invalid party', () => {
const screen = render( const screen = render(<PartyLink id="this-party-is-not-valid" />);
<MockedProvider>
<PartyLink id="this-party-is-not-valid" />
</MockedProvider>
);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument(); expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
}); });
it('if the key is a validator, render their name instead', async () => {
const screen = render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<PartyLink id="13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e" />
</MemoryRouter>
</MockedProvider>
);
// Wait for hook to update with mock data
await act(() => new Promise((resolve) => setTimeout(resolve, 0)));
await expect(screen.getByText('Validator Node')).toBeInTheDocument();
});
it('links a valid party to the party page', () => { it('links a valid party to the party page', () => {
const aValidParty = const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e'; '13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render( const screen = render(
<MockedProvider> <MemoryRouter>
<MemoryRouter> <PartyLink id={aValidParty} />
<PartyLink id={aValidParty} /> </MemoryRouter>
</MemoryRouter>
</MockedProvider>
); );
const el = screen.getByText(aValidParty); const el = screen.getByText(aValidParty);
@@ -1,44 +1,22 @@
import { Routes } from '../../../routes/route-names'; import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useMemo, type ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import Hash from '../hash'; import Hash from '../hash';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error'; import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { Icon, truncateMiddle } from '@vegaprotocol/ui-toolkit'; import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { useExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
import type { ExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
export const SPECIAL_CASE_NETWORK_ID = export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000'; '0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network'; export const SPECIAL_CASE_NETWORK = 'network';
export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
if (!data || data?.nodesConnection?.edges?.length === 0) {
return id;
}
const validator = data.nodesConnection.edges?.find((e) => {
return e?.node.pubkey === id;
});
if (validator) {
return validator.node.name;
}
return id;
}
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & { export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string; id: string;
truncate?: boolean; truncate?: boolean;
}; };
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => { const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery();
const name = useMemo(() => getNameForParty(id, data), [data, id]);
const useName = name !== id;
// Some transactions will involve the 'network' party, which is alias for '000...000' // Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network' // The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) { if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
@@ -60,20 +38,13 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
} }
return ( return (
<span className="whitespace-nowrap"> <Link
{useName && <Icon size={4} name="cube" className="mr-2" />} className="underline font-mono"
<Link {...props}
className="underline font-mono" to={`/${Routes.PARTIES}/${id}`}
{...props} >
to={`/${Routes.PARTIES}/${id}`} <Hash text={truncate ? truncateMiddle(id) : id} />
> </Link>
{useName ? (
name
) : (
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
)}
</Link>
</span>
); );
}; };
@@ -1,9 +1,8 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/markets'; import type { MarketInfoWithData } from '@vegaprotocol/market-info';
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets'; import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
import { import {
LiquidityInfoPanel,
LiquidityMonitoringParametersInfoPanel,
InstrumentInfoPanel, InstrumentInfoPanel,
KeyDetailsInfoPanel, KeyDetailsInfoPanel,
LiquidityPriceRangeInfoPanel, LiquidityPriceRangeInfoPanel,
@@ -13,18 +12,20 @@ import {
RiskModelInfoPanel, RiskModelInfoPanel,
RiskParametersInfoPanel, RiskParametersInfoPanel,
SettlementAssetInfoPanel, SettlementAssetInfoPanel,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/markets'; import { MarketInfoTable } from '@vegaprotocol/market-info';
import type { DataSourceDefinition } from '@vegaprotocol/types'; import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => { export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
if (!market) return null; if (!market) return null;
const settlementData = market.tradableInstrument.instrument.product const settlementData =
.dataSourceSpecForSettlementData.data as DataSourceDefinition; market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
const terminationData = market.tradableInstrument.instrument.product .data;
.dataSourceSpecForTradingTermination.data as DataSourceDefinition; const terminationData =
market.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data;
const getSigners = (data: DataSourceDefinition) => { const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') { if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
@@ -40,69 +41,133 @@ export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
return []; return [];
}; };
const showTwoOracles = isEqual( const oraclePanels = isEqual(
getSigners(settlementData), getSigners(settlementData),
getSigners(terminationData) getSigners(terminationData)
); )
? [
{
title: t('Settlement Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
{
title: t('Termination Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="termination"
/>
),
},
]
: [
{
title: t('Oracle'),
content: (
<OracleInfoPanel
noBorder={false}
market={market}
type="settlementData"
/>
),
},
];
const headerClassName = 'font-alpha calt text-xl mt-4 border-b-2 pb-2'; const panels = [
{
return ( title: t('Key details'),
<div> content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
<h2 className={headerClassName}>{t('Key details')}</h2> },
<KeyDetailsInfoPanel market={market} /> {
<h2 className={headerClassName}>{t('Instrument')}</h2> title: t('Instrument'),
<InstrumentInfoPanel market={market} /> content: <InstrumentInfoPanel noBorder={false} market={market} />,
<h2 className={headerClassName}>{t('Settlement asset')}</h2> },
<SettlementAssetInfoPanel market={market} /> {
<h2 className={headerClassName}>{t('Metadata')}</h2> title: t('Settlement asset'),
<MetadataInfoPanel market={market} /> content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
<h2 className={headerClassName}>{t('Risk model')}</h2> },
<RiskModelInfoPanel market={market} /> {
<h2 className={headerClassName}>{t('Risk parameters')}</h2> title: t('Metadata'),
<RiskParametersInfoPanel market={market} /> content: <MetadataInfoPanel noBorder={false} market={market} />,
<h2 className={headerClassName}>{t('Risk factors')}</h2> },
<RiskFactorsInfoPanel market={market} /> {
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => ( title: t('Risk model'),
content: <RiskModelInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk parameters'),
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk factors'),
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
},
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
(trigger, i) => ({
title: t(`Price monitoring trigger ${i + 1}`),
content: <MarketInfoTable noBorder={false} data={trigger} />,
})
),
...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({
title: t(`Price monitoring bound ${i + 1}`),
content: (
<> <>
<h2 className={headerClassName}> <MarketInfoTable
{t('Price monitoring bounds %s', [(i + 1).toString()])} noBorder={false}
</h2> data={{
<PriceMonitoringBoundsInfoPanel maxValidPrice: trigger.maxValidPrice,
market={market} minValidPrice: trigger.minValidPrice,
triggerIndex={i + 1} }}
decimalPlaces={market.decimalPlaces}
/>
<MarketInfoTable
noBorder={false}
data={{ referencePrice: trigger.referencePrice }}
decimalPlaces={
market.tradableInstrument.instrument.product.settlementAsset
.decimals
}
/> />
</> </>
),
})),
{
title: t('Liquidity monitoring parameters'),
content: (
<LiquidityMonitoringParametersInfoPanel
noBorder={false}
market={market}
/>
),
},
{
title: t('Liquidity'),
content: <LiquidityInfoPanel market={market} noBorder={false} />,
},
{
title: t('Liquidity price range'),
content: (
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
),
},
...oraclePanels,
];
return (
<>
{panels.map((p) => (
<div key={p.title} className="mb-3">
<h2 className="font-alpha calt text-xl">{p.title}</h2>
{p.content}
</div>
))} ))}
{(market.priceMonitoringSettings?.parameters?.triggers || []).map( </>
(trigger, i) => (
<>
<h2 className={headerClassName}>
{t('Price monitoring settings %s', [(i + 1).toString()])}
</h2>
<MarketInfoTable data={trigger} key={i} />
</>
)
)}
<h2 className={headerClassName}>{t('Liquidity monitoring')}</h2>
<LiquidityMonitoringParametersInfoPanel market={market} />
<h2 className={headerClassName}>{t('Liquidity')}</h2>
<LiquidityInfoPanel market={market} />
<h2 className={headerClassName}>{t('Liquidity price range')}</h2>
<LiquidityPriceRangeInfoPanel market={market} />
{showTwoOracles ? (
<>
<h2 className={headerClassName}>{t('Settlement oracle')}</h2>
<OracleInfoPanel market={market} type="settlementData" />
<h2 className={headerClassName}>{t('Termination oracle')}</h2>
<OracleInfoPanel market={market} type="termination" />
</>
) : (
<>
<h2 className={headerClassName}>{t('Oracle')}</h2>
<OracleInfoPanel market={market} type="settlementData" />
</>
)}
</div>
); );
}; };
@@ -1,9 +1,8 @@
import { useMemo } from 'react'; import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { ColDef } from 'ag-grid-community'; import { AgGridColumn } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { import type {
VegaICellRendererParams, VegaICellRendererParams,
@@ -40,79 +39,6 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
}; };
}, []); }, []);
const columnDefs = useMemo<ColDef[]>(
() => [
{
colId: 'code',
headerName: t('Code'),
field: 'tradableInstrument.instrument.code',
},
{
colId: 'name',
headerName: t('Name'),
field: 'tradableInstrument.instrument.name',
},
{
headerName: t('Status'),
field: 'state',
hide: window.innerWidth <= BREAKPOINT_MD,
valueGetter: ({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
},
},
{
colId: 'asset',
headerName: t('Settlement asset'),
field: 'tradableInstrument.instrument.product.settlementAsset.symbol',
hide: window.innerWidth <= BREAKPOINT_MD,
cellRenderer: ({
data,
}: VegaICellRendererParams<
MarketFieldsFragment,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
);
},
},
{
flex: 2,
headerName: t('Market ID'),
field: 'id',
hide: window.innerWidth <= BREAKPOINT_MD,
},
{
colId: 'actions',
headerName: '',
field: 'id',
cellRenderer: ({
value,
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
value ? (
<Link className="underline" to={value}>
{t('View details')}
</Link>
) : (
''
),
},
],
[openAssetDetailsDialog]
);
return ( return (
<AgGrid <AgGrid
ref={gridRef} ref={gridRef}
@@ -128,13 +54,79 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
autoHeight: true, autoHeight: true,
}} }}
columnDefs={columnDefs}
suppressCellFocus={true} suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => { onRowClicked={({ data, event }: RowClickedEvent) => {
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') { if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
navigate(data.id); navigate(data.id);
} }
}} }}
/> >
<AgGridColumn
colId="code"
headerName={t('Code')}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
colId="name"
headerName={t('Name')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Status')}
field="state"
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
<AgGridColumn
colId="asset"
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset"
hide={window.innerWidth <= BREAKPOINT_MD}
cellRenderer={({
value,
}: VegaICellRendererParams<
MarketFieldsFragment,
'tradableInstrument.instrument.product.settlementAsset'
>) =>
value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
)
}
/>
<AgGridColumn
flex={2}
headerName={t('Market ID')}
field="id"
hide={window.innerWidth <= BREAKPOINT_MD}
/>
<AgGridColumn
colId="actions"
headerName=""
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
value ? (
<Link className="underline" to={value}>
{t('View details')}
</Link>
) : (
''
)
}
/>
</AgGrid>
); );
}; };
@@ -13,10 +13,6 @@ fragment ExplorerDeterministicOrderFields on Order {
remaining remaining
size size
rejectionReason rejectionReason
peggedOrder {
reference
offset
}
party { party {
id id
} }
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } }; export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID']; orderId: Types.Scalars['ID'];
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } }; export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql` export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order { fragment ExplorerDeterministicOrderFields on Order {
@@ -29,10 +29,6 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
remaining remaining
size size
rejectionReason rejectionReason
peggedOrder {
reference
offset
}
party { party {
id id
} }
@@ -12,7 +12,7 @@ type Amend = components['schemas']['v1OrderAmendment'];
function renderAmendOrderDetails( function renderAmendOrderDetails(
id: string, id: string,
version: number | undefined, version: number,
amend: Amend, amend: Amend,
mocks: MockedResponse[] mocks: MockedResponse[]
) { ) {
@@ -25,11 +25,7 @@ function renderAmendOrderDetails(
); );
} }
function renderExistingAmend( function renderExistingAmend(id: string, version: number, amend: Amend) {
id: string,
version: number | undefined,
amend: Amend
) {
const mocks = [ const mocks = [
{ {
request: { request: {
@@ -53,7 +49,6 @@ function renderExistingAmend(
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC, timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
price: '200', price: '200',
side: 'BUY', side: 'BUY',
peggedOrder: null,
remaining: '99', remaining: '99',
rejectionReason: 'rejection', rejectionReason: 'rejection',
reference: '123', reference: '123',
@@ -82,56 +77,6 @@ function renderExistingAmend(
}, },
}, },
}, },
{
request: {
query: ExplorerDeterministicOrderDocument,
variables: {
orderId: '123',
},
},
result: {
data: {
orderByID: {
__typename: 'Order',
id: '123',
type: 'GTT',
status: Schema.OrderStatus.STATUS_ACTIVE,
version: 100,
createdAt: '123',
updatedAt: '456',
expiresAt: '789',
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
peggedOrder: null,
price: '200',
side: 'BUY',
remaining: '99',
rejectionReason: 'rejection',
reference: '123',
size: '200',
party: {
__typename: 'Party',
id: '234',
},
market: {
__typename: 'Market',
id: 'amend-to-order-latest-version',
state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: '5',
tradableInstrument: {
instrument: {
name: 'amend-to-order-latest-version-test',
product: {
__typename: 'Future',
quoteName: '123',
},
},
},
},
},
},
},
},
{ {
request: { request: {
query: ExplorerMarketDocument, query: ExplorerMarketDocument,
@@ -212,15 +157,4 @@ describe('Amend order details', () => {
expect(await res.findByText('New price')).toBeInTheDocument(); expect(await res.findByText('New price')).toBeInTheDocument();
expect(await res.findByText('-7879')).toBeInTheDocument(); expect(await res.findByText('-7879')).toBeInTheDocument();
}); });
it('Fetches latest version when version is not specified', async () => {
const amend: Amend = {
price: '-7879',
};
const res = renderExistingAmend('123', undefined, amend);
expect(
await res.findByText('amend-to-order-latest-version')
).toBeInTheDocument();
});
}); });
@@ -12,7 +12,7 @@ import { wrapperClasses } from './deterministic-order-details';
export interface AmendOrderDetailsProps { export interface AmendOrderDetailsProps {
id: string; id: string;
amend: components['schemas']['v1OrderAmendment']; amend: components['schemas']['v1OrderAmendment'];
// Version to fetch. Latest is provided by default // Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
version?: number; version?: number;
} }
@@ -34,11 +34,13 @@ export function getSideDeltaColour(delta: string): string {
* @param param0 * @param param0
* @returns * @returns
*/ */
const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => { const AmendOrderDetails = ({
const variables = version ? { orderId: id, version } : { orderId: id }; id,
version = 0,
amend,
}: AmendOrderDetailsProps) => {
const { data, error } = useExplorerDeterministicOrderQuery({ const { data, error } = useExplorerDeterministicOrderQuery({
variables, variables: { orderId: id, version },
}); });
if (error || (data && !data.orderByID)) { if (error || (data && !data.orderByID)) {
@@ -5,7 +5,6 @@ import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time'; import { Time } from '../time';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels'; import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market'; import SizeInMarket from '../size-in-market/size-in-market';
import { TxOrderPeggedReference } from '../txs/details/order/tx-order-peg';
export interface DeterministicOrderDetailsProps { export interface DeterministicOrderDetailsProps {
id: string; id: string;
@@ -69,35 +68,25 @@ const DeterministicOrderDetails = ({
<span className="mx-5 text-base">@</span> <span className="mx-5 text-base">@</span>
<PriceInMarket price={o.price} marketId={o.market.id} /> <PriceInMarket price={o.price} marketId={o.market.id} />
</h2> </h2>
<p className="text-gray-200"> <p className="text-gray-500 mb-4">
In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />. In <MarketLink id={o.market.id} /> at <Time date={o.createdAt} />.
</p> </p>
{o.peggedOrder ? (
<p className="text-gray-200">
{t('Price peg')}:{' '}
<TxOrderPeggedReference
side={o.side}
reference={o.peggedOrder.reference}
offset={o.peggedOrder.offset}
marketId={o.market.id}
/>
</p>
) : null}
{o.reference ? ( {o.reference ? (
<p className="text-gray-500 mt-4"> <p className="text-gray-500 mb-4">
<span>{t('Reference')}</span>: {o.reference} <span>{t('Reference')}</span>: {o.reference}
</p> </p>
) : null} ) : null}
<div className="grid md:grid-cols-4 gap-x-6 mt-4"> <div className="grid md:grid-cols-4 gap-x-6">
<div className="mb-12 md:mb-0"> {version !== 0 ? null : (
<h2 className="text-2xl font-bold text-dark mb-4"> <div className="mb-12 md:mb-0">
{t('Status')} <h2 className="text-2xl font-bold text-dark mb-4">
</h2> {t('Status')}
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize"> </h2>
{statusText[o.status]} <h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
</h5> {statusText[o.status]}
</div> </h5>
</div>
)}
<div className="mb-12 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2> <h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
@@ -106,6 +95,17 @@ const DeterministicOrderDetails = ({
</h5> </h5>
</div> </div>
{version !== 0 ? null : (
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Remaining')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.remaining} marketId={o.market.id} />
</h5>
</div>
)}
<div className=""> <div className="">
<h2 className="text-2xl font-bold text-dark mb-4"> <h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')} {t('Version')}
@@ -31,7 +31,6 @@ const mock = {
side: 'SIDE_BUY', side: 'SIDE_BUY',
remaining: '100', remaining: '100',
size: '100', size: '100',
peggedOrder: null,
party: { party: {
id: '456', id: '456',
}, },
@@ -1,6 +1,7 @@
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals'; import { VoteProgress } from '@vegaprotocol/proposals';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { import type {
@@ -8,7 +9,7 @@ import type {
VegaValueFormatterParams, VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { RowClickedEvent, ColDef } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { import {
@@ -63,128 +64,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
title: '', title: '',
content: null, content: null,
}); });
const columnDefs = useMemo<ColDef[]>(
() => [
{
colId: 'title',
headerName: t('Title'),
field: 'rationale.title',
flex: 2,
wrapText: true,
},
{
colId: 'type',
maxWidth: 180,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Type'),
field: 'terms.change.__typename',
},
{
maxWidth: 100,
headerName: t('State'),
field: 'state',
valueFormatter: ({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
},
},
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{
colId: 'cDate',
maxWidth: 150,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Closing date'),
field: 'terms.closingDatetime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
},
},
{
colId: 'eDate',
maxWidth: 150,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Enactment date'),
field: 'terms.enactmentDatetime',
valueFormatter: ({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
},
},
{
colId: 'actions',
minWidth: window.innerWidth > BREAKPOINT_MD ? 221 : 80,
maxWidth: 221,
sortable: false,
filter: false,
resizable: false,
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button className="underline max-md:hidden" onClick={openDialog}>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
},
},
],
[requiredMajorityPercentage, tokenLink]
);
return ( return (
<> <>
<AgGrid <AgGrid
@@ -203,7 +83,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
filterParams: { buttons: ['reset'] }, filterParams: { buttons: ['reset'] },
autoHeight: true, autoHeight: true,
}} }}
columnDefs={columnDefs}
suppressCellFocus={true} suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => { onRowClicked={({ data, event }: RowClickedEvent) => {
if ( if (
@@ -215,7 +94,128 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
window.open(proposalPage, '_blank'); window.open(proposalPage, '_blank');
} }
}} }}
/> >
<AgGridColumn
colId="title"
headerName={t('Title')}
field="rationale.title"
flex={2}
wrapText={true}
/>
<AgGridColumn
colId="type"
maxWidth={180}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Type')}
field="terms.change.__typename"
/>
<AgGridColumn
maxWidth={100}
headerName={t('State')}
field="state"
valueFormatter={({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
}}
/>
<AgGridColumn
colId="voting"
maxWidth={100}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Voting')}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
}}
/>
<AgGridColumn
colId="cDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Closing date')}
field="terms.closingDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="eDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Enactment date')}
field="terms.enactmentDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="actions"
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
maxWidth={221}
sortable={false}
filter={false}
resizable={false}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button
className="underline max-md:hidden"
onClick={openDialog}
>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
}}
/>
</AgGrid>
<JsonViewerDialog <JsonViewerDialog
open={dialog.open} open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })} onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
@@ -1,24 +1,19 @@
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go // https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([ export const ErrorCodes = new Map([
[51, 'Transaction failed validation'], [51, 'Transaction failed validation'],
[60, 'Transaction could not be decoded'], [60, 'Transaction could not be decoded'],
[70, 'Error'], [70, 'Error'],
[71, 'Partial success/error'],
[80, 'Unknown command'], [80, 'Unknown command'],
[89, 'Rejected as spam'], [89, 'Rejected as spam'],
[0, 'Success'], [0, 'Success'],
]); ]);
export const successCodes = new Set([0, 71]); export const successCodes = new Set([0]);
interface ChainResponseCodeProps { interface ChainResponseCodeProps {
code: number; code: number;
hideLabel?: boolean; hideLabel?: boolean;
error?: string; error?: string;
hideIfOk?: boolean;
small?: boolean;
} }
/** /**
@@ -30,22 +25,10 @@ export const ChainResponseCode = ({
code, code,
hideLabel = false, hideLabel = false,
error, error,
hideIfOk = false,
small = false,
}: ChainResponseCodeProps) => { }: ChainResponseCodeProps) => {
if (hideIfOk && code === 0) {
return null;
}
const isSuccess = successCodes.has(code); const isSuccess = successCodes.has(code);
const size = small ? 3 : 4;
const successColour = const icon = isSuccess ? '✅' : '❌';
code === 71 ? '!fill-vega-orange' : '!fill-vega-green-600';
const icon = isSuccess ? (
<Icon size={size} name="tick-circle" className={`${successColour}`} />
) : (
<Icon size={size} name="cross" className="!fill-vega-pink-500" />
);
const label = ErrorCodes.get(code) || 'Unknown response code'; const label = ErrorCodes.get(code) || 'Unknown response code';
// Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245 // Hack for batches with many errors - see https://github.com/vegaprotocol/vega/issues/7245
@@ -53,28 +36,18 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error; error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return ( return (
<Tooltip <div title={`Response code: ${code} - ${label}`} className="inline-block">
description={ <span
<span> className="mr-2"
Response code: {code} - {label} aria-label={isSuccess ? 'Success' : 'Warning'}
</span> role="img"
} >
> {icon}
<div className="mt-[-1px] inline-block"> </span>
<span {hideLabel ? null : <span>{label}</span>}
className="mr-2" {!hideLabel && !!displayError ? (
aria-label={isSuccess ? 'Success' : 'Warning'} <span className="ml-1 whitespace-pre">&mdash;&nbsp;{displayError}</span>
role="img" ) : null}
> </div>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
{!hideLabel && !!displayError ? (
<span className="ml-1 whitespace-pre">
&mdash;&nbsp;{displayError}
</span>
) : null}
</div>
</Tooltip>
); );
}; };
@@ -1,113 +0,0 @@
import { render } from '@testing-library/react';
import type { TxDetailsOrderProps } from './tx-order-peg';
import { TxOrderPeggedReference, getMarketDecimals } from './tx-order-peg';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { PeggedReference, Side } from '@vegaprotocol/types';
// Mock the useExplorerMarketQuery hook
jest.mock('../../../links/market-link/__generated__/Market', () => ({
useExplorerMarketQuery: jest.fn().mockReturnValue({
data: {
market: { decimalPlaces: 0 },
},
loading: false,
}),
}));
describe('getSettlementAsset', () => {
it('should return the decimal places if data is defined', () => {
const data = {
market: {
__typename: 'Market',
id: '123',
decimalPlaces: 8,
},
};
const result = getMarketDecimals(data as Partial<ExplorerMarketQuery>);
expect(result).toEqual(8);
});
it('should return 0 if data is undefined', () => {
const result = getMarketDecimals(undefined);
expect(result).toEqual(0);
});
});
describe('TxOrderPeggedReference', () => {
beforeEach(() => {
// Mock the useExplorerMarketQuery hook return value
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: {
settlementAsset: 'some-settlement-asset',
},
loading: false,
});
});
afterEach(() => {
jest.resetAllMocks();
});
it('should render the offset and reference correctly', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: PeggedReference.PEGGED_REFERENCE_MID,
marketId: 'some-market-id',
};
const { getByTestId } = render(<TxOrderPeggedReference {...props} />);
expect(getByTestId('pegged-reference')).toHaveTextContent('Mid + 10');
});
it('should return null if the reference is "PEGGED_REFERENCE_UNSPECIFIED"', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: 'PEGGED_REFERENCE_UNSPECIFIED',
marketId: 'some-market-id',
};
const { container } = render(<TxOrderPeggedReference {...props} />);
expect(container.firstChild).toBeNull();
});
it('should render the offset without formatting initially, then render the formatted version', () => {
const props: TxDetailsOrderProps = {
side: Side.SIDE_BUY,
offset: '10',
reference: PeggedReference.PEGGED_REFERENCE_BEST_ASK,
marketId: 'some-market-id',
};
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: null,
loading: true,
});
const screen = render(<TxOrderPeggedReference {...props} />);
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
'Ask + 10'
);
(useExplorerMarketQuery as jest.Mock).mockReturnValue({
data: {
market: {
decimalPlaces: 10,
},
},
loading: false,
});
screen.rerender(<TxOrderPeggedReference {...props} />);
expect(screen.getByTestId('pegged-reference')).toHaveTextContent(
'Ask + 0.000000001'
);
});
});
@@ -1,72 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { TableCell, TableRow } from '../../../table';
import type { VegaPeggedReference } from '../liquidity-provision/liquidity-provision-details';
import { Side, PeggedReferenceMapping } from '@vegaprotocol/types';
import { useExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import type { ExplorerMarketQuery } from '../../../links/market-link/__generated__/Market';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
export interface TxDetailsOrderProps {
offset: string;
reference: VegaPeggedReference;
marketId: string;
side: Side;
}
export function getMarketDecimals(
data: ExplorerMarketQuery | undefined
): number {
return data?.market?.decimalPlaces || 0;
}
/**
* Summarises an order's peg
*/
export const TxOrderPeggedReferenceRow = ({
offset,
reference,
marketId,
side,
}: TxDetailsOrderProps) => {
return (
<TableRow modifier="bordered">
<TableCell>{t('Pegged order')}</TableCell>
<TableCell>
<TxOrderPeggedReference
side={side}
offset={offset}
reference={reference}
marketId={marketId}
/>
</TableCell>
</TableRow>
);
};
export const TxOrderPeggedReference = ({
offset,
reference,
marketId,
side,
}: TxDetailsOrderProps) => {
const { data, loading } = useExplorerMarketQuery({
variables: { id: marketId },
});
const direction = side === Side.SIDE_BUY ? '+' : '-';
const decimalPlaces = getMarketDecimals(data);
if (reference === 'PEGGED_REFERENCE_UNSPECIFIED') {
return null;
}
return (
<span data-testid="pegged-reference">
{PeggedReferenceMapping[reference]}&nbsp;
{direction}&nbsp;
{!loading && data
? addDecimalsFormatNumber(offset, decimalPlaces)
: offset}
</span>
);
};
@@ -6,32 +6,10 @@ import {
SPECIAL_CASE_NETWORK_ID, SPECIAL_CASE_NETWORK_ID,
} from '../../../../links/party-link/party-link'; } from '../../../../links/party-link/party-link';
import SizeInAsset from '../../../../size-in-asset/size-in-asset'; import SizeInAsset from '../../../../size-in-asset/size-in-asset';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { headerClasses, wrapperClasses } from '../transfer-details'; import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer'; import type { Transfer } from '../transfer-details';
type Transfer = components['schemas']['commandsv1Transfer'];
type AccountTypes = components['schemas']['vegaAccountType'];
const AccountType: Record<AccountTypes, string> = {
ACCOUNT_TYPE_UNSPECIFIED: 'Unspecified',
ACCOUNT_TYPE_INSURANCE: 'Insurance',
ACCOUNT_TYPE_SETTLEMENT: 'Settlement',
ACCOUNT_TYPE_MARGIN: 'Margin',
ACCOUNT_TYPE_GENERAL: 'General',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: 'Infrastructure',
ACCOUNT_TYPE_FEES_LIQUIDITY: 'Liquidity',
ACCOUNT_TYPE_FEES_MAKER: 'Maker',
ACCOUNT_TYPE_BOND: 'Bond',
ACCOUNT_TYPE_EXTERNAL: 'External',
ACCOUNT_TYPE_GLOBAL_INSURANCE: 'Global Insurance',
ACCOUNT_TYPE_GLOBAL_REWARD: 'Global Reward',
ACCOUNT_TYPE_PENDING_TRANSFERS: 'Pending Transfers',
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: 'Maker Paid Fees',
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: 'Maker Received Fees',
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: 'LP Received Fees',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: 'Market Proposers',
ACCOUNT_TYPE_HOLDING: 'Holding',
};
interface TransferParticipantsProps { interface TransferParticipantsProps {
transfer: Transfer; transfer: Transfer;
@@ -52,22 +30,22 @@ export function TransferParticipants({
}: TransferParticipantsProps) { }: TransferParticipantsProps) {
// This mapping is required as the global account types require a type to be set, while // This mapping is required as the global account types require a type to be set, while
// the underlying protobufs allow for every field to be undefined. // the underlying protobufs allow for every field to be undefined.
const fromAcct: AccountTypes = const fromAcct =
transfer.fromAccountType && transfer.fromAccountType &&
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED' transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? transfer.fromAccountType ? AccountType[transfer.fromAccountType]
: 'ACCOUNT_TYPE_GENERAL'; : AccountType.ACCOUNT_TYPE_GENERAL;
const fromAccountTypeLabel: string = transfer.fromAccountType const fromAccountTypeLabel = transfer.fromAccountType
? AccountType[fromAcct] ? AccountTypeMapping[fromAcct]
: 'Unknown'; : 'Unknown';
const toAcct: AccountTypes = const toAcct =
transfer.toAccountType && transfer.toAccountType &&
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED' transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? transfer.toAccountType ? AccountType[transfer.toAccountType]
: 'ACCOUNT_TYPE_GENERAL'; : AccountType.ACCOUNT_TYPE_GENERAL;
const toAccountTypeLabel = transfer.fromAccountType const toAccountTypeLabel = transfer.fromAccountType
? AccountType[toAcct] ? AccountTypeMapping[toAcct]
: 'Unknown'; : 'Unknown';
return ( return (
@@ -27,9 +27,9 @@ export function TransferRepeat({ recurring }: TransferRepeatProps) {
<div className={wrapperClasses}> <div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Active epochs')}</h2> <h2 className={headerClasses}>{t('Active epochs')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6"> <div className="relative block rounded-lg py-6 text-center p-6">
<div> <p>
<EpochOverview id={recurring.startEpoch} /> <EpochOverview id={recurring.startEpoch} />
</div> </p>
<p className="leading-10 my-2"> <p className="leading-10 my-2">
<IconForEpoch <IconForEpoch
start={recurring.startEpoch} start={recurring.startEpoch}
@@ -37,13 +37,13 @@ export function TransferRepeat({ recurring }: TransferRepeatProps) {
current={data?.epoch.id} current={data?.epoch.id}
/> />
</p> </p>
<div> <p>
{recurring.endEpoch ? ( {recurring.endEpoch ? (
<EpochOverview id={recurring.endEpoch} /> <EpochOverview id={recurring.endEpoch} />
) : ( ) : (
<span>{t('Forever')}</span> <span>{t('Forever')}</span>
)} )}
</div> </p>
</div> </div>
</div> </div>
); );
@@ -8,7 +8,7 @@ import { DispatchMetricLabels } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy']; export type Strategy = components['schemas']['vegaDispatchStrategy'];
const metricLabels: Record<Metric, string> = { const metricLabels = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric', DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels, ...DispatchMetricLabels,
}; };
@@ -3,7 +3,7 @@ import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards'; import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants'; import { TransferParticipants } from './blocks/transfer-participants';
export type Recurring = components['schemas']['commandsv1RecurringTransfer']; export type Recurring = components['schemas']['v1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses = export const wrapperClasses =
@@ -1,86 +0,0 @@
import { render } from '@testing-library/react';
import { TxDetailsLiquidityAmendment } from './tx-liquidity-amend';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
describe('TxDetailsLiquidityAmendment', () => {
const mockTxData = {
hash: 'test',
command: {
liquidityProvisionAmendment: {
marketId: 'BTC-USD',
commitmentAmount: 100,
fee: '0.01',
},
},
};
const mockPubKey = '123';
const mockBlockData = {
result: {
block: {
header: {
height: '123',
},
},
},
};
it('should render the component with correct data', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquidityAmendment
txData={mockTxData as BlockExplorerTransactionResult}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(getByText('Market')).toBeInTheDocument();
expect(getByText('BTC-USD')).toBeInTheDocument();
expect(getByText('Commitment amount')).toBeInTheDocument();
expect(getByText('100')).toBeInTheDocument();
expect(getByText('Fee')).toBeInTheDocument();
expect(getByText('1%')).toBeInTheDocument();
});
it('should display awaiting message when tx data is undefined', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquidityAmendment
txData={undefined}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(
getByText('Awaiting Block Explorer transaction details')
).toBeInTheDocument();
});
it('should display awaiting message when liquidityProvisionAmendment is undefined', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquidityAmendment
txData={{ command: {} } as BlockExplorerTransactionResult}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(
getByText('Awaiting Block Explorer transaction details')
).toBeInTheDocument();
});
});
@@ -7,7 +7,6 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer'; import type { components } from '../../../../types/explorer';
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details'; import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
import PriceInMarket from '../../price-in-market/price-in-market'; import PriceInMarket from '../../price-in-market/price-in-market';
import BigNumber from 'bignumber.js';
export type LiquidityAmendment = export type LiquidityAmendment =
components['schemas']['v1LiquidityProvisionAmendment']; components['schemas']['v1LiquidityProvisionAmendment'];
@@ -35,10 +34,6 @@ export const TxDetailsLiquidityAmendment = ({
txData.command.liquidityProvisionAmendment; txData.command.liquidityProvisionAmendment;
const marketId: string = amendment.marketId || '-'; const marketId: string = amendment.marketId || '-';
const fee = amendment.fee
? new BigNumber(amendment.fee).times(100).toString()
: '-';
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@@ -68,7 +63,7 @@ export const TxDetailsLiquidityAmendment = ({
{amendment.fee ? ( {amendment.fee ? (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Fee')}</TableCell> <TableCell>{t('Fee')}</TableCell>
<TableCell>{fee}%</TableCell> <TableCell>{amendment.fee}%</TableCell>
</TableRow> </TableRow>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
@@ -1,86 +0,0 @@
import { render } from '@testing-library/react';
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
describe('TxDetailsLiquiditySubmission', () => {
const mockTxData = {
hash: 'test',
command: {
liquidityProvisionSubmission: {
marketId: 'BTC-USD',
commitmentAmount: 100,
fee: '0.01',
},
},
};
const mockPubKey = '123';
const mockBlockData = {
result: {
block: {
header: {
height: '123',
},
},
},
};
it('should render the component with correct data', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquiditySubmission
txData={mockTxData as BlockExplorerTransactionResult}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(getByText('Market')).toBeInTheDocument();
expect(getByText('BTC-USD')).toBeInTheDocument();
expect(getByText('Commitment amount')).toBeInTheDocument();
expect(getByText('100')).toBeInTheDocument();
expect(getByText('Fee')).toBeInTheDocument();
expect(getByText('1%')).toBeInTheDocument();
});
it('should display awaiting message when tx data is undefined', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquiditySubmission
txData={undefined}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(
getByText('Awaiting Block Explorer transaction details')
).toBeInTheDocument();
});
it('should display awaiting message when liquidityProvisionSubmission is undefined', () => {
const { getByText } = render(
<MockedProvider>
<MemoryRouter>
<TxDetailsLiquiditySubmission
txData={{ command: {} } as BlockExplorerTransactionResult}
pubKey={mockPubKey}
blockData={mockBlockData as TendermintBlocksResponse}
/>
</MemoryRouter>
</MockedProvider>
);
expect(
getByText('Awaiting Block Explorer transaction details')
).toBeInTheDocument();
});
});
@@ -7,7 +7,6 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer'; import type { components } from '../../../../types/explorer';
import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details'; import { LiquidityProvisionDetails } from './liquidity-provision/liquidity-provision-details';
import PriceInMarket from '../../price-in-market/price-in-market'; import PriceInMarket from '../../price-in-market/price-in-market';
import BigNumber from 'bignumber.js';
export type LiquiditySubmission = export type LiquiditySubmission =
components['schemas']['v1LiquidityProvisionSubmission']; components['schemas']['v1LiquidityProvisionSubmission'];
@@ -34,10 +33,6 @@ export const TxDetailsLiquiditySubmission = ({
txData.command.liquidityProvisionSubmission; txData.command.liquidityProvisionSubmission;
const marketId: string = submission.marketId || '-'; const marketId: string = submission.marketId || '-';
const fee = submission.fee
? new BigNumber(submission.fee).times(100).toString()
: '-';
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@@ -67,7 +62,7 @@ export const TxDetailsLiquiditySubmission = ({
{submission.fee ? ( {submission.fee ? (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Fee')}</TableCell> <TableCell>{t('Fee')}</TableCell>
<TableCell>{fee}%</TableCell> <TableCell>{submission.fee}%</TableCell>
</TableRow> </TableRow>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
@@ -7,7 +7,6 @@ import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids'; import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import DeterministicOrderDetails from '../../order-details/deterministic-order-details'; import DeterministicOrderDetails from '../../order-details/deterministic-order-details';
import Hash from '../../links/hash'; import Hash from '../../links/hash';
import { TxOrderPeggedReferenceRow } from './order/tx-order-peg';
interface TxDetailsOrderProps { interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -30,8 +29,6 @@ export const TxDetailsOrder = ({
return <>{t('Awaiting Block Explorer transaction details')}</>; return <>{t('Awaiting Block Explorer transaction details')}</>;
} }
const marketId = txData.command.orderSubmission.marketId || '-'; const marketId = txData.command.orderSubmission.marketId || '-';
const reference = txData.command.orderSubmission.peggedOrder;
const side = txData.command.orderSubmission.side;
let deterministicId = ''; let deterministicId = '';
@@ -66,14 +63,6 @@ export const TxDetailsOrder = ({
<MarketLink id={marketId} /> <MarketLink id={marketId} />
</TableCell> </TableCell>
</TableRow> </TableRow>
{reference ? (
<TxOrderPeggedReferenceRow
side={side}
offset={reference.offset}
reference={reference.reference}
marketId={marketId}
/>
) : null}
</TableWithTbody> </TableWithTbody>
{deterministicId.length > 0 ? ( {deterministicId.length > 0 ? (
@@ -4,7 +4,6 @@ import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint
import { TxDetailsShared } from './shared/tx-details-shared'; import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table'; import { TableCell, TableRow, TableWithTbody } from '../../table';
import ProposalLink from '../../links/proposal-link/proposal-link'; import ProposalLink from '../../links/proposal-link/proposal-link';
import { VoteIcon } from '../../vote-icon/vote-icon';
interface TxProposalVoteProps { interface TxProposalVoteProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -31,22 +30,27 @@ export const TxProposalVote = ({
return <>{t('Awaiting Block Explorer transaction details')}</>; return <>{t('Awaiting Block Explorer transaction details')}</>;
} }
const vote = txData.command.voteSubmission.value === 'VALUE_YES'; const vote = txData.command.voteSubmission.value ? '👍' : '👎';
return ( return (
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} /> <TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Proposal details')}</TableCell> <TableCell>{t('Proposal details')}</TableCell>
<TableCell> <TableCell>
<ProposalLink id={txData.command.voteSubmission.proposalId} /> <ProposalLink id={txData.command.voteSubmission.proposalId} />
</TableCell> </TableCell>
</TableRow> </TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal')}</TableCell>
<TableCell>{txData.command.voteSubmission.proposalId}</TableCell>
</TableRow>
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableCell>{t('Vote')}</TableCell> <TableCell>{t('Vote')}</TableCell>
<TableCell> <TableCell>{vote}</TableCell>
<VoteIcon vote={vote} />
</TableCell>
</TableRow> </TableRow>
</TableWithTbody> </TableWithTbody>
); );
@@ -1,56 +0,0 @@
import type { Proposal } from './tx-proposal';
import { proposalRequiresSignatureBundle } from './tx-proposal';
describe('proposalRequiresSignatureBundle', () => {
it('should return false for freeform proposals, which do not require a signature bundle to enact', () => {
const mock = {
terms: {
newFreeform: {},
},
};
expect(proposalRequiresSignatureBundle(mock)).toEqual(false);
});
it('should return false for newMarket proposals, which do not require a signature bundle to enact', () => {
const mock = {
terms: {
newMarket: {},
},
};
expect(proposalRequiresSignatureBundle(mock)).toEqual(false);
});
it('should return true for newAsset proposals, which do require a signature bundle to enact', () => {
const mock = {
terms: {
newAsset: {},
},
};
expect(proposalRequiresSignatureBundle(mock)).toEqual(true);
});
it('should return true for updateAsset proposals, which do require a signature bundle to enact', () => {
const mock = {
terms: {
updateAsset: {},
},
};
expect(proposalRequiresSignatureBundle(mock)).toEqual(true);
});
it('should return false when bad data is supplied', () => {
expect(
proposalRequiresSignatureBundle(false as unknown as Proposal)
).toEqual(false);
expect(
proposalRequiresSignatureBundle(undefined as unknown as Proposal)
).toEqual(false);
expect(
proposalRequiresSignatureBundle({ test: false } as unknown as Proposal)
).toEqual(false);
});
});
@@ -28,16 +28,11 @@ interface TxProposalProps {
* @returns boolean True if a signature bundle is required. Used to fetch a signature bundle * @returns boolean True if a signature bundle is required. Used to fetch a signature bundle
*/ */
export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean { export function proposalRequiresSignatureBundle(proposal?: Proposal): boolean {
const proposalsThatRequireBundles = ['newAsset', 'updateAsset'];
if (!proposal?.terms) { if (!proposal?.terms) {
return false; return false;
} }
return !!['newAsset', 'updateAsset'].filter((requiredIfExists) =>
return ( has(proposal.terms, requiredIfExists)
proposalsThatRequireBundles.filter((requiredIfExists) =>
has(proposal.terms, requiredIfExists)
).length > 0
); );
} }
@@ -86,7 +81,6 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset; const tx = proposal.terms?.newAsset || proposal.terms?.updateAsset;
// This component is not rendered if no bundle is required
const SignatureBundleComponent = proposal.terms?.newAsset const SignatureBundleComponent = proposal.terms?.newAsset
? ProposalSignatureBundleNewAsset ? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset; : ProposalSignatureBundleUpdateAsset;
@@ -1,28 +0,0 @@
import { t } from '@vegaprotocol/i18n';
export interface FilterLabelProps {
filters: Set<string>;
}
/**
* Renders the list (currently limited to 1) of filters set by the
* Transaction Filter
*/
export function FilterLabel({ filters }: FilterLabelProps) {
if (!filters || filters.size !== 1) {
return (
<span data-testid="filter-empty" className="uppercase">
{t('Filter')}
</span>
);
}
return (
<div data-testid="filter-selected">
<span className="uppercase">{t('Filters')}:</span>&nbsp;
<code className="bg-vega-light-150 dark:bg-vega-light-300 px-2 rounded-md capitalize dark:text-black">
{Array.from(filters)[0]}
</code>
</div>
);
}
@@ -1,21 +0,0 @@
import { render, screen } from '@testing-library/react';
import { TxsFilter } from './tx-filter';
import type { FilterOption } from './tx-filter';
describe('TxsFilter', () => {
it('renders holding text when nothing is selected', () => {
const filters: Set<FilterOption> = new Set([]);
const setFilters = jest.fn();
render(<TxsFilter filters={filters} setFilters={setFilters} />);
expect(screen.getByTestId('filter-empty')).toBeInTheDocument();
expect(screen.getByText('Filter')).toBeInTheDocument();
});
it('renders the submit order filter as selected', () => {
const filters: Set<FilterOption> = new Set(['Submit Order']);
const setFilters = jest.fn();
render(<TxsFilter filters={filters} setFilters={setFilters} />);
expect(screen.getByTestId('filter-selected')).toBeInTheDocument();
expect(screen.getByText('Submit Order')).toBeInTheDocument();
});
});
@@ -1,164 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItemIndicator,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
DropdownMenuSubContent,
Icon,
Button,
} from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction } from 'react';
import { FilterLabel } from './tx-filter-label';
// All possible transaction types. Should be generated.
export type FilterOption =
| 'Amend LiquidityProvision Order'
| 'Amend Order'
| 'Batch Market Instructions'
| 'Cancel LiquidityProvision Order'
| 'Cancel Order'
| 'Cancel Transfer Funds'
| 'Chain Event'
| 'Delegate'
| 'Ethereum Key Rotate Submission'
| 'Issue Signatures'
| 'Key Rotate Submission'
| 'Liquidity Provision Order'
| 'Node Signature'
| 'Node Vote'
| 'Proposal'
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Submit Oracle Data'
| 'Submit Order'
| 'Transfer Funds'
| 'Undelegate'
| 'Validator Heartbeat'
| 'Vote on Proposal'
| 'Withdraw';
// Alphabetised list of transaction types to appear at the top level
export const PrimaryFilterOptions: FilterOption[] = [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Cancel Transfer Funds',
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
'Undelegate',
'Vote on Proposal',
'Withdraw',
];
// Alphabetised list of transaction types to nest under a 'More...' submenu
export const SecondaryFilterOptions: FilterOption[] = [
'Chain Event',
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
];
export const AllFilterOptions: FilterOption[] = [
...PrimaryFilterOptions,
...SecondaryFilterOptions,
];
export interface TxFilterProps {
filters: Set<FilterOption>;
setFilters: Dispatch<SetStateAction<Set<FilterOption>>>;
}
/**
* Renders a structured dropdown menu of all of the available transaction
* types. It allows a user to select one transaction type to view. Later
* it will support multiple selection, but until the API supports that it is
* one or all.
* @param filters null or Set of transaction types
* @param setFilters A function to update the filters prop
* @returns
*/
export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
return (
<DropdownMenu
modal={false}
trigger={
<DropdownMenuTrigger className="ml-0">
<Button size="xs" data-testid="filter-trigger">
<FilterLabel filters={filters} />
</Button>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent>
{filters.size > 0 ? null : (
<>
<DropdownMenuCheckboxItem
onCheckedChange={() => setFilters(new Set(AllFilterOptions))}
>
{t('Clear filters')} <Icon name="cross" />
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
</>
)}
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={() => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{t('More Types')}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{SecondaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
};
@@ -1,113 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { TxsListNavigation } from './tx-list-navigation';
const NOOP = () => {
return;
};
describe('TxsListNavigation', () => {
it('renders transaction list navigation', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeInTheDocument();
expect(screen.getByText('Older')).toBeInTheDocument();
});
it('calls previousPage when "Newer" button is clicked', () => {
const previousPageMock = jest.fn();
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={previousPageMock}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
fireEvent.click(screen.getByText('Newer'));
expect(previousPageMock).toHaveBeenCalledTimes(1);
});
it('calls nextPage when "Older" button is clicked', () => {
const nextPageMock = jest.fn();
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={nextPageMock}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={true}
>
<span></span>
</TxsListNavigation>
);
fireEvent.click(screen.getByText('Older'));
expect(nextPageMock).toHaveBeenCalledTimes(1);
});
it('disables "Older" button if hasMoreTxs is false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={false}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Older')).toBeDisabled();
});
it('disables "Newer" button if hasPreviousPage is false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={true}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeDisabled();
});
it('disables both buttons when more and previous are false', () => {
render(
<TxsListNavigation
refreshTxs={NOOP}
nextPage={NOOP}
previousPage={NOOP}
hasMoreTxs={false}
hasPreviousPage={false}
>
<span></span>
</TxsListNavigation>
);
expect(screen.getByText('Newer')).toBeDisabled();
expect(screen.getByText('Older')).toBeDisabled();
});
});
@@ -1,63 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { BlocksRefetch } from '../blocks';
import { Button } from '@vegaprotocol/ui-toolkit';
export interface TxListNavigationProps {
refreshTxs: () => void;
nextPage: () => void;
previousPage: () => void;
loading?: boolean;
hasPreviousPage: boolean;
hasMoreTxs: boolean;
children: React.ReactNode;
}
/**
* Displays a list of transactions with filters and controls to navigate through the list.
*
* @returns {JSX.Element} Transaction List and controls
*/
export const TxsListNavigation = ({
refreshTxs,
nextPage,
previousPage,
hasMoreTxs,
hasPreviousPage,
children,
loading = false,
}: TxListNavigationProps) => {
return (
<>
<menu className="mb-2 w-full ">{children}</menu>
<menu className="mb-2 w-full">
<BlocksRefetch refetch={refreshTxs} />
<div className="float-right">
<Button
className="mr-2"
size="xs"
disabled={!hasPreviousPage || loading}
onClick={() => {
previousPage();
}}
>
{t('Newer')}
</Button>
<Button
size="xs"
disabled={!hasMoreTxs || loading}
onClick={() => {
nextPage();
}}
>
{t('Older')}
</Button>
</div>
<div className="float-right mr-2">
{loading ? (
<span className="text-vega-light-300">{t('Loading...')}</span>
) : null}
</div>
</menu>
</>
);
};
@@ -1,6 +1,5 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../types/explorer'; import type { components } from '../../../types/explorer';
import { VoteIcon } from '../vote-icon/vote-icon';
interface TxOrderTypeProps { interface TxOrderTypeProps {
orderType: string; orderType: string;
@@ -16,7 +15,7 @@ interface StringMap {
const displayString: StringMap = { const displayString: StringMap = {
OrderSubmission: 'Order Submission', OrderSubmission: 'Order Submission',
'Submit Order': 'Order', 'Submit Order': 'Order',
OrderCancellation: 'Cancel order', OrderCancellation: 'Order Cancellation',
OrderAmendment: 'Order Amendment', OrderAmendment: 'Order Amendment',
VoteSubmission: 'Vote Submission', VoteSubmission: 'Vote Submission',
WithdrawSubmission: 'Withdraw Submission', WithdrawSubmission: 'Withdraw Submission',
@@ -24,7 +23,6 @@ const displayString: StringMap = {
LiquidityProvisionSubmission: 'LP order', LiquidityProvisionSubmission: 'LP order',
'Liquidity Provision Order': 'LP order', 'Liquidity Provision Order': 'LP order',
LiquidityProvisionCancellation: 'LP cancel', LiquidityProvisionCancellation: 'LP cancel',
'Cancel LiquidityProvision Order': 'LP cancel',
LiquidityProvisionAmendment: 'LP update', LiquidityProvisionAmendment: 'LP update',
'Amend LiquidityProvision Order': 'Amend LP', 'Amend LiquidityProvision Order': 'Amend LP',
ProposalSubmission: 'Governance Proposal', ProposalSubmission: 'Governance Proposal',
@@ -37,34 +35,12 @@ const displayString: StringMap = {
UndelegateSubmission: 'Undelegation', UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation', KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable', StateVariableProposal: 'State Variable',
'State Variable Proposal': 'State Variable',
Transfer: 'Transfer', Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer', CancelTransfer: 'Cancel Transfer',
'Cancel Transfer Funds': 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat', ValidatorHeartbeat: 'Heartbeat',
'Validator Heartbeat': 'Heartbeat',
'Batch Market Instructions': 'Batch', 'Batch Market Instructions': 'Batch',
'Stop Orders Submission': 'Stop',
StopOrdersSubmission: 'Stop',
StopOrdersCancellation: 'Cancel stop',
'Stop Orders Cancellation': 'Cancel stop',
}; };
export function getLabelForOrderType(
orderType: string,
command: components['schemas']['v1InputData']
): string {
if (command.orderSubmission) {
if (command.orderSubmission.peggedOrder) {
return 'Peg';
}
if (command.orderSubmission.icebergOpts) {
return 'Iceberg';
}
}
return 'Order';
}
/** /**
* Given a proposal, will return a specific label * Given a proposal, will return a specific label
* @param chainEvent * @param chainEvent
@@ -136,8 +112,6 @@ export function getLabelForChainEvent(
return t('Signer threshold'); return t('Signer threshold');
} }
return t('Multisig update'); return t('Multisig update');
} else if (chainEvent.contractCall) {
return t('Contract call');
} }
return t('Chain Event'); return t('Chain Event');
} }
@@ -163,15 +137,12 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType; let type = displayString[orderType] || orderType;
let colours = let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-250'; 'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
// This will get unwieldy and should probably produce a different colour of tag // This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) { if (type === 'Chain Event' && !!command?.chainEvent) {
type = getLabelForChainEvent(command.chainEvent); type = getLabelForChainEvent(command.chainEvent);
colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink'; colours = 'text-white dark-text-white bg-vega-pink dark:bg-vega-pink';
} else if (type === 'Validator Heartbeat') {
colours =
'text-white dark-text-white bg-vega-light-200 dark:bg-vega-dark-100';
} else if (type === 'Proposal' || type === 'Governance Proposal') { } else if (type === 'Proposal' || type === 'Governance Proposal') {
if (command && !!command.proposalSubmission) { if (command && !!command.proposalSubmission) {
type = getLabelForProposal(command.proposalSubmission); type = getLabelForProposal(command.proposalSubmission);
@@ -179,17 +150,6 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
colours = 'text-black bg-vega-yellow'; colours = 'text-black bg-vega-yellow';
} }
if (type === 'Vote on Proposal') {
return (
<VoteIcon
vote={command?.voteSubmission?.value === 'VALUE_YES'}
yesText="Proposal vote"
noText="Proposal vote"
useVoteColour={false}
/>
);
}
if (type === 'Vote on Proposal' || type === 'Vote Submission') { if (type === 'Vote on Proposal' || type === 'Vote Submission') {
colours = 'text-black bg-vega-yellow'; colours = 'text-black bg-vega-yellow';
} }
@@ -197,7 +157,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
return ( return (
<div <div
data-testid="tx-type" data-testid="tx-type"
className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`} className={`text-sm rounded-md leading-none px-2 py-2 inline-block ${colours}`}
> >
{type} {type}
</div> </div>
@@ -1,4 +1,3 @@
import { MockedProvider } from '@apollo/client/testing';
import { TxsInfiniteListItem } from './txs-infinite-list-item'; import { TxsInfiniteListItem } from './txs-infinite-list-item';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@@ -84,22 +83,21 @@ describe('Txs infinite list item', () => {
it('renders data correctly', () => { it('renders data correctly', () => {
render( render(
<MockedProvider> <MemoryRouter>
<MemoryRouter> <TxsInfiniteListItem
<TxsInfiniteListItem type="testType"
type="testType" submitter="testPubKey"
submitter="testPubKey" hash="testTxHash"
hash="testTxHash" block="1"
block="1" code={0}
code={0} command={{}}
command={{}} />
/> </MemoryRouter>
</MemoryRouter>
</MockedProvider>
); );
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash'); expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey'); expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
expect(screen.getByTestId('tx-type')).toHaveTextContent('testType'); expect(screen.getByTestId('tx-type')).toHaveTextContent('testType');
expect(screen.getByTestId('tx-block')).toHaveTextContent('1'); expect(screen.getByTestId('tx-block')).toHaveTextContent('1');
expect(screen.getByTestId('tx-success')).toHaveTextContent('Success: ✅');
}); });
}); });
@@ -1,3 +1,4 @@
import React from 'react';
import { TruncatedLink } from '../truncate/truncated-link'; import { TruncatedLink } from '../truncate/truncated-link';
import { Routes } from '../../routes/route-names'; import { Routes } from '../../routes/route-names';
import { TxOrderType } from './tx-order-type'; import { TxOrderType } from './tx-order-type';
@@ -5,25 +6,8 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
import { toHex } from '../search/detect-search'; import { toHex } from '../search/detect-search';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code'; import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import isNumber from 'lodash/isNumber'; import isNumber from 'lodash/isNumber';
import { PartyLink } from '../links';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import type { Screen } from '@vegaprotocol/react-helpers';
import { useMemo } from 'react';
const DEFAULT_TRUNCATE_LENGTH = 7; const TRUNCATE_LENGTH = 10;
export function getIdTruncateLength(screen: Screen): number {
if (['xxxl', 'xxl'].includes(screen)) {
return 64;
} else if (['xl', 'lg', 'md'].includes(screen)) {
return 32;
}
return DEFAULT_TRUNCATE_LENGTH;
}
export function shouldTruncateParty(screen: Screen): boolean {
return !['xxxl', 'xxl', 'xl'].includes(screen);
}
export const TxsInfiniteListItem = ({ export const TxsInfiniteListItem = ({
hash, hash,
@@ -33,12 +17,6 @@ export const TxsInfiniteListItem = ({
block, block,
command, command,
}: Partial<BlockExplorerTransactionResult>) => { }: Partial<BlockExplorerTransactionResult>) => {
const { screenSize } = useScreenDimensions();
const idTruncateLength = useMemo(
() => getIdTruncateLength(screenSize),
[screenSize]
);
if ( if (
!hash || !hash ||
!submitter || !submitter ||
@@ -51,40 +29,68 @@ export const TxsInfiniteListItem = ({
} }
return ( return (
<tr <div
data-testid="transaction-row" data-testid="transaction-row"
className="transaction-row text-left items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item py-[2px]" className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
> >
<td <div
className="text-sm leading-none whitespace-nowrap font-mono" className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="tx-hash" data-testid="tx-hash"
> >
{isNumber(code) ? ( <span className="md:hidden uppercase text-vega-dark-300">
<ChainResponseCode code={code} hideLabel={true} hideIfOk={true} /> ID:&nbsp;
) : ( </span>
code
)}
<TruncatedLink <TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`} to={`/${Routes.TX}/${toHex(hash)}`}
text={hash} text={hash}
startChars={idTruncateLength} startChars={TRUNCATE_LENGTH}
endChars={0} endChars={TRUNCATE_LENGTH}
/> />
</td> </div>
<td className="text-sm leading-none"> <div
className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="md:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
</div>
<div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} command={command} /> <TxOrderType orderType={type} command={command} />
</td> </div>
<td className="text-sm leading-none" data-testid="pub-key"> <div
<PartyLink truncate={shouldTruncateParty(screenSize)} id={submitter} /> className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
</td> data-testid="tx-block"
<td className="text-sm items-center font-mono" data-testid="tx-block"> >
<span className="md:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<TruncatedLink <TruncatedLink
to={`/${Routes.BLOCKS}/${block}`} to={`/${Routes.BLOCKS}/${block}`}
text={block} text={block}
startChars={5} startChars={TRUNCATE_LENGTH}
endChars={5} endChars={TRUNCATE_LENGTH}
/> />
</td> </div>
</tr> <div
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="md:hidden uppercase text-vega-dark-300">
Success:&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
) : (
code
)}
</div>
</div>
); );
}; };
@@ -1,9 +1,8 @@
import { TxsInfiniteList } from './txs-infinite-list'; import { TxsInfiniteList } from './txs-infinite-list';
import { render, screen } from '@testing-library/react'; import { render, screen, fireEvent, act } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response'; import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import { Side } from '@vegaprotocol/types'; import { Side } from '@vegaprotocol/types';
import { MockedProvider } from '@apollo/client/testing';
const generateTxs = (number: number): BlockExplorerTransactionResult[] => { const generateTxs = (number: number): BlockExplorerTransactionResult[] => {
return Array.from(Array(number)).map((_) => ({ return Array.from(Array(number)).map((_) => ({
@@ -41,7 +40,7 @@ describe('Txs infinite list', () => {
it('should display a "no items" message when no items provided', () => { it('should display a "no items" message when no items provided', () => {
render( render(
<TxsInfiniteList <TxsInfiniteList
txs={undefined as unknown as BlockExplorerTransactionResult[]} txs={undefined}
areTxsLoading={false} areTxsLoading={false}
hasMoreTxs={false} hasMoreTxs={false}
loadMoreTxs={() => null} loadMoreTxs={() => null}
@@ -49,7 +48,23 @@ describe('Txs infinite list', () => {
/> />
); );
expect(screen.getByTestId('emptylist')).toBeInTheDocument(); expect(screen.getByTestId('emptylist')).toBeInTheDocument();
expect(screen.getByText('No transactions found')).toBeInTheDocument(); expect(
screen.getByText('This chain has 0 transactions')
).toBeInTheDocument();
});
it('error is displayed at item level', () => {
const txs = generateTxs(1);
render(
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={() => null}
error={Error('test error!')}
/>
);
expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
}); });
it('item renders data of n length into list of n length', () => { it('item renders data of n length into list of n length', () => {
@@ -58,22 +73,85 @@ describe('Txs infinite list', () => {
const txs = generateTxs(7); const txs = generateTxs(7);
render( render(
<MemoryRouter> <MemoryRouter>
<MockedProvider> <TxsInfiniteList
<TxsInfiniteList txs={txs}
txs={txs} areTxsLoading={false}
areTxsLoading={false} hasMoreTxs={false}
hasMoreTxs={false} loadMoreTxs={() => null}
loadMoreTxs={() => null} error={undefined}
error={undefined} />
/>
</MockedProvider>
</MemoryRouter> </MemoryRouter>
); );
expect( expect(
screen screen
.getByTestId('transactions-list') .getByTestId('infinite-scroll-wrapper')
.querySelectorAll('.transaction-row') .querySelectorAll('.txs-infinite-list-item')
).toHaveLength(7); ).toHaveLength(7);
}); });
it('tries to load more items when required to initially fill the list', () => {
// For example, if initially rendering 15, the bottom of the list is
// in view of the viewport, and the callback should be executed
const txs = generateTxs(15);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={true}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
expect(callback.mock.calls.length).toEqual(1);
});
it('does not try to load more items if there are no more', () => {
const txs = generateTxs(3);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={false}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
expect(callback.mock.calls.length).toEqual(0);
});
it('loads more items is called when scrolled', () => {
const txs = generateTxs(14);
const callback = jest.fn();
render(
<MemoryRouter>
<TxsInfiniteList
txs={txs}
areTxsLoading={false}
hasMoreTxs={true}
loadMoreTxs={callback}
error={undefined}
/>
</MemoryRouter>
);
act(() => {
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
target: { scrollY: 2000 },
});
});
expect(callback.mock.calls.length).toEqual(1);
});
}); });
@@ -1,4 +1,8 @@
import React from 'react';
import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { TxsInfiniteListItem } from './txs-infinite-list-item'; import { TxsInfiniteListItem } from './txs-infinite-list-item';
import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response'; import type { BlockExplorerTransactionResult } from '../../routes/types/block-explorer-response';
import EmptyList from '../empty-list/empty-list'; import EmptyList from '../empty-list/empty-list';
@@ -7,46 +11,71 @@ import { Loader } from '@vegaprotocol/ui-toolkit';
interface TxsInfiniteListProps { interface TxsInfiniteListProps {
hasMoreTxs: boolean; hasMoreTxs: boolean;
areTxsLoading: boolean | undefined; areTxsLoading: boolean | undefined;
txs: BlockExplorerTransactionResult[]; txs: BlockExplorerTransactionResult[] | undefined;
loadMoreTxs: () => void; loadMoreTxs: () => void;
error: Error | undefined; error: Error | undefined;
className?: string; className?: string;
hasFilters?: boolean;
} }
interface ItemProps { interface ItemProps {
tx: BlockExplorerTransactionResult; index: BlockExplorerTransactionResult;
style: React.CSSProperties;
isLoading: boolean;
error: Error | undefined;
} }
const Item = ({ tx }: ItemProps) => { // eslint-disable-next-line @typescript-eslint/no-empty-function
const { hash, submitter, type, command, block, code, index: blockIndex } = tx; const NOOP = () => {};
return (
<TxsInfiniteListItem const Item = ({ index, style, isLoading, error }: ItemProps) => {
type={type} let content;
code={code} if (error) {
command={command} content = t(`Cannot fetch transaction`);
submitter={submitter} } else if (isLoading) {
hash={hash} content = <Loader />;
block={block} } else {
index={blockIndex} const {
/> hash,
); submitter,
type,
command,
block,
code,
index: blockIndex,
} = index;
content = (
<TxsInfiniteListItem
type={type}
code={code}
command={command}
submitter={submitter}
hash={hash}
block={block}
index={blockIndex}
/>
);
}
return <div style={style}>{content}</div>;
}; };
export const TxsInfiniteList = ({ export const TxsInfiniteList = ({
hasMoreTxs,
areTxsLoading, areTxsLoading,
txs, txs,
loadMoreTxs,
error,
className, className,
hasFilters = false,
}: TxsInfiniteListProps) => { }: TxsInfiniteListProps) => {
if (!txs || txs.length === 0) { const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm'].includes(screenSize);
if (!txs) {
if (!areTxsLoading) { if (!areTxsLoading) {
return ( return (
<EmptyList <EmptyList
heading={t('No transactions found')} heading={t('This chain has 0 transactions')}
label={ label={t('Check back soon')}
hasFilters ? t('Try a different filter') : t('Check back soon')
}
/> />
); );
} else { } else {
@@ -54,26 +83,56 @@ export const TxsInfiniteList = ({
} }
} }
// If there are more items to be loaded then add an extra row to hold a loading indicator.
const itemCount = hasMoreTxs ? txs.length + 1 : txs.length;
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
// eslint-disable-next-line @typescript-eslint/no-empty-function
const loadMoreItems = areTxsLoading ? NOOP : loadMoreTxs;
// Every row is loaded except for our loading indicator row.
const isItemLoaded = (index: number) => !hasMoreTxs || index < txs.length;
return ( return (
<div className="overflow-scroll"> <div className={className} data-testid="transactions-list">
<table className={className} data-testid="transactions-list"> <div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<thead> <div className="col-span-3">
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left"> <span className="hidden xl:inline">{t('Transaction')} &nbsp;</span>
<th> <span>ID</span>
<span className="hidden xl:inline">{t('Txn')} &nbsp;</span> </div>
<span>ID</span> <div className="col-span-3">{t('Submitted By')}</div>
</th> <div className="col-span-2">{t('Type')}</div>
<th>{t('Type')}</th> <div className="col-span-1">{t('Block')}</div>
<th className="text-left">{t('From')}</th> <div className="col-span-1">{t('Success')}</div>
<th>{t('Block')}</th> </div>
</tr> <div data-testid="infinite-scroll-wrapper">
</thead> <InfiniteLoader
<tbody> isItemLoaded={isItemLoaded}
{txs.map((t) => ( itemCount={itemCount}
<Item key={t.hash} tx={t} /> loadMoreItems={loadMoreItems}
))} >
</tbody> {({ onItemsRendered, ref }) => (
</table> <List
className="List"
height={995}
itemCount={itemCount}
itemSize={isStacked ? 134 : 50}
onItemsRendered={onItemsRendered}
ref={ref}
width={'100%'}
>
{({ index, style }) => (
<Item
index={txs[index]}
style={style}
isLoading={!isItemLoaded(index)}
error={error}
/>
)}
</List>
)}
</InfiniteLoader>
</div>
</div> </div>
); );
}; };
@@ -1,17 +1,23 @@
import { Table, TableRow } from '../table'; import { Routes } from '../../routes/route-names';
import { TruncatedLink } from '../truncate/truncated-link';
import { TxOrderType } from './tx-order-type';
import { Table, TableRow, TableCell } from '../table';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers'; import { useFetch } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response'; import type { BlockExplorerTransactions } from '../../routes/types/block-explorer-response';
import isNumber from 'lodash/isNumber';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import { getTxsDataUrl } from '../../hooks/use-txs-data'; import { getTxsDataUrl } from '../../hooks/use-txs-data';
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list'; import EmptyList from '../empty-list/empty-list';
import { TxsInfiniteListItem } from './txs-infinite-list-item';
interface TxsPerBlockProps { interface TxsPerBlockProps {
blockHeight: string; blockHeight: string;
txCount: number; txCount: number;
} }
const truncateLength = 5;
export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => { export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
const filters = `filters[block.height]=${blockHeight}`; const filters = `filters[block.height]=${blockHeight}`;
const url = getTxsDataUrl({ limit: txCount.toString(), filters }); const url = getTxsDataUrl({ limit: txCount.toString(), filters });
@@ -27,23 +33,53 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
<thead> <thead>
<TableRow modifier="bordered" className="font-mono"> <TableRow modifier="bordered" className="font-mono">
<td>{t('Transaction')}</td> <td>{t('Transaction')}</td>
<td>{t('Type')}</td>
<td>{t('From')}</td> <td>{t('From')}</td>
<td>{t('Block')}</td> <td>{t('Type')}</td>
<td>{t('Status')}</td>
</TableRow> </TableRow>
</thead> </thead>
<tbody> <tbody>
{data.transactions.map( {data.transactions.map(
({ hash, submitter, type, command, code, block }) => { ({ hash, submitter, type, command, code }) => {
return ( return (
<TxsInfiniteListItem <TableRow
block={block} modifier="bordered"
hash={hash} key={hash}
submitter={submitter} data-testid="transaction-row"
type={type} >
command={command} <TableCell
code={code} modifier="bordered"
/> className="pr-12 font-mono"
>
<TruncatedLink
to={`/${Routes.TX}/${hash}`}
text={hash}
startChars={truncateLength}
endChars={truncateLength}
/>
</TableCell>
<TableCell
modifier="bordered"
className="pr-12 font-mono"
>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={truncateLength}
endChars={truncateLength}
/>
</TableCell>
<TableCell modifier="bordered">
<TxOrderType orderType={type} command={command} />
</TableCell>
<TableCell modifier="bordered" className="text">
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
) : (
code
)}
</TableCell>
</TableRow>
); );
} }
)} )}
@@ -1,50 +0,0 @@
import { render } from '@testing-library/react';
import { VoteIcon } from './vote-icon';
describe('Vote TX icon', () => {
it('should use the text For by default for yes votes', () => {
const yes = render(<VoteIcon vote={true} />);
expect(yes.getByTestId('label')).toHaveTextContent('For');
});
it('should use the yesText for yes votes if specified', () => {
const yes = render(<VoteIcon vote={true} yesText="Test" />);
expect(yes.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the tick icon for yes votes', () => {
const no = render(<VoteIcon vote={true} />);
expect(no.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
it('should use the text Against by default for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByTestId('label')).toHaveTextContent('Against');
});
it('should use the noText for no votes if specified', () => {
const no = render(<VoteIcon vote={false} noText="Test" />);
expect(no.getByTestId('label')).toHaveTextContent('Test');
});
it('should display the delete icon for no votes', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.getByRole('img')).toHaveAttribute('aria-label', 'delete icon');
});
it('useVoteColour prop can be used to override coloured background', () => {
const no = render(<VoteIcon vote={false} />);
expect(no.container.children[0]).toHaveClass('bg-vega-pink-550');
const monochromeNo = render(
<VoteIcon vote={false} useVoteColour={false} />
);
expect(monochromeNo.container.children[0]).not.toHaveClass(
'bg-vega-pink-550'
);
expect(monochromeNo.container.children[0]).toHaveClass('bg-vega-dark-200');
});
});
@@ -1,71 +0,0 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
export interface VoteIconProps {
// True is a yes vote, false is undefined or no vorte
vote: boolean;
// Defaults to 'For', but can be any text
yesText?: string;
// Defaults to 'Against', but can be any text
noText?: string;
// If set to false the background will not be coloured
useVoteColour?: boolean;
}
function getBgColour(useVoteColour: boolean, vote: boolean) {
if (useVoteColour === false) {
return 'bg-vega-dark-200';
}
return vote ? 'bg-vega-green-550' : 'bg-vega-pink-550';
}
function getFillColour(useVoteColour: boolean, vote: boolean) {
if (useVoteColour === false) {
return 'white';
}
return vote ? 'vega-green-300' : 'vega-pink-300';
}
function getTextColour(useVoteColour: boolean, vote: boolean) {
if (useVoteColour === false) {
return 'white';
}
return vote ? 'vega-green-200' : 'vega-pink-200';
}
/**
* Displays a lozenge with an icon representing the way a user voted for a proposal.
* The yes and no text can be overridden
*
* @returns
*/
export function VoteIcon({
vote,
useVoteColour = true,
yesText = 'For',
noText = 'Against',
}: VoteIconProps) {
const label = vote ? yesText : noText;
const icon: IconName = vote ? 'tick-circle' : 'delete';
const bg = getBgColour(useVoteColour, vote);
const fill = getFillColour(useVoteColour, vote);
const text = getTextColour(useVoteColour, vote);
return (
<div
className={`voteicon inline-block py-0 px-2 py rounded-md text-white whitespace-nowrap leading-tight sm align-top ${bg}`}
>
<Icon
name={icon}
size={3}
className={`mr-2 p-0 mb-[-1px] fill-${fill}`}
/>
<span className={`text-${text}`} data-testid="label">
{label}
</span>
</div>
);
}
+33 -69
View File
@@ -10,18 +10,16 @@ import isNumber from 'lodash/isNumber';
export interface TxsStateProps { export interface TxsStateProps {
txsData: BlockExplorerTransactionResult[]; txsData: BlockExplorerTransactionResult[];
hasMoreTxs: boolean; hasMoreTxs: boolean;
cursor: string; lastCursor: string;
previousCursors: string[];
hasPreviousPage: boolean;
} }
export interface IUseTxsData { export interface IUseTxsData {
limit: number; limit?: number;
filters?: string; filters?: string;
} }
interface IGetTxsDataUrl { interface IGetTxsDataUrl {
limit: string; limit?: string;
filters?: string; filters?: string;
} }
@@ -35,96 +33,62 @@ export const getTxsDataUrl = ({ limit, filters }: IGetTxsDataUrl) => {
// Hacky fix for param as array // Hacky fix for param as array
let urlAsString = url.toString(); let urlAsString = url.toString();
if (filters) { if (filters) {
urlAsString += '&' + filters.replace(' ', '%20'); urlAsString += '&' + filters;
} }
return urlAsString; return urlAsString;
}; };
export const useTxsData = ({ limit, filters }: IUseTxsData) => { export const useTxsData = ({ limit, filters }: IUseTxsData) => {
const [ const [{ txsData, hasMoreTxs, lastCursor }, setTxsState] =
{ txsData, hasMoreTxs, cursor, previousCursors, hasPreviousPage }, useState<TxsStateProps>({
setTxsState, txsData: [],
] = useState<TxsStateProps>({ hasMoreTxs: true,
txsData: [], lastCursor: '',
hasMoreTxs: false, });
previousCursors: [],
cursor: '',
hasPreviousPage: false,
});
const url = getTxsDataUrl({ limit: limit.toString(), filters }); const url = getTxsDataUrl({ limit: limit?.toString(), filters });
const { const {
state: { data, error, loading }, state: { data, error, loading },
refetch, refetch,
} = useFetch<BlockExplorerTransactions>(url, {}, true); } = useFetch<BlockExplorerTransactions>(url, {}, false);
useEffect(() => { useEffect(() => {
if (!loading && data && isNumber(data.transactions.length)) { if (data && isNumber(data?.transactions?.length)) {
setTxsState((prev) => { setTxsState((prev) => ({
return { txsData: [...prev.txsData, ...data.transactions],
...prev, hasMoreTxs: data.transactions.length > 0,
txsData: data.transactions, lastCursor:
hasMoreTxs: data.transactions.length >= limit, data.transactions[data.transactions.length - 1]?.cursor || '',
cursor: data?.transactions.at(-1)?.cursor || '', }));
};
});
} }
}, [loading, setTxsState, data, limit]); }, [setTxsState, data]);
const nextPage = useCallback(() => {
const c = data?.transactions.at(0)?.cursor;
const newPreviousCursors = c ? [...previousCursors, c] : previousCursors;
setTxsState((prev) => ({
...prev,
hasPreviousPage: true,
previousCursors: newPreviousCursors,
}));
const loadTxs = useCallback(() => {
return refetch({ return refetch({
limit, limit: limit,
before: cursor, before: lastCursor,
}); });
}, [data, previousCursors, cursor, limit, refetch]); }, [lastCursor, limit, refetch]);
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 refreshTxs = useCallback(async () => { const refreshTxs = useCallback(async () => {
setTxsState(() => ({ setTxsState((prev) => ({
...prev,
lastCursor: '',
hasMoreTxs: true,
txsData: [], txsData: [],
cursor: '',
previousCursors: [],
hasMoreTxs: false,
hasPreviousPage: false,
})); }));
}, [setTxsState]);
refetch({ limit });
}, [setTxsState, limit, refetch, filters]); // eslint-disable-line react-hooks/exhaustive-deps
return { return {
txsData, data,
loading, loading,
error, error,
txsData,
hasMoreTxs, hasMoreTxs,
hasPreviousPage, lastCursor,
previousCursors,
cursor,
refreshTxs, refreshTxs,
nextPage, loadTxs,
previousPage,
}; };
}; };
@@ -1,5 +1,7 @@
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import type { TendermintBlocksResponse } from '../tendermint-blocks-response';
import { RouteTitle } from '../../../components/route-title'; import { RouteTitle } from '../../../components/route-title';
import { TimeAgo } from '../../../components/time-ago'; import { TimeAgo } from '../../../components/time-ago';
import { import {
@@ -12,7 +14,7 @@ import { TxsPerBlock } from '../../../components/txs/txs-per-block';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { Routes } from '../../route-names'; import { Routes } from '../../route-names';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useBlockInfo } from '@vegaprotocol/tendermint'; import { useFetch } from '@vegaprotocol/react-helpers';
import { NodeLink } from '../../../components/links'; import { NodeLink } from '../../../components/links';
import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useDocumentTitle } from '../../../hooks/use-document-title';
import EmptyList from '../../../components/empty-list/empty-list'; import EmptyList from '../../../components/empty-list/empty-list';
@@ -22,7 +24,10 @@ const Block = () => {
useDocumentTitle(['Blocks', `Block #${block}`]); useDocumentTitle(['Blocks', `Block #${block}`]);
const { const {
state: { data: blockData, loading, error }, state: { data: blockData, loading, error },
} = useBlockInfo(Number(block)); } = useFetch<TendermintBlocksResponse>(
`${DATA_SOURCES.tendermintUrl}/block?height=${block}`,
{ cache: 'force-cache' }
);
return ( return (
<section> <section>
+1 -3
View File
@@ -21,7 +21,6 @@ import {
import { Footer } from '../components/footer/footer'; import { Footer } from '../components/footer/footer';
import { Header } from '../components/header'; import { Header } from '../components/header';
import { Routes } from './route-names'; import { Routes } from './route-names';
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
const DialogsContainer = () => { const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore(); const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -40,7 +39,6 @@ export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME)); const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment(); const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto'; const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
useExplorerNodeNamesLazyQuery();
return ( return (
<> <>
@@ -51,7 +49,7 @@ export const Layout = () => {
'grid grid-rows-[auto_1fr_auto] grid-cols-1', 'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200', 'border-vega-light-200 dark:border-vega-dark-200',
'antialiased text-black dark:text-white', 'antialiased text-black dark:text-white',
'relative' 'overflow-hidden relative'
)} )}
> >
<div> <div>
@@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { marketInfoWithDataProvider } from '@vegaprotocol/markets'; import { marketInfoWithDataProvider } from '@vegaprotocol/market-info';
import { PageTitle } from '../../components/page-helpers/page-title'; import { PageTitle } from '../../components/page-helpers/page-title';
export const MarketPage = () => { export const MarketPage = () => {
@@ -1,6 +1,6 @@
import { useScrollToLocation } from '../../hooks/scroll-to-location'; import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
import { marketsProvider } from '@vegaprotocol/markets'; import { marketsProvider } from '@vegaprotocol/market-list';
import { RouteTitle } from '../../components/route-title'; import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@@ -1,13 +1,12 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import { OracleDetailsType, isInternalSourceType } from './oracle-details-type'; import { OracleDetailsType } from './oracle-details-type';
import type { SourceType } from './oracle'; import type { SourceTypeName } from './oracle-details-type';
import { PropertyKeyType } from '@vegaprotocol/types';
function renderComponent(type: SourceType) { function renderComponent(type: SourceTypeName) {
return <OracleDetailsType sourceType={type} />; return <OracleDetailsType type={type} />;
} }
function renderWrappedComponent(type: SourceType) { function renderWrappedComponent(type: SourceTypeName) {
return ( return (
<table> <table>
<tbody>{renderComponent(type)}</tbody> <tbody>{renderComponent(type)}</tbody>
@@ -15,53 +14,19 @@ function renderWrappedComponent(type: SourceType) {
); );
} }
function mock(name: string): SourceType {
return {
sourceType: {
filters: [
{
__typename: 'Filter',
key: {
name,
type: PropertyKeyType.TYPE_STRING,
},
},
],
},
};
}
describe('Oracle type view', () => { describe('Oracle type view', () => {
it('Renders nothing when type is null', () => { it('Renders nothing when type is null', () => {
const res = render(renderComponent(null as unknown as SourceType)); const res = render(renderComponent(null as unknown as SourceTypeName));
expect(res.container).toBeEmptyDOMElement(); expect(res.container).toBeEmptyDOMElement();
}); });
it('Renders Internal time for internal sources - timestamp', () => { it('Renders Internal time for internal sources', () => {
const s = mock('vegaprotocol.builtin.timestamp'); const res = render(renderWrappedComponent('DataSourceDefinitionInternal'));
expect(isInternalSourceType(s)).toEqual(true); expect(res.getByText('Internal time')).toBeInTheDocument();
const res = render(renderWrappedComponent(s));
expect(res.getByText('Internal data')).toBeInTheDocument();
});
it('Renders Internal time for internal sources - potential future types', () => {
const s = mock('vegaprotocol.builtin.boolean');
expect(isInternalSourceType(s)).toEqual(true);
const res = render(renderWrappedComponent(s));
expect(res.getByText('Internal data')).toBeInTheDocument();
});
it('Renders External data otherwise - prices.external.whatever', () => {
const s = mock('prices.external.whatever');
expect(isInternalSourceType(s)).toEqual(false);
const res = render(renderWrappedComponent(s));
expect(res.getByText('External data')).toBeInTheDocument();
}); });
it('Renders External data otherwise', () => { it('Renders External data otherwise', () => {
const s = mock('prices.external.vegaprotocol.builtin.'); const res = render(renderWrappedComponent('DataSourceDefinitionExternal'));
expect(isInternalSourceType(s)).toEqual(false);
const res = render(renderWrappedComponent(s));
expect(res.getByText('External data')).toBeInTheDocument(); expect(res.getByText('External data')).toBeInTheDocument();
}); });
}); });
@@ -1,51 +1,27 @@
import { TableRow, TableCell, TableHeader } from '../../../components/table'; import { TableRow, TableCell, TableHeader } from '../../../components/table';
import type { SourceType } from './oracle'; import type { SourceType } from './oracle';
/** export type SourceTypeName = SourceType['__typename'] | undefined;
* Basic function to determine if a source is internal or external.
*
* This should be distinguishable using __typename, but the type is incorrectly
* reported at the moment, so instead we check the filters.
*
* @param s SourceType
* @returns boolean True if the source is internal
*/
export function isInternalSourceType(s: SourceType) {
if ('filters' in s.sourceType) {
const filters = s.sourceType.filters;
if (filters) {
return (
filters?.filter((f) => {
return f.key.name?.indexOf('vegaprotocol.builtin.') === 0;
}).length > 0
);
}
}
return false;
}
interface OracleDetailsTypeProps { interface OracleDetailsTypeProps {
sourceType: SourceType; type: SourceTypeName;
} }
/** /**
* Renders a a single table row for the Oracle Details view that shows * Renders a a single table row for the Oracle Details view that shows
* if the oracle is using the internal time oracle or external data * if the oracle is using the internal time oracle or external data
*/ */
export function OracleDetailsType({ sourceType }: OracleDetailsTypeProps) { export function OracleDetailsType({ type }: OracleDetailsTypeProps) {
if (!sourceType) { if (!type) {
return null; return null;
} }
const isInternal = isInternalSourceType(sourceType);
return ( return (
<TableRow modifier="bordered"> <TableRow modifier="bordered">
<TableHeader scope="row">Type</TableHeader> <TableHeader scope="row">Type</TableHeader>
<TableCell modifier="bordered"> <TableCell modifier="bordered">
{isInternal ? 'Internal data' : 'External data'} {type === 'DataSourceDefinitionInternal'
? 'Internal time'
: 'External data'}
</TableCell> </TableCell>
</TableRow> </TableRow>
); );
@@ -2,15 +2,12 @@ import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import type { SourceType } from './oracle'; import type { SourceType } from './oracle';
import { OracleSigners } from './oracle-signers'; import { OracleSigners } from './oracle-signers';
import { MockedProvider } from '@apollo/client/testing';
function renderComponent(sourceType: SourceType) { function renderComponent(sourceType: SourceType) {
return ( return (
<MockedProvider> <MemoryRouter>
<MemoryRouter> <OracleSigners sourceType={sourceType} />
<OracleSigners sourceType={sourceType} /> </MemoryRouter>
</MemoryRouter>
</MockedProvider>
); );
} }
@@ -53,7 +53,7 @@ export const OracleDetails = ({
<OracleLink id={id} /> <OracleLink id={id} />
</TableCell> </TableCell>
</TableRow> </TableRow>
<OracleDetailsType sourceType={sourceType} /> <OracleDetailsType type={sourceType.__typename} />
<OracleSigners sourceType={sourceType} /> <OracleSigners sourceType={sourceType} />
<OracleMarkets id={id} /> <OracleMarkets id={id} />
<TableRow modifier="bordered"> <TableRow modifier="bordered">
@@ -1,40 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
export const Disclaimer = () => {
return (
<section>
<div className="px-40 max-sm:px-0 max-md:px-10 mb-4 max-w-5xl">
<RouteTitle data-testid="disclaimer-header">
{t('Disclaimer')}
</RouteTitle>
<p className="mt-3">
The Vega Block Explorer is an application that allows users to, among
other things, browse through blocks, view wallet addresses, network
hashrate, transaction data and other key information on the Vega
blockchain. It is free, public and open source software. Software
upgrades may contain bugs or security vulnerabilities that might
result in loss of functionality.
</p>
<p className="mt-3">
The Vega Block Explorer uses data from nodes on the Vega Blockchain.
The developers of the Vega Block Explorer do not operate or run the
Vega Blockchain or any other blockchain.
</p>
<p className="mt-3 font-semibold">
The Vega Block Explorer is provided as is. The developers of the
Vega Block Explorer make no representations or warranties of any kind,
whether express or implied, statutory or otherwise regarding the Vega
Block Explorer. They disclaim all warranties of merchantability,
quality, fitness for purpose. They disclaim all warranties that the
Vega Block Explorer is free of harmful components or errors.
</p>
<p className="mt-3 font-bold">
No developer of the Vega Block Explorer accepts any responsibility
for, or liability to users in connection with their use of the Vega
Block Explorer.
</p>
</div>
</section>
);
};
@@ -54,7 +54,7 @@ export const PartyBlockStake = ({
{p?.stakingSummary.currentStakeAvailable ? ( {p?.stakingSummary.currentStakeAvailable ? (
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow noBorder={true}> <KeyValueTableRow noBorder={true}>
<div>{t('Associated to key')}</div> <div>{t('Available stake')}</div>
<div> <div>
<GovernanceAssetBalance <GovernanceAssetBalance
price={p.stakingSummary.currentStakeAvailable} price={p.stakingSummary.currentStakeAvailable}
@@ -62,7 +62,7 @@ export const PartyBlockStake = ({
</div> </div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow noBorder={true}> <KeyValueTableRow noBorder={true}>
<div>{t('Staked to validator')}</div> <div>{t('Active stake')}</div>
<div> <div>
<GovernanceAssetBalance price={linkedStake || '0'} /> <GovernanceAssetBalance price={linkedStake || '0'} />
</div> </div>
@@ -1,6 +1,6 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useMemo, useState } from 'react'; import { useMemo } from 'react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { SubHeading } from '../../../components/sub-heading'; import { SubHeading } from '../../../components/sub-heading';
import { toNonHex } from '../../../components/search/detect-search'; import { toNonHex } from '../../../components/search/detect-search';
@@ -14,11 +14,8 @@ import { PartyBlockStake } from './components/party-block-stake';
import { PartyBlockAccounts } from './components/party-block-accounts'; import { PartyBlockAccounts } from './components/party-block-accounts';
import { isValidPartyId } from './components/party-id-error'; import { isValidPartyId } from './components/party-id-error';
import { useDataProvider } from '@vegaprotocol/data-provider'; import { useDataProvider } from '@vegaprotocol/data-provider';
import { TxsListNavigation } from '../../../components/txs/tx-list-navigation';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
const Party = () => { const Party = () => {
const [filters, setFilters] = useState(new Set(AllFilterOptions));
const { party } = useParams<{ party: string }>(); const { party } = useParams<{ party: string }>();
useDocumentTitle(['Public keys', party || '-']); useDocumentTitle(['Public keys', party || '-']);
@@ -27,24 +24,10 @@ const Party = () => {
const partyId = toNonHex(party ? party : ''); const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions(); const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]); const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
const baseFilters = `filters[tx.submitter]=${partyId}`; const filters = `filters[tx.submitter]=${partyId}`;
const f = const { hasMoreTxs, loadTxs, error, txsData, loading } = useTxsData({
filters && filters.size === 1 limit: 10,
? `${baseFilters}&filters[cmd.type]=${Array.from(filters)[0]}` filters,
: baseFilters;
const {
hasMoreTxs,
nextPage,
previousPage,
error,
refreshTxs,
loading,
txsData,
hasPreviousPage,
} = useTxsData({
limit: 25,
filters: f,
}); });
const variables = useMemo(() => ({ partyId }), [partyId]); const variables = useMemo(() => ({ partyId }), [partyId]);
@@ -67,7 +50,7 @@ const Party = () => {
text: t('Go back'), text: t('Go back'),
action: () => navigate(-1), action: () => navigate(-1),
className: 'py-1', className: 'py-1',
size: 'small', size: 'sm',
}} }}
/> />
</div> </div>
@@ -98,24 +81,14 @@ const Party = () => {
</div> </div>
<SubHeading>{t('Transactions')}</SubHeading> <SubHeading>{t('Transactions')}</SubHeading>
<TxsListNavigation
refreshTxs={refreshTxs}
nextPage={nextPage}
previousPage={previousPage}
hasPreviousPage={hasPreviousPage}
loading={loading}
hasMoreTxs={hasMoreTxs}
>
<TxsFilter filters={filters} setFilters={setFilters} />
</TxsListNavigation>
{!error && txsData ? ( {!error && txsData ? (
<TxsInfiniteList <TxsInfiniteList
hasMoreTxs={hasMoreTxs} hasMoreTxs={hasMoreTxs}
areTxsLoading={loading} areTxsLoading={loading}
txs={txsData} txs={txsData}
loadMoreTxs={nextPage} loadMoreTxs={loadTxs}
error={error} error={error}
className="mb-28 w-full" className="mb-28"
/> />
) : ( ) : (
<Splash> <Splash>
@@ -10,5 +10,4 @@ export const Routes = {
MARKETS: 'markets', MARKETS: 'markets',
ORACLES: 'oracles', ORACLES: 'oracles',
NETWORK_PARAMETERS: 'network-parameters', NETWORK_PARAMETERS: 'network-parameters',
DISCLAIMER: 'disclaimer',
}; };

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