Compare commits

..
2064 changed files with 175859 additions and 110431 deletions
-4
View File
@@ -3,7 +3,3 @@ apps/**/node_modules/*
tmp/*
.dockerignore
dockerfiles
node_modules
.git
.github
.vscode
+3 -2
View File
@@ -14,7 +14,8 @@ What we need to achieve and who for
## Tasks
- [ ]
- [ ]
- [ ] What do we need to do first
- [ ] and then what?
- [ ] Etc.
## Additional details / background info
-21
View File
@@ -1,21 +0,0 @@
---
name: Release
about: A template to outline the steps needed to for a successful release of our frontend apps
title: 'Release [add dapp version]-core-[add core version]'
labels:
assignees: ''
---
### Tasks
- [ ] Review [link to core release](xxx)
- [ ] Tag frontend-monorepo
- [ ] Create release and generate release notes
- [ ] Run `@smoke` tests
- [ ] Run `@regression` tests
- [ ] Run `@slow` tests
- [ ] Explorative testing of key flows
- [ ] Set `release/[network]` to tagged commit
- [ ] Verify builds (on Netlify and Fleek) are successful
- [ ] Verify build has been deployed
- [ ] Smoke testing on deployed app
+4 -7
View File
@@ -22,14 +22,11 @@ So that
## Tasks
- [ ] UX (if needed)
- [ ] Design (if needed)
- [ ] Explore and sketch
- [ ] Team and stakeholder review
- [ ] Specs reviewed and created or adjusted
- [ ] Implementation
- [ ] Testing (unit and/or e2e)
- [ ] Code review
- [ ] QA review
- [ ] Visual Design
- [ ] Team review
- [ ] Etc.
## 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
@@ -13,7 +13,7 @@ env:
jobs:
add_issue:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: 'Add issue to project board'
run: |
-16
View File
@@ -1,16 +0,0 @@
name: 'Check if branch is shorter than 52 chars'
on: pull_request
jobs:
branch-naming-rules:
runs-on: ubuntu-latest
steps:
# echo "branches that are longer than 51 chars can't be parsed by kubernetes to create previews. Each app has prefix of it's name like: 'governance-' (12 chars), what leaves 51 max branch length"
# current parsable length: $( git rev-parse --abbrev-ref HEAD | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | wc -c)
- uses: deepakputhraya/action-branch-name@master
with:
# regex: '([a-z])+\/([a-z])+' # Regex the branch should match. This example enforces grouping
# allowed_prefixes: 'feature,stable,fix' # All branches should start with the given prefix
# ignore: master,develop # Ignore exactly matching branch names from convention
min_length: 1 # Min length of the branch name
max_length: 51 # Max length of the branch name
-293
View File
@@ -1,293 +0,0 @@
name: CI/CD
on:
push:
branches:
- release/*
- develop
- main
# uncomment pull_request and comment pull_request_target to test CI changes against feature branch not target branch (develop)
# pull_request:
pull_request_target:
types:
- opened
- ready_for_review
- reopened
- edited
- synchronize
jobs:
node-modules:
runs-on: ubuntu-22.04
name: 'Cache yarn modules'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Cache node modules
id: cache
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# comment out "restore-keys" if you need to rebuild yarn from 0
restore-keys: |
${{ runner.os }}-cache-node-modules-
- name: Setup node
uses: actions/setup-node@v3
if: steps.cache.outputs.cache-hit != 'true'
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: yarn install
if: steps.cache.outputs.cache-hit != 'true'
run: yarn install --pure-lockfile
lint-pr-title:
needs: node-modules
if: ${{ github.event_name == 'pull_request' }}
name: Verify PR title
uses: ./.github/workflows/lint-pr.yml
secrets: inherit
lint-test-build:
timeout-minutes: 60
needs: node-modules
runs-on: ubuntu-22.04
name: '(CI) lint + unit test + build'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v3
with:
main-branch-name: develop
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Build affected spec
run: yarn nx affected --target=build-spec
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build || (yarn install && yarn nx affected:build)
# See affected apps
- name: See affected apps
run: |
affected="$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)"
branch_slug="$(echo '${{ github.head_ref || github.ref_name }}' | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | cut -c 1-50 )"
echo ">>>> debug"
echo "NX_BASE: ${{ env.NX_BASE }}"
echo "NX_HEAD: ${{ env.NX_HEAD }}"
echo "Affected: ${affected}"
echo "Branch slug: ${branch_slug}"
echo ">>>> eof debug"
projects_array=()
preview_governance="not deployed"
preview_trading="not deployed"
preview_explorer="not deployed"
preview_tools="not deployed"
# parse if affected is any of three main applications, if none - use all of them
if echo "$affected" | grep -q governance; then
echo "Governance is affected"
projects_array+=("governance")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
fi
if echo "$affected" | grep -q trading; then
echo "Trading is affected"
projects_array+=("trading")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
fi
if echo "$affected" | grep -q explorer; then
echo "Explorer is affected"
projects_array+=("explorer")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
if [[ ${#projects_array[@]} -eq 0 ]]; then
projects_array=("governance" "trading" "explorer")
preview_governance=$(printf "https://%s.%s.vega.rocks" "governance" "$branch_slug")
preview_trading=$(printf "https://%s.%s.vega.rocks" "trading" "$branch_slug")
preview_explorer=$(printf "https://%s.%s.vega.rocks" "explorer" "$branch_slug")
fi
# applications parsed before this loop are applicable for running e2e-tests
projects_e2e_array=()
for project in "${projects_array[@]}"; do
projects_e2e_array+=("${project}-e2e")
done
# all applications below this loop are not applicable for running e2e-test
# check if pull request event to deploy tools
if [[ "${{ github.event_name }}" = "pull_request" ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on preview"
preview_tools=$(printf "https://%s.%s.vega.rocks" "tools" "$branch_slug")
projects_array+=("multisig-signer")
fi
# those apps deploy only from develop to mainnet
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
if echo "$affected" | grep -q multisig-signer; then
echo "Tools are affected"
echo "Deploying tools on s3"
projects_array+=("multisig-signer")
fi
if echo "$affected" | grep -q static; then
echo "static is affected"
echo "Deploying static on s3"
projects_array+=("static")
fi
if echo "$affected" | grep -q ui-toolkit; then
echo "ui-toolkit is affected"
echo "Deploying ui-toolkit on s3"
projects_array+=("ui-toolkit")
fi
fi
echo "Projects: ${projects_array[@]}"
echo "Projects E2E: ${projects_e2e_array[@]}"
projects_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_array[@]}")
projects_e2e_json=$(jq -M --compact-output --null-input '$ARGS.positional' --args -- "${projects_e2e_array[@]}")
echo PROJECTS_E2E=$projects_e2e_json >> $GITHUB_ENV
echo PROJECTS=$projects_json >> $GITHUB_ENV
echo PREVIEW_GOVERNANCE=$preview_governance >> $GITHUB_ENV
echo PREVIEW_TRADING=$preview_trading >> $GITHUB_ENV
echo PREVIEW_EXPLORER=$preview_explorer >> $GITHUB_ENV
echo PREVIEW_TOOLS=$preview_tools >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
projects-e2e: ${{ env.PROJECTS_E2E }}
preview_governance: ${{ env.PREVIEW_GOVERNANCE }}
preview_trading: ${{ env.PREVIEW_TRADING }}
preview_explorer: ${{ env.PREVIEW_EXPLORER }}
preview_tools: ${{ env.PREVIEW_TOOLS }}
cypress:
needs: lint-test-build
name: '(CI) cypress'
if: ${{ needs.lint-test-build.outputs.projects-e2e != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects-e2e }}
tags: '@smoke @regression'
publish-dist:
needs: lint-test-build
name: '(CD) publish dist'
if: ${{ needs.lint-test-build.outputs.projects != '[]' }}
uses: ./.github/workflows/publish-dist.yml
secrets: inherit
with:
projects: ${{ needs.lint-test-build.outputs.projects }}
dist-check:
runs-on: ubuntu-latest
needs:
- publish-dist
- lint-test-build
if: ${{ github.event_name == 'pull_request' }}
timeout-minutes: 60
name: '(CD) comment preview links'
steps:
- name: Find Comment
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
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 -L --fail "${{ needs.lint-test-build.outputs.preview_governance }}"; do
echo "waiting for governance preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_explorer }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_explorer }}"; do
echo "waiting for explorer preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_trading }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_trading }}"; do
echo "waiting for trading preview"
sleep 5
done
fi
if [[ "${{ needs.lint-test-build.outputs.preview_tools }}" =~ $regex ]]; then
until curl -L --fail "${{ needs.lint-test-build.outputs.preview_tools }}"; do
echo "waiting for tools preview"
sleep 5
done
fi
- name: Create comment
uses: peter-evans/create-or-update-comment@v3
if: ${{ steps.fc.outputs.comment-id == 0 }}
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Previews:
* governance: ${{ needs.lint-test-build.outputs.preview_governance }}
* explorer: ${{ needs.lint-test-build.outputs.preview_explorer }}
* 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:
name: '(CI) cypress - check'
if: ${{ always() }}
needs: cypress
runs-on: ubuntu-22.04
steps:
- run: |
result="${{ needs.cypress.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
+4 -5
View File
@@ -13,16 +13,14 @@ on:
jobs:
cypress-run:
name: Run Cypress Trading tests -- live environment
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: 16.15.1
node-version: 16
- name: Run Cypress tests
uses: cypress-io/github-action@v4
@@ -33,4 +31,5 @@ jobs:
config: baseUrl=${{ github.event.inputs.url }}
env: grepTags=@live
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -11,7 +11,7 @@ on:
type: choice
options:
- explorer-e2e
- governance-e2e
- token-e2e
- trading-e2e
tags:
description: 'Test tags to run'
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: '["explorer-e2e","governance-e2e","trading-e2e"]'
projects: '["explorer-e2e","token-e2e","trading-e2e"]'
tags: '@smoke @regression @slow'
+78
View File
@@ -0,0 +1,78 @@
name: Cypress tests - PR
on:
push:
branches:
- develop
- main
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
pr:
runs-on: ubuntu-latest
steps:
- name: Checkout frontend mono repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup node
uses: actions/setup-node@v3
- name: Remove package.json & yarn.lock to avoid installing everything
run: rm package.json yarn.lock
- name: Install nx
run: yarn add nx
# Check SHAs
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ github.base_ref || github.ref_name }}
set-environment-variables-for-job: true
# See affected apps
- name: See affected apps
run: |
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected"
projects=""
if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?}
projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV
outputs:
projects: ${{ env.PROJECTS }}
run:
needs: pr
if: ${{ needs.pr.outputs.projects != '[]' }}
uses: ./.github/workflows/cypress-run.yml
secrets: inherit
with:
projects: ${{ needs.pr.outputs.projects }}
tags: '@smoke @regression'
# Report single result at the end, to avoid mess with required checks in PR
result:
if: ${{ always() }}
needs: run
runs-on: ubuntu-latest
name: Cypress result
steps:
- run: |
result="${{ needs.run.result }}"
if [[ $result == "success" || $result == "skipped" ]]; then
exit 0
else
exit 1
fi
+3 -12
View File
@@ -1,4 +1,3 @@
name: (CI) Cypress Run
on:
workflow_call:
inputs:
@@ -18,9 +17,8 @@ jobs:
fail-fast: false
matrix:
project: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.project }}
runs-on: self-hosted-runner
timeout-minutes: 100
timeout-minutes: 30
steps:
# Checks if skip cache was requested
- name: Set skip-nx-cache flag
@@ -33,7 +31,6 @@ jobs:
with:
fetch-depth: 0
path: './frontend-monorepo'
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# Restore node_modules from cache if possible
- name: Restore node_modules from cache
@@ -67,7 +64,7 @@ jobs:
######
- name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --browser chrome --env.grepTags="${{ inputs.tags }}"
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}"
working-directory: frontend-monorepo
env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
@@ -90,13 +87,7 @@ jobs:
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
if: ${{ always() }}
with:
name: logs-${{ matrix.project }}
path: /home/runner/.vegacapsule/testnet/logs
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
with:
name: test-report-${{ matrix.project }}
path: frontend-monorepo/apps/${{ matrix.project }}/cypress/reports
+9 -12
View File
@@ -8,25 +8,22 @@ on:
jobs:
master:
name: Generate Queries
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
uses: actions/checkout@v2
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.14.0
- name: Install root dependencies
run: yarn install --frozen-lockfile
run: yarn install
- name: Generate queries
run: node ./scripts/get-queries.js
- uses: actions/upload-artifact@v2
with:
name: queries
-31
View File
@@ -1,31 +0,0 @@
---
name: Verify PR title
on:
workflow_call:
jobs:
lint_pr:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+23
View File
@@ -0,0 +1,23 @@
---
name: Verify PR title
on:
pull_request:
types: [opened, ready_for_review, reopened, edited, synchronize]
jobs:
lint_pr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.14.0
- name: Install root dependencies
run: yarn install
- name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+32
View File
@@ -0,0 +1,32 @@
name: Generate tranches
on:
schedule:
- cron: '0 */6 * * *'
jobs:
master:
name: Generate Queries
runs-on: ubuntu-latest
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.14.0
- 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
-315
View File
@@ -1,315 +0,0 @@
name: (CD) Publish docker + s3
on:
workflow_call:
inputs:
projects:
required: true
type: string
jobs:
publish-dist:
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(inputs.projects) }}
name: ${{ matrix.app }}
runs-on: ubuntu-22.04
timeout-minutes: 25
steps:
- name: Check out code
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry (ghcr)
if: ${{ github.event_name == 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to the Container registry (docker hub)
uses: docker/login-action@v2
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
with:
# registry: registry.hub.docker.com
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Cache node modules
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-cache-node-modules-${{ hashFiles('yarn.lock') }}
# https://docs.github.com/en/actions/learn-github-actions/contexts
- name: Define dist variables
if: ${{ github.event_name == 'push' }}
run: |
envName=''
domain="vega.rocks"
bucketName=''
if [[ "${{ github.ref }}" =~ .*release/.* ]]; then
envName="$(echo ${{ github.ref }} | rev | cut -d '/' -f 1 | rev)"
elif [[ "${{ github.ref }}" =~ .*develop$ ]]; then
envName="stagnet1"
if [[ "${{ matrix.app }}" = "multisig-signer" ]]; then
envName="mainnet"
bucketName="tools.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "static" ]]; then
envName="mainnet"
bucketName="static.vega.xyz"
fi
if [[ "${{ matrix.app }}" = "ui-toolkit" ]]; then
envName="mainnet"
bucketName="ui.vega.rocks"
fi
elif [[ "${{ github.ref }}" =~ .*main$ ]]; then
envName="mainnet"
fi
if [[ "${envName}" = "mainnet" ]]; then
domain="vega.xyz"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
elif [[ "${envName}" = "testnet" ]]; then
domain="fairground.wtf"
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${domain}"
fi
fi
if [[ -z "${bucketName}" ]]; then
bucketName="${{ matrix.app }}.${envName}.${domain}"
fi
echo "bucket name: ${bucketName}"
echo "env name: ${envName}"
echo BUCKET_NAME=${bucketName} >> $GITHUB_ENV
echo ENV_NAME=${envName} >> $GITHUB_ENV
- name: Build local dist
run: |
flags=""
if [[ ! -z "${{ env.ENV_NAME }}" ]]; then
flags="--env=${{ env.ENV_NAME }}"
fi
if [ "${{ matrix.app }}" = "trading" ]; then
yarn nx export trading $flags || (yarn install && yarn nx export trading $flags)
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
yarn nx build ${{ matrix.app }} $flags || (yarn install && yarn nx build ${{ matrix.app }} $flags)
DIST_LOCATION=dist/apps/${{ matrix.app }}
fi
mv $DIST_LOCATION dist-result
tree dist-result
- name: Build and export to local Docker
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
file: docker/node-outside-docker.Dockerfile
load: true
build-args: |
APP=${{ matrix.app }}
ENV_NAME=${{ env.ENV_NAME }}
tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Image digest
if: ${{ github.event_name == 'pull_request' }}
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Sanity check docker image
run: |
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat /ipfs-hash > ${{ matrix.app }}-ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local sh -c 'apk add --update tree; tree /usr/share/nginx/html'
- name: Publish dist as docker image (ghcr)
uses: docker/build-push-action@v3
continue-on-error: true
id: ghcr-push
if: ${{ github.event_name == 'pull_request' }}
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)
uses: docker/build-push-action@v3
continue-on-error: true
id: dockerhub-push
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
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 }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && '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 }}:${{ endsWith(github.ref, 'main') && 'mainnet' || endsWith(github.ref, 'release/testnet') && 'testnet' || '' }}
# bucket creation in github.com/vegaprotocol/terraform//frontend
- name: Publish dist to s3
uses: jakejarvis/s3-sync-action@master
# s3 releases are not happening for trading on mainnet - it's IPFS
if: ${{ github.event_name == 'push' && ( matrix.app != 'trading' || (matrix.app == 'trading' && !( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) ) ) }}
with:
args: --acl private --follow-symlinks --delete
env:
AWS_S3_BUCKET: ${{ env.BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: 'eu-west-1'
SOURCE_DIR: 'dist-result'
- name: Add preview label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ github.event_name == 'pull_request' }}
with:
labels: ${{ matrix.app }}-preview
number: ${{ github.event.number }}
- name: Trigger fleek deployment
# release to ipfs happens only on mainnet (represented by main branch) for trading
if: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'release/testnet') ) }}
run: |
if echo ${{ github.ref }} | grep -q main; 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 echo ${{ github.ref }} | grep -q release/testnet; 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: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
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: ${{ matrix.app == 'trading' && github.event_name == 'push' && ( endsWith(github.ref, 'main') || endsWith(github.ref, 'testnet') ) }}
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 echo ${{ github.ref }} | grep -q main; then
echo $new_hash > cidv0-mainnet.txt
echo $new_cid > cidv1-mainnet.txt
git add cidv0-mainnet.txt cidv1-mainnet.txt
elif echo ${{ github.ref }} | grep -q release/testnet; 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"
)
@@ -0,0 +1,126 @@
name: Publish docker containers
'on':
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
- 'v[0-9]+.[0-9]+.[0-9]+-*'
workflow_dispatch:
inputs:
publish:
description: 'Publish tag to Docker Hub & GitHub Registry'
required: false
type: boolean
default: false
tag:
description: 'Git Tag to build and publish'
required: false
type: string
default: ''
apps:
description: 'Applications to build and publish'
required: false
type: choice
options:
- '["explorer", "token", "trading"]'
- '["explorer"]'
- '["token"]'
- '["trading"]'
archs:
description: 'Architecture to build and publish'
required: false
type: choice
options:
- linux/amd64, linux/arm64
- linux/amd64
- linux/arm64
jobs:
master:
strategy:
fail-fast: false
matrix:
app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
name: Build the ${{ matrix.app }} image
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
with:
ref: ${{ inputs.tag }}
- name: Set up QEMU
id: quemu
uses: docker/setup-qemu-action@v2
- name: Available platforms
run: echo ${{ steps.qemu.outputs.platforms }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
if: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine Docker Image tag
id: tags
run: |
hash=$(git rev-parse HEAD|cut -b1-8)
versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
echo ::set-output name=version::${versionTag}
echo ::set-output name=npmVersion::$(cat dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }} | grep FROM | head -n 1 | awk '{print $2}' | cut -d ':' -f 2 | cut -d '-' -f 1 )
- name: Print config
run: |
git rev-parse --verify HEAD
git status
echo "inputs.tag=${{ inputs.tag }}"
echo "inputs.publish=${{ inputs.publish }}"
echo "inputs.apps=${{ inputs.apps }}"
echo "inputs.archs=${{ inputs.archs }}"
echo "steps.tags.outputs.version=${{ steps.tags.outputs.version }}"
- uses: actions/setup-node@v3
with:
node-version: ${{ steps.tags.outputs.npmVersion }}
- name: Build frontend dists
run: |
yarn --verbose --pure-lockfile
yarn nx ${{ matrix.app =='trading' && 'export' || 'build' }} ${{ matrix.app }} --pure-lockfile
- name: Build and export to local Docker
uses: docker/build-push-action@v3
with:
context: .
push: false
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
load: true
tags: vegaprotocol/${{ matrix.app }}:local
- name: Sanity check docker image
run: |
docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
- name: Build and push to DockerHub
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: APP=${{ matrix.app }}
platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
tags: |
vegaprotocol/${{ matrix.app }}:latest
vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-48
View File
@@ -1,48 +0,0 @@
name: Publish libs to npm
on:
workflow_dispatch:
inputs:
project:
description: 'Monorepo project to publish'
required: true
type: choice
options:
- announcements
- ui-toolkit
- react-helpers
- tailwindcss-config
- types
- utils
- i18n
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-22.04
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup node
uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
# https://stackoverflow.com/questions/61010294/how-to-cache-yarn-packages-in-github-actions
cache: yarn
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn nx build ${{inputs.project}}
- name: Publish project to @vegaprotocol
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: dist/libs/${{inputs.project}}/package.json
access: public
-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
)
+46
View File
@@ -0,0 +1,46 @@
name: Unit tests & build
on:
push:
branches:
- develop
- main
pull_request:
jobs:
pr:
name: Test and lint - PR
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ github.base_ref }}
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.14.0
- name: Restore node_modules from cache
uses: actions/cache@v3
with:
path: '**/node_modules'
key: node_modules-${{ hashFiles('**/yarn.lock') }}
- name: Install root dependencies
run: yarn install --frozen-lockfile
- name: Check formatting
run: yarn nx format:check
- name: Lint affected
run: yarn nx affected:lint --max-warnings=0
- name: Test affected
run: yarn nx affected:test
- name: Build affected
run: yarn nx affected:build
- name: Build affected spec
run: yarn nx affected --target=build-spec
-6
View File
@@ -2,7 +2,6 @@
# compiled output
/dist
/dist-result
/tmp
/out-tsc
/tools/executors/**/*.js
@@ -47,8 +46,3 @@ cypress.env.json
# Next.js
.next
#cypress
/apps/**/cypress/reports/
/apps/**/cypress/downloads/
/apps/**/fixtures/wallet/node**
+1 -1
View File
@@ -1 +1 @@
16.15.1
16.14.0
+1
View File
@@ -7,4 +7,5 @@ __generated___
apps/static/src/assets/devnet-tranches.json
apps/static/src/assets/mainnet-tranches.json
apps/static/src/assets/stagnet3-tranches.json
apps/static/src/assets/testnet-tranches.json
Vendored
+19 -1
View File
@@ -1,2 +1,20 @@
@Library('vega-shared-library') _
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
def commitHash = 'UNKNOWN'
pipeline {
agent any
options {
skipDefaultCheckout true
parallelsAlwaysFailFast()
}
stages {
stage('approbation') {
steps {
sh 'printenv'
checkout scm
runApprobation ignoreFailure: false, frontendBranch: env.BRANCH_NAME, type: 'frontend'
}
}
}
}
-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
+14 -57
View File
@@ -72,7 +72,7 @@ Run `nx serve my-app` for a dev server. Navigate to the port specified in `app/<
In order to generate the schemas for your GraphQL queries, you can run `GRAPHQL_SCHEMA_PATH=[YOUR SCHEMA FILE / API URL HERE] nx run types:generate`.
```bash
export GRAPHQL_SCHEMA_PATH=https://api.n07.testnet.vega.xyz/graphql
export GRAPHQL_SCHEMA_PATH=https://api.n11.testnet.vega.xyz/graphql
yarn nx run types:generate
```
@@ -103,68 +103,25 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console
# Docker & Vegacapsule
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
## Docker
In order to run a container on port 3000:
The [Dockerfile](./dockerfiles) for running the frontends is pretty basic, merely building the application with the APP arg that is passed in and serving the application from [nginx](./nginx/nginx.conf). The only complexity that exists is that there is a script which allows the passing of run time environment variables to the containers. See configuration below for how to do this.
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile dockerfiles/Dockerfile.cra . --build-arg APP=[YOUR APP] --tag=[TAG]
```
In order to run a container:
```bash
docker run -p 3000:80 [TAG]
```
## Build instructions
The [`docker`](./docker) subfolder has some docker configurations for easily setting up your own hosted version of console either for the web, or ready for pinning on IPFS
### nx build outside the docker
Packaging prepared dist into [`nginx`](https://hub.docker.com/_/nginx)([server configuration](./nginx/nginx.conf)) docker image involves building the application on docker host machine from source.
As a prerequisite you need to perform build of `dist` directory and move its content for specific application to `dist-result` directory. Use following script to do it with a single command:
```bash
./docker/prepare-dist.sh
```
You can build any of the containers locally with the following command:
```bash
docker build --dockerfile docker/node-outside-docker.Dockerfile . --tag=[TAG]
```
### nx build inside the docker
Using multistage dockerfile dist is compiled using [node](https://hub.docker.com/_/node) image and later packed to nginx as in [dist build](#dist-build) example.
```bash
docker build --build-arg APP=[YOUR APP] --build-arg NODE_VERSION=$(cat .nvmrc) --build-arg ENV_NAME=mainnet -t [TAG] -f docker/node-inside-docker.Dockerfile .
```
### Computing ipfs-hash of the build
At the moment this feature is important only for `trading` (console) releases.
Each docker build finishes with hash calculation for dist directory. Resulting hash is added to file named as `/ipfs-hash`. Once docker image is produced you can run following commad to display ipfs-hash:
```bash
make recalculate-ipfs TAG=vegaprotocol/trading:{YOUR_VERSION}
```
**updating hash:** recompiling dist directory (even if there are no changed to source code) results in different hash computed by ipfs command.
### Verifying ipfs-hash of existing current application version
An IPFS CID will be attached to every [release](https://github.com/vegaprotocol/frontend-monorepo/releases). If you are intending to pin an application on IPFS, you can check that your build matches by running the following steps:
1. Show latest release by runnning: `make latest-release`. You need to configure [`gh`](https://cli.github.com/) for this step to work, otherwise please provide release manually from [github](https://github.com/vegaprotocol/frontend-monorepo/releases) or [dockerhub](https://hub.docker.com/r/vegaprotocol/trading)
2. Set RELEASE environment variable to value that you want to validate: `export RELEASE=$(make latest-release)` or `export RELEASE=vXX.XX.XX`
3. Set TAG environment variable to image that you want to validate: `export TAG=vegaprotocol/trading:$RELEASE`
4. Download docker image with the desired release `docker pull $TAG`.
5. Recalculate hash: `make recalculate-ipfs`
6. You should see exactly same hash produced by ipfs command as one placed with the release notes: `make show-latest-release`
7. If you want to extract dist from docker image to your local filesystem you can run following command: `make unpack`
8. Now `dist` directory contains valid application build. **it is not possible to calculate same ipfs hash on files that are result of copy operation**
Images ending with `.dist` are to pack locally created transpiled HTML files into nginx container for non-compatible with yarn architectures like M1 Mac
## Config
@@ -204,7 +161,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
bash setup-vegawallet.sh
```
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/governance-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
3. copy generated `api-token` and paste the token into `CYPRESS_VEGA_WALLET_API_TOKEN` environment variable in either `apps/token-e2e/.env` or `apps/explorer-e2e/.env` depending on which project needs testing.
Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
+4 -3
View File
@@ -1,8 +1,9 @@
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_URL=http://localhost:3008/graphql
NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_ENV=CUSTOM
NX_VEGA_CONFIG_URL=
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/capsule-network.json
CYPRESS_VEGA_TENDERMINT_URL=http://localhost:26617
@@ -17,4 +18,4 @@ NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
CYPRESS_VEGA_WALLET_API_TOKEN=
CYPRESS_VEGA_URL=http://localhost:3008/graphql
CYPRESS_VEGA_URL=http://localhost:3028/query
+1
View File
@@ -1,4 +1,5 @@
# 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_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
NX_VEGA_URL=https://api.n04.d.vega.xyz/graphql
+2 -1
View File
@@ -1,7 +1,8 @@
# 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_WEBSOCKET_URL=wss://mainnet-observer-proxy01.ops.vega.xyz/websocket
NX_VEGA_URL=https://api.vega.community/graphql
NX_VEGA_URL=https://api.vega.xyz/query
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest
+18
View File
@@ -0,0 +1,18 @@
# App configuration variables
NX_CHAIN_EXPLORER_URL=https://explorer.vega.trading/.netlify/functions/chain-explorer-api
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
NX_VEGA_ENV=STAGNET3
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
# App flags
NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=0
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
+2 -1
View File
@@ -1,7 +1,8 @@
# 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_WEBSOCKET_URL=wss://lb.testnet.vega.xyz/tm/websocket
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_VEGA_URL=https://api.n11.testnet.vega.xyz/graphql
NX_VEGA_ENV=TESTNET
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
+2 -4
View File
@@ -1,11 +1,10 @@
const { defineConfig } = require('cypress');
module.exports = defineConfig({
reporter: '../../node_modules/cypress-mochawesome-reporter',
projectId: 'et4snf',
e2e: {
setupNodeEvents(on, config) {
require('cypress-mochawesome-reporter/plugin')(on);
require('@cypress/grep/src/plugin')(config);
return config;
},
@@ -22,11 +21,10 @@ module.exports = defineConfig({
chromeWebSecurity: false,
viewportWidth: 1440,
viewportHeight: 900,
testIsolation: false,
},
env: {
environment: 'CUSTOM',
networkQueryUrl: 'http://localhost:3008/graphql',
networkQueryUrl: 'http://localhost:3028/query',
ethUrl: 'https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8',
commitHash: 'dev',
tsConfig: 'tsconfig.json',
@@ -10,8 +10,6 @@
"governance.proposal.updateMarket.minVoterBalance",
"governance.proposal.updateNetParam.minProposerBalance",
"governance.proposal.updateNetParam.minVoterBalance",
"governance.proposal.updateAsset.minProposerBalance",
"governance.proposal.updateAsset.minVoterBalance",
"reward.staking.delegation.maxPayoutPerEpoch",
"reward.staking.delegation.maxPayoutPerParticipant",
"reward.staking.delegation.minimumValidatorStake",
@@ -21,6 +19,10 @@
"validators.delegation.minAmount"
],
"fiveDecimal": [
"governance.proposal.updateAsset.minProposerBalance",
"governance.proposal.updateAsset.minVoterBalance",
"governance.proposal.updateAsset.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"market.fee.factors.infrastructureFee",
"market.fee.factors.makerFee",
"market.liquidity.bondPenaltyParameter",
@@ -75,8 +77,6 @@
"governance.proposal.updateMarket.requiredParticipationLP",
"governance.proposal.updateNetParam.requiredMajority",
"governance.proposal.updateNetParam.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"governance.proposal.updateAsset.requiredParticipation",
"validators.vote.required"
],
"duration": [
@@ -8,7 +8,7 @@ context('Asset page', { tags: '@regression' }, () => {
it('should be able to see full assets list', () => {
cy.getAssets().then((assets) => {
assets.forEach((asset) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
});
@@ -25,7 +25,7 @@ context('Asset page', { tags: '@regression' }, () => {
});
cy.getAssets().then((assets) => {
assets.forEach((asset) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
});
});
@@ -33,7 +33,7 @@ context('Asset page', { tags: '@regression' }, () => {
it('should open details page when clicked on "View details"', () => {
cy.getAssets().then((assets) => {
assets.forEach((asset) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.should('contain.text', 'View details');
+13 -7
View File
@@ -1,27 +1,33 @@
context('Blocks page', { tags: '@regression' }, function () {
before('visit token home page', function () {
cy.visit('/');
});
describe('Verify elements on page', function () {
beforeEach(() => {
cy.visit('/blocks');
});
const blockNavigation = 'a[href="/blocks"]';
const blockHeight = '[data-testid="block-height"]';
const blockTime = '[data-testid="block-time"]';
const blockHeader = '[data-testid="block-header"]';
const previousBlockBtn = '[data-testid="previous-block"]';
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
beforeEach('navigate to blocks page', function () {
cy.get(blockNavigation).click();
});
it('Blocks page is displayed', function () {
validateBlocksDisplayed();
});
it('Blocks page is displayed on mobile', function () {
cy.switchToMobile();
cy.common_switch_to_mobile_and_click_toggle();
cy.get(blockNavigation).click();
validateBlocksDisplayed();
});
it('Block validator page is displayed', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get(blockHeight).eq(0).click();
cy.get('[data-testid="block-validator"]').should('not.be.empty');
cy.get(blockTime).should('not.be.empty');
//TODO: Add assertion for transactions when txs are added
@@ -29,7 +35,7 @@ context('Blocks page', { tags: '@regression' }, function () {
it('Navigate to previous block', function () {
waitForBlocksResponse();
cy.get(blockHeight).eq(0).find('a').click({ force: true });
cy.get(blockHeight).eq(0).click();
cy.get(blockHeader)
.invoke('text')
.then(($blockHeaderTxt) => {
+126 -19
View File
@@ -6,39 +6,75 @@ context('Home Page', function () {
describe('Stats page', { tags: '@smoke' }, function () {
const statsValue = '[data-testid="stats-value"]';
it('Should show connected environment', function () {
const deployedEnv = Cypress.env('environment').toUpperCase();
cy.get('[data-testid="stats-environment"]').should(
'have.text',
`/ ${deployedEnv}`
);
});
it('should show connected environment stats', function () {
const statTitles = {
0: 'Status',
1: 'Epoch',
2: 'Block height',
3: 'Uptime',
4: 'Total nodes',
5: 'Total staked',
6: 'Backlog',
7: 'Trades / second',
8: 'Orders / block',
9: 'Orders / second',
10: 'Transactions / block',
11: 'Block time',
12: 'Time',
13: 'App',
14: 'Tendermint',
15: 'Up since',
16: 'Chain ID',
1: 'Height',
2: 'Uptime',
3: 'Total nodes',
4: 'Total staked',
5: 'Backlog',
6: 'Trades / second',
7: 'Orders / block',
8: 'Orders / second',
9: 'Transactions / block',
10: 'Block time',
11: 'Time',
12: 'App',
13: 'Tendermint',
14: 'Up since',
15: 'Chain ID',
};
cy.get('[data-testid="stats-title"]')
.each(($list, index) => {
cy.wrap($list).should('contain.text', statTitles[index]);
cy.wrap($list).should('have.text', statTitles[index]);
})
.then(($list) => {
cy.wrap($list).should('have.length', 17);
cy.wrap($list).should('have.length', 16);
});
cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
cy.get(statsValue).eq(1).should('not.be.empty');
cy.get(statsValue)
.eq(2)
.invoke('text')
.should('match', /\d+d \d+h \d+m \d+s/i);
cy.get(statsValue).eq(3).should('have.text', '2');
cy.get(statsValue)
.eq(4)
.invoke('text')
.should('match', /\d+\.\d\d(?!\d)/i);
cy.get(statsValue).eq(5).should('have.text', '0');
cy.get(statsValue).eq(6).should('have.text', '0');
cy.get(statsValue).eq(7).should('have.text', '0');
cy.get(statsValue).eq(8).should('have.text', '0');
cy.get(statsValue).eq(9).should('not.be.empty');
cy.get(statsValue).eq(10).should('not.be.empty');
cy.get(statsValue).eq(11).should('not.be.empty');
cy.get(statsValue)
.eq(12)
.invoke('text')
.should('match', /v\d+\.\d+\.\d+/i);
cy.get(statsValue)
.eq(13)
.invoke('text')
.should('match', /\d+\.\d+\.\d+/i);
cy.get(statsValue).eq(14).should('not.be.empty');
cy.get(statsValue).eq(15).should('not.be.empty');
});
it('Block height should be updating', function () {
cy.get(statsValue)
.eq(2)
.eq(1)
.invoke('text')
.then((blockHeightTxt) => {
cy.get(statsValue)
@@ -50,4 +86,75 @@ context('Home Page', function () {
});
});
});
describe('Git info', function () {
it('git info is rendered on the footer of the page', function () {
cy.getByTestId('git-info').within(() => {
cy.getByTestId('git-network-data').within(() => {
cy.contains('Reading network data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('networkQueryUrl'));
cy.getByTestId('link').should('be.visible');
});
cy.getByTestId('git-eth-data').within(() => {
cy.contains('Reading Ethereum data from').should('be.visible');
cy.get('span').should('have.text', Cypress.env('ethUrl'));
});
cy.getByTestId('git-commit-hash').within(() => {
cy.contains('Version/commit hash:').should('be.visible');
cy.getByTestId('link').should('have.text', Cypress.env('commitHash'));
});
});
});
});
describe('Search bar', function () {
it('Successful search for specific id by block id', function () {
const blockId = '973624';
search(blockId);
cy.url().should('include', blockId);
});
it('Successful search for specific id by tx hash', function () {
const txHash =
'9ED3718AA8308E7E08EC588EE7AADAF49711D2138860D8914B4D81A2054D9FB8';
search(txHash);
cy.url().should('include', txHash);
});
it('Successful search for specific id by tx id', function () {
const txId =
'0x61DCCEBB955087F50D0B85382DAE138EDA9631BF1A4F92E563D528904AA38898';
search(txId);
cy.url().should('include', txId);
});
it('Error message displayed when invalid search by wrong string length', function () {
search('9ED3718AA8308E7E08EC588EE7AADAF497D2138860D8914B4D81A2054D9FB8');
validateSearchError("Something doesn't look right");
});
it('Error message displayed when invalid search by invalid hash', function () {
search(
'9ED3718AA8308E7E08ECht8EE753DAF49711D2138860D8914B4D81A2054D9FB8'
);
validateSearchError('Transaction is not hexadecimal');
});
it('Error message displayed when searching empty field', function () {
cy.get('[data-testid="search"]').clear();
cy.get('[data-testid="search-button"]').click();
validateSearchError('Search required');
});
function search(searchTxt) {
cy.get('[data-testid="search"]').clear().type(searchTxt);
cy.get('[data-testid="search-button"]').click();
}
function validateSearchError(expectedError) {
cy.get('[data-testid="search-error"]').should('have.text', expectedError);
}
});
});
@@ -2,14 +2,17 @@ context('Network parameters page', { tags: '@smoke' }, function () {
before('navigate to network parameter page', function () {
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat');
});
describe('Verify elements on page', function () {
beforeEach(() => {
cy.visit('/network-parameters');
});
describe('Verify elements on page', function () {
const networkParametersNavigation = 'a[href="/network-parameters"]';
const networkParametersHeader = '[data-testid="network-param-header"]';
const tableRows = '[data-testid="key-value-table-row"]';
before('navigate to network parameter page', function () {
cy.visit('/');
cy.get(networkParametersNavigation).click();
});
it('should show network parameter heading at top of page', function () {
cy.get(networkParametersHeader)
.should('have.text', 'Network Parameters')
@@ -138,7 +141,7 @@ context('Network parameters page', { tags: '@smoke' }, function () {
});
});
it.skip('should list each network parameter displayed as a currency value with four decimals - in the correct format', function () {
it('should list each network parameter displayed as a currency value with four decimals - in the correct format', function () {
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
@@ -198,8 +201,55 @@ context('Network parameters page', { tags: '@smoke' }, function () {
});
});
it('should be able to see network parameters - on mobile', function () {
cy.switchToMobile();
it('should be able to switch network parameter page - between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not already set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(networkParametersNavigation)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
it.skip('should be able to see network parameters - on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(networkParametersNavigation).click();
cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => {
@@ -6,7 +6,7 @@ const customNodeBtn = 'custom-node';
context.skip('Node switcher', { tags: '@regression' }, function () {
beforeEach('visit home page', function () {
cy.intercept('GET', 'https://static.vega.xyz/assets/capsule-network.json', {
hosts: ['http://localhost:3008/graphql'],
hosts: ['http://localhost:3028/query'],
}).as('nodeData');
cy.visit('/');
cy.wait('@nodeData');
@@ -219,7 +219,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
balance type asset {id symbol decimals}}}}}}}}';
cy.request({
method: 'POST',
url: `http://localhost:3008/graphql`,
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
@@ -1,15 +1,303 @@
context('Validator page', { tags: '@smoke' }, function () {
const validatorMenuHeading = 'a[href="/validators"]';
const tendermintDataHeader = '[data-testid="tendermint-header"]';
const vegaDataHeader = '[data-testid="vega-header"]';
const jsonSection = '.language-json';
before('Visit validators page and obtain data', function () {
cy.visit('/validators');
cy.visit('/');
cy.get(validatorMenuHeading).click();
cy.get_validators().as('validators');
cy.get_nodes().as('nodes');
});
describe('Verify elements on page', function () {
it('should be able to see validator tiles', function () {
cy.getNodes().then((nodes) => {
nodes.forEach((node) => {
cy.get(`[validator-id="${node.id}"]`).should('be.visible');
before('Ensure at least two validators are present', function () {
assert.isAtLeast(
this.validators.length,
2,
'Ensuring at least two validators exist'
);
});
it('should be able to see validator page sections', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
});
it('should be able to see relevant validator information in tendermint section', function () {
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((validatorsInJson) => {
this.validators.forEach((validator, index) => {
const validatorInJson =
validatorsInJson.result.validators[index];
assert.equal(
validatorInJson.address,
validator.address,
`Checking that validator address shown in json matches system data`
);
cy.contains(validator.address).should('be.visible');
assert.equal(
validatorInJson.pub_key.type,
validator.pub_key.type,
`Checking that validator public key type shown in json matches system data`
);
cy.contains(validator.pub_key.type).should('be.visible');
assert.equal(
validatorInJson.pub_key.value,
validator.pub_key.value,
`Checking that validator public key value shown in json matches system data`
);
cy.contains(validator.pub_key.value).should('be.visible');
assert.equal(
validatorInJson.voting_power,
validator.voting_power,
`Checking that validator voting power in json matches system data`
);
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
});
// Test disabled 2022/11/15 during the 0.62.1 upgrade. The JSON structure changed, and
// this test failed. Rather than fix it, it will be replaced when the validator view displays
// something useful rather than just dumping out the JSON.
xit('should be able to see relevant node information in vega data section', function () {
cy.get(vegaDataHeader)
.contains('Vega data')
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((nodesInJson) => {
this.nodes.forEach((node, index) => {
const nodeInJson = nodesInJson.edges[index].node;
// Vegacapsule shows no info or null for following fields:
// name, infoURL, avatarUrl, location, epoch data
// Therefore, these values remain unchecked.
assert.equal(
nodeInJson.__typename,
node.__typename,
`Checking that node __typename shown in json matches system data`
);
cy.contains(node.__typename).should('be.visible');
assert.equal(
nodeInJson.id,
node.id,
`Checking that node id shown in json matches system data`
);
cy.contains(node.id).should('be.visible');
assert.equal(
nodeInJson.pubkey,
node.pubkey,
`Checking that node pubkey shown in json matches system data`
);
cy.contains(node.pubkey).should('be.visible');
assert.equal(
nodeInJson.tmPubkey,
node.tmPubkey,
`Checking that node tmPubkey shown in json matches system data`
);
cy.contains(node.tmPubkey).should('be.visible');
assert.equal(
nodeInJson.ethereumAddress,
node.ethereumAddress,
`Checking that node ethereumAddress shown in json matches system data`
);
cy.contains(node.ethereumAddress).should('be.visible');
assert.equal(
nodeInJson.stakedByOperator,
node.stakedByOperator,
`Checking that node stakedByOperator value shown in json matches system data`
);
cy.contains(node.stakedByOperator).should('be.visible');
assert.equal(
nodeInJson.stakedByDelegates,
node.stakedByDelegates,
`Checking that node stakedByDelegates value shown in json matches system data`
);
cy.contains(node.stakedByDelegates).should('be.visible');
assert.equal(
nodeInJson.stakedTotal,
node.stakedTotal,
`Checking that node stakedTotal shown in json matches system data`
);
cy.contains(node.stakedTotal).should('be.visible');
assert.equal(
nodeInJson.pendingStake,
node.pendingStake,
`Checking that node pendingStake shown in json matches system data`
);
cy.contains(node.pendingStake).should('be.visible');
assert.equal(
nodeInJson.status,
node.status,
`Checking that node status shown in json matches system data`
);
cy.contains(node.status).should('be.visible');
});
});
});
});
it('should be able to see validator page displayed on mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(validatorMenuHeading).click();
cy.get(vegaDataHeader)
.contains('Vega data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.and('is.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
cy.get(tendermintDataHeader)
.contains('Tendermint data')
.next()
.within(() => {
this.validators.forEach((validator) => {
cy.contains(validator.address).should('be.visible');
cy.contains(validator.pub_key.type).should('be.visible');
cy.contains(validator.pub_key.value).should('be.visible');
cy.contains(validator.voting_power).should('be.visible');
// Proposer priority can change frequently mid test
// Therefore only checking the field name is present.
cy.contains('proposer_priority').should('be.visible');
});
});
});
it('should be able to switch validator page between light and dark mode', function () {
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
const themeSwitcher = '[data-testid="theme-switcher"]';
const jsonFields = '.hljs';
const sideMenuBackground = '.absolute';
// Engage dark mode if not allready set
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.then((background_color) => {
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
cy.get(themeSwitcher).click();
});
// Engage white mode
cy.get(themeSwitcher).click();
// White Mode
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', whiteThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', whiteThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', whiteThemeSideMenuBackgroundColor);
// Dark Mode
cy.get(themeSwitcher).click();
cy.get(validatorMenuHeading)
.should('have.css', 'background-color')
.and('include', darkThemeSelectedMenuOptionColor);
cy.get(jsonFields)
.should('have.css', 'background-color')
.and('include', darkThemeJsonFieldBackColor);
cy.get(sideMenuBackground)
.should('have.css', 'background-color')
.and('include', darkThemeSideMenuBackgroundColor);
});
Cypress.Commands.add('get_validators', () => {
cy.request({
method: 'GET',
url: `http://localhost:26617/validators`,
headers: { 'content-type': 'application/json' },
})
.its(`body.result.validators`)
.then(function (response) {
let validators = [];
response.forEach((account, index) => {
validators[index] = account;
});
return validators;
});
});
Cypress.Commands.add('get_nodes', () => {
const mutation =
'{nodesConnection { edges { node { id name infoUrl avatarUrl pubkey tmPubkey ethereumAddress \
location stakedByOperator stakedByDelegates stakedTotal pendingStake \
epochData { total offline online __typename } status name __typename}}}}';
cy.request({
method: 'POST',
url: `http://localhost:3028/query`,
body: {
query: mutation,
},
headers: { 'content-type': 'application/json' },
})
.its(`body.data.nodesConnection.edges`)
.then(function (response) {
let nodes = [];
response.forEach((node) => {
nodes.push(node);
});
return nodes;
});
});
});
});
});
-1
View File
@@ -15,6 +15,5 @@
import '@vegaprotocol/cypress';
import './common.functions.js';
import 'cypress-mochawesome-reporter/register';
import registerCypressGrep from '@cypress/grep';
registerCypressGrep();
+9 -17
View File
@@ -1,21 +1,13 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
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_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_VEGA_GOVERNANCE_URL=https://governance.stagnet1.vega.rocks
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_NETWORKS='{"TESTNET":"https://explorer.fairground.wtf","MAINNET":"https://explorer.vega.xyz"}'
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases/tag/
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
# App flags
NX_EXPLORER_ASSETS=1
+10 -2
View File
@@ -1,10 +1,18 @@
# App configuration variables
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_EXPLORER_URL=/
# App flags
NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_MARKETS=1
NX_EXPLORER_ORACLES=1
NX_EXPLORER_TXS_LIST=0
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
+6 -6
View File
@@ -1,12 +1,12 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
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_TENDERMINT_WEBSOCKET_URL=wss://tm.be.devnet1.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=DEVNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
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_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_VEGA_EXPLORER_URL=/
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet1-network.json
+7 -10
View File
@@ -1,13 +1,10 @@
# App configuration variables
NX_TENDERMINT_URL=https://be.vega.community
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_URL=https://api.vega.community/graphql
NX_TENDERMINT_URL=https://be.explorer.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mainnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=MAINNET
NX_BLOCK_EXPLORER=https://be.vega.community/rest
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz/
NX_VEGA_CONSOLE_URL=https://console.vega.xyz
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
+21
View File
@@ -0,0 +1,21 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/mirror-network.json
NX_VEGA_NETWORKS='{"DEVNET":"https://dev.token.vega.xyz","STAGNET3":"https://stagnet3.token.vega.xyz","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}'
NX_VEGA_ENV=MIRROR
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
# App flags
NX_EXPLORER_ASSETS=1
NX_EXPLORER_GENESIS=1
NX_EXPLORER_GOVERNANCE=1
NX_EXPLORER_NETWORK_PARAMETERS=1
NX_EXPLORER_PARTIES=1
NX_EXPLORER_VALIDATORS=1
NX_EXPLORER_MARKETS=0
NX_EXPLORER_ORACLES=0
NX_EXPLORER_TXS_LIST=1
+12
View File
@@ -0,0 +1,12 @@
# App configuration variables
NX_VEGA_ENV=SANDBOX
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
+14 -2
View File
@@ -1,2 +1,14 @@
# .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"}'
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_BLOCK_EXPLORER=https://be.stagnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://stagnet1.token.vega.xyz
+9
View File
@@ -0,0 +1,9 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
+6 -6
View File
@@ -1,13 +1,13 @@
# App configuration variables
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/websocket/
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/testnet-network.json
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=TESTNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_NETWORKS={}
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://governance.fairground.wtf
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
-15
View File
@@ -1,15 +0,0 @@
# App configuration variables
NX_VEGA_ENV=VALIDATOR_TESTNET
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/testnet2/testnet2.toml
NX_VEGA_URL=https://api-validators-testnet.vega.rocks/graphql
NX_VEGA_REST=https://api-validators-testnet.vega.rocks/
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/rest
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://explorer.validators-testnet.vega.rocks/
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks/
+1 -2
View File
@@ -2,7 +2,6 @@
NX_TENDERMINT_URL=http://localhost:26607/
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_VEGA_EXPLORER_URL=/
+2 -1
View File
@@ -35,11 +35,12 @@ Example configurations are provided here:
- [Devnet](./.env.devnet)
- [Capsule](./.env.capsule)
- [Testnet](./.env.testnet)
- [Stagnet3](./.env.stagnet3)
For convenience, you can boot the app injecting one of the configurations above by running:
```bash
yarn nx run explorer:serve --env={env} # e.g. stagnet1
yarn nx run explorer:serve --env={env} # e.g. stagnet3
```
There are a few different configuration options offered for this app:
+1 -1
View File
@@ -70,7 +70,7 @@
"executor": "@nrwl/workspace:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.71.4/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"
]
}
},
-5
View File
@@ -1,5 +0,0 @@
function ReactMarkdown({ children }) {
return <div>{children}</div>;
}
export default ReactMarkdown;
+48 -34
View File
@@ -1,46 +1,60 @@
import {
AppFailure,
NetworkLoader,
NodeGuard,
NodeSwitcherDialog,
useEnvironment,
useInitializeEnv,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import classnames from 'classnames';
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
import { Nav } from './components/nav';
import { Header } from './components/header';
import { Main } from './components/main';
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import { Footer } from './components/footer/footer';
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
import {
AssetDetailsDialog,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
import { Suspense } from 'react';
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
return (
<AssetDetailsDialog
assetId={id}
trigger={trigger || null}
asJson={asJson}
open={isOpen}
onChange={setOpen}
/>
);
};
function App() {
const { VEGA_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useNodeSwitcherStore(
(store) => [store.dialogOpen, store.setDialogOpen]
const layoutClasses = classnames(
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
'min-h-[100vh] mx-auto my-0',
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
'bg-white dark:bg-black',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
);
return (
<TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
<NodeGuard
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}
/>
<AnnouncementBanner>
<div className="font-alpha calt uppercase text-center text-lg text-white">
<span className="pr-4">Mainnet sim 2 coming in March!</span>
<ExternalLink href="https://fairground.wtf/">
Learn more
</ExternalLink>
</div>
</AnnouncementBanner>
<div className={layoutClasses}>
<Header />
<Nav />
<Main />
<Footer />
</div>
<DialogsContainer />
</NetworkLoader>
</TendermintWebsocketProvider>
);
@@ -1,12 +1,11 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
export type AssetBalanceProps = {
assetId: string;
price: string;
showAssetLink?: boolean;
showAssetSymbol?: boolean;
};
/**
@@ -17,21 +16,18 @@ const AssetBalance = ({
assetId,
price,
showAssetLink = true,
showAssetSymbol = false,
}: AssetBalanceProps) => {
const { data: asset, loading } = useAssetDataProvider(assetId);
const { data: asset } = useAssetDataProvider(assetId);
const label =
!loading && asset && asset.decimals
asset && asset.decimals
? addDecimalsFormatNumber(price, asset.decimals)
: price;
return (
<div className="inline-block">
<span>{label}</span>{' '}
{showAssetLink && asset?.id ? (
<AssetLink showAssetSymbol={showAssetSymbol} assetId={assetId} />
) : null}
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
</div>
);
};
@@ -1,4 +1,4 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
import AssetBalance from './asset-balance';
@@ -13,17 +13,11 @@ const DEFAULT_DECIMALS = 18;
* the governance asset first, which is set by a network parameter
*/
const GovernanceAssetBalance = ({ price }: GovernanceAssetBalanceProps) => {
const { data, loading } = useExplorerGovernanceAssetQuery();
const { data } = useExplorerGovernanceAssetQuery();
if (!loading && data && data.networkParameter?.value) {
if (data && data.networkParameter?.value) {
const governanceAssetId = data.networkParameter.value;
return (
<AssetBalance
price={price}
showAssetSymbol={true}
assetId={governanceAssetId}
/>
);
return <AssetBalance price={price} assetId={governanceAssetId} />;
} else {
return (
<div className="inline-block">
@@ -1,11 +1,11 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
@@ -2,11 +2,11 @@ import React from 'react';
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { Routes } from '../../routes/route-names';
import { Link } from 'react-router-dom';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { TimeAgo } from '../time-ago';
import { TableWithTbody, TableRow, TableCell } from '../table';
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
interface BlockProps {
block: BlockMeta;
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import React from 'react';
import type { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from './block-data';
@@ -1,7 +1,7 @@
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/react-helpers';
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from './block-data';
import EmptyList from '../empty-list/empty-list';
@@ -1,13 +1,29 @@
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';
const BlocksRefetchInWebsocketProvider = ({
callback,
mocketLocation,
}: {
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', () => {
@@ -16,8 +32,111 @@ describe('Blocks refetch', () => {
const mocket = new WS(mocketLocation, { jsonProtocol: true });
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();
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 { t } from '@vegaprotocol/i18n';
import { Button, Icon } from '@vegaprotocol/ui-toolkit';
import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/react-helpers';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
interface BlocksRefetchProps {
refetch: () => void;
}
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 = () => {
refetch();
setBlocksToLoad(0);
};
return (
<Button onClick={refresh} data-testid="refresh" size="xs">
<Icon name="refresh" className="!align-baseline mr-2" size={3} />
{t('Load new')}
</Button>
<div className="mb-4">
<span data-testid="new-blocks">{blocksToLoad} new blocks - </span>
<ButtonLink onClick={refresh} data-testid="refresh">
{t('refresh to see latest')}
</ButtonLink>
</div>
);
};
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import {
Button,
Dialog,
@@ -19,12 +19,12 @@ const EmptyList = ({ heading, label }: EmptyListProps) => {
<div className="mt-4">
{heading ? (
<h1 className="font-alpha calt text-xl uppercase text-center leading-relaxed">
<h1 className="font-alpha text-xl uppercase text-center leading-relaxed">
{heading}
</h1>
) : null}
{label ? (
<p className="font-alpha calt text-gray-500 text-center">{label}</p>
<p className="font-alpha text-gray-500 text-center">{label}</p>
) : null}
</div>
</div>
@@ -1,24 +0,0 @@
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
@@ -1,99 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerEpochQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
export const ExplorerEpochDocument = gql`
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
`;
/**
* __useExplorerEpochQuery__
*
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerEpochQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
export const ExplorerFutureEpochDocument = gql`
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
`;
/**
* __useExplorerFutureEpochQuery__
*
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerFutureEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerFutureEpochQuery({
* variables: {
* },
* });
*/
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
@@ -1,56 +0,0 @@
import { render } from '@testing-library/react';
import { IconForEpoch } from './epoch';
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
describe('IconForEpoch', () => {
it('Handles malformed dates', () => {
const start = 'This is n0t a d4te';
const end = '📅';
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('defaults to a calendar icon', () => {
const start = null as unknown as string;
const end = null as unknown as string;
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start and end are both in the future, stick with calendar', () => {
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start is in the past and end is in the future, this is currently active', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'circle icon'
);
});
it('if start and end are in the paste, this is done', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
});
@@ -1,125 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
import { getSecondsFromInterval } from '@vegaprotocol/utils';
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
describe('getSecondsFromInterval', () => {
it('returns 0 for bad data', () => {
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
expect(getSecondsFromInterval('')).toEqual(0);
expect(getSecondsFromInterval('🧙')).toEqual(0);
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
});
it('parses out months from a capital M', () => {
expect(getSecondsFromInterval('2M')).toEqual(5184000);
});
it('parses out days from a capital D', () => {
expect(getSecondsFromInterval('1D')).toEqual(86400);
});
it('parses out hours from a lower case h', () => {
expect(getSecondsFromInterval('11h')).toEqual(39600);
});
it('parses out minutes from a lower case m', () => {
expect(getSecondsFromInterval('10m')).toEqual(600);
});
it('parses out seconds from a lower case s', () => {
expect(getSecondsFromInterval('99s')).toEqual(99);
});
it('parses complex examples', () => {
expect(getSecondsFromInterval('24h')).toEqual(86400);
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
});
});
describe('calculateEpochData', () => {
it('Handles bad data', () => {
const currentEpochId = null as unknown as string;
const missingEpochId = null as unknown as string;
const epochStart = null as unknown as string;
const epochLength = null as unknown as string;
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('label', 'Missing data');
expect(res).toHaveProperty('isInFuture', false);
});
it('Calculates that a bigger epoch number is in the future from basic data', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = '';
const epochLength = '';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('isInFuture', true);
});
it('If it has an epoch length and a start time, it provides an estimate', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/in less than a minute$/);
});
it('Provide decent string for past', () => {
const currentEpochId = '20';
const missingEpochId = '10';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/less than a minute ago$/);
});
});
describe('EpochMissingOverview', () => {
function renderComponent(missingEpochId: string) {
return render(
<MockedProvider>
<EpochMissingOverview missingEpochId={missingEpochId} />
</MockedProvider>
);
}
it('renders a - if no id is provided', () => {
const n = null as unknown as string;
const screen = renderComponent(n);
expect(screen.getByTestId('empty')).toBeInTheDocument();
});
});
@@ -1,110 +0,0 @@
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
import addSeconds from 'date-fns/addSeconds';
import formatDistance from 'date-fns/formatDistance';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import isFuture from 'date-fns/isFuture';
import { getSecondsFromInterval, isValidDate } from '@vegaprotocol/utils';
export type EpochMissingOverviewProps = {
missingEpochId?: string;
};
/**
* Renders a set of details for an epoch that has no representation in the
* data node. This is primarily for one of two reasons:
*
* 1. The epoch hasn't happened yet
* 2. The epoch happened before a snapshot, and thus the details don't exist
*
* This component is used when the API has responded with no data for an epoch
* by ID, so we already know that we can't display start time/block etc.
*
* We can detect 1 if the epoch is a higher number than the current epoch
* We can detect 2 if the epoch is in the past, but we still get no response.
*/
const EpochMissingOverview = ({
missingEpochId,
}: EpochMissingOverviewProps) => {
const { data, error, loading } = useExplorerFutureEpochQuery();
// This should not happen, but it's easily handled
if (!missingEpochId) {
return <span data-testid="empty">-</span>;
}
// No data should also not happen - we've requested the current epoch. This
// could happen at chain restart, but shouldn't. If it does, fallback.
if (!data || loading || error) {
return <span data-testid="empty">{missingEpochId}</span>;
}
// If we have enough information to predict a future or past block time, let's do it
if (
!missingEpochId ||
!data.epoch.id ||
!data.epoch.timestamps.start ||
!data?.networkParameter?.value
) {
return <span data-testid="empty">{missingEpochId}</span>;
}
const { label, isInFuture } = calculateEpochData(
data.epoch.id,
missingEpochId,
data.epoch.timestamps.start,
data.networkParameter.value
);
return (
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
<p>
{isInFuture ? (
<Icon name="calendar" className="mr-1" />
) : (
<Icon name="outdated" className="mr-1" />
)}
{missingEpochId}
</p>
</Tooltip>
);
};
export function calculateEpochData(
currentEpochId: string,
missingEpochId: string,
epochStart: string,
epochLength: string
) {
// Blank string will be return 0 seconds from getSecondsFromInterval
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
if (!epochStart || !epochLength) {
// Let's just take a guess
return {
label: 'Missing data',
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
};
}
const startFrom = new Date(epochStart);
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
const label =
isValidDate(futureDate) && isValidDate(startFrom)
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
futureDate,
startFrom,
{ addSuffix: true }
)}`
: 'Missing data';
return {
label,
isInFuture: isFuture(futureDate),
};
}
export default EpochMissingOverview;
@@ -1,127 +0,0 @@
import { useExplorerEpochQuery } from './__generated__/Epoch';
import { t } from '@vegaprotocol/i18n';
import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import EpochMissingOverview from './epoch-missing';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import isPast from 'date-fns/isPast';
const borderClass =
'border-solid border-2 border-vega-dark-200 border-collapse';
export type EpochOverviewProps = {
id?: string;
};
/**
* Displays detailed information about an epoch, given an ID. This
* works for past epochs and current epochs - future epochs, and a
* few other situations (see epoch-missing) will not return us
* enough information to render this.
*
* The details are hidden in a tooltip, behind the epoch number
*/
const EpochOverview = ({ id }: EpochOverviewProps) => {
const { data, error, loading } = useExplorerEpochQuery({
variables: { id: id || '' },
});
const ti = data?.epoch.timestamps;
if (
error?.message &&
error.message.includes('no resource corresponding to this id')
) {
return <EpochMissingOverview missingEpochId={id} />;
}
if (!ti || loading || error) {
return <span>{id}</span>;
}
const description = (
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<br />
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<br />
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
);
return (
<Tooltip description={description}>
<p>
<IconForEpoch start={ti.start} end={ti.end} />
{id}
</p>
</Tooltip>
);
};
export type IconForEpochProps = {
start: string;
end: string;
};
/**
* Chooses an icon to display next to the epoch number, representing
* when the epoch is relative to now (i.e. not yet started, started,
* finished)
*/
export function IconForEpoch({ start, end }: IconForEpochProps) {
const startHasPassed = start ? isPast(new Date(start)) : false;
const endHasPassed = end ? isPast(new Date(end)) : false;
let i: IconProps['name'] = 'calendar';
if (!startHasPassed && !endHasPassed) {
i = 'calendar';
} else if (startHasPassed && !endHasPassed) {
i = 'circle';
} else if (startHasPassed && endHasPassed) {
i = 'tick-circle';
}
return <Icon name={i} className="mr-2" />;
}
export default EpochOverview;
@@ -1,75 +1,69 @@
import {
useEnvironment,
useNodeSwitcherStore,
} from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { Link, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react';
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { ENV } from '../../config/env';
import { Routes } from '../../routes/route-names';
import { Link as RouteLink } from 'react-router-dom';
export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const setNodeSwitcherOpen = useNodeSwitcherStore(
(store) => store.setDialogOpen
);
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['md', 'lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
() => ['lg', 'xl'].includes(screenSize),
[screenSize]
);
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">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
href={
GIT_ORIGIN_URL
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
: undefined
}
target={GIT_ORIGIN_URL ? '_blank' : undefined}
>
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
)}
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
{ENV.addresses.feedback ? (
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
) : null}
</div>
<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">
<RouteLink to={`/${Routes.DISCLAIMER}`} className="underline">
Disclaimer
</RouteLink>
</div>
</footer>
</div>
</footer>
<NodeSwitcherDialog
open={nodeSwitcherOpen}
setOpen={setNodeSwitcherOpen}
/>
</>
);
};
export const NodeUrl = ({ url }: { url: string }) => {
const NodeUrl = ({ url }: { url: string }) => {
// get base url from api url, api sub domain
const urlObj = new URL(url);
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';
jest.mock('@vegaprotocol/environment', () => ({
...jest.requireActual('@vegaprotocol/environment'),
NetworkSwitcher: () => (
<div data-testid="network-switcher">NetworkSwitcher</div>
),
@@ -14,18 +13,19 @@ jest.mock('../search', () => ({
}));
const renderComponent = () => (
<MemoryRouter initialEntries={['/txs']}>
<MemoryRouter>
<Header />
</MemoryRouter>
);
describe('Header', () => {
it('should render navigation', () => {
it('should render heading', () => {
render(renderComponent());
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
'Vega Explorer'
);
});
it('should render search', () => {
render(renderComponent());
@@ -1,111 +1,43 @@
import { matchPath, useLocation, useMatch } from 'react-router-dom';
import {
ThemeSwitcher,
Navigation,
NavigationList,
NavigationItem,
NavigationLink,
NavigationBreakpoint,
NavigationTrigger,
NavigationContent,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { Search } from '../search';
import { Routes } from '../../routes/route-names';
import { NetworkSwitcher } from '@vegaprotocol/environment';
import type { Navigable } from '../../routes/router-config';
import { isNavigable } from '../../routes/router-config';
import { routerConfig } from '../../routes/router-config';
import { useMemo } from 'react';
import compact from 'lodash/compact';
import { Search } from '../search';
const routeToNavigationItem = (r: Navigable) => (
<NavigationItem key={r.handle.name}>
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink>
</NavigationItem>
);
import { useNavStore } from '../nav';
export const Header = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const pages = routerConfig[0].children || [];
const mainItems = compact(
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
pages.find((r) => r.path === n)
)
).filter(isNavigable);
const groupedItems = compact(
[
Routes.PARTIES,
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].map((n) => pages.find((r) => r.path === n))
).filter(isNavigable);
const { pathname } = useLocation();
/**
* Because the grouped items are displayed in a sub menu under an "Other" item
* we need to determine whether any underlying item is active to highlight the
* trigger in the same fashion as any other top-level `NavigationLink`.
* This function checks whether the current location pathname is one of the
* underlying NavigationLinks.
*/
const isOnOther = useMemo(() => {
for (const path of groupedItems.map((r) => r.path)) {
const matched = matchPath(`${path}/*`, pathname);
if (matched) return true;
}
return false;
}, [groupedItems, pathname]);
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
const headerClasses = classnames(
'md:col-span-2',
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
'p-4 gap-2 md:gap-4',
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
'dark text-white'
);
return (
<Navigation
appName="Explorer"
theme="system"
breakpoints={[490, 900]}
actions={
<>
<ThemeSwitcher />
{!isHome && <Search />}
</>
}
onResize={(width, el) => {
if (width < 1157) {
// switch to magnifying glass trigger when width < 1157
el.classList.remove('nav-search-full');
el.classList.add('nav-search-compact');
} else {
el.classList.remove('nav-search-compact');
el.classList.add('nav-search-full');
}
}}
>
<NavigationList hide={[NavigationBreakpoint.Small]}>
<NavigationItem>
<NetworkSwitcher />
</NavigationItem>
</NavigationList>
<NavigationList
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
<header className={headerClasses}>
<div className="flex h-full items-center sm:items-stretch gap-4">
<Link to={Routes.HOME}>
<h1
className="text-white text-3xl font-alpha uppercase calt mb-0"
data-testid="explorer-header"
>
{t('Vega Explorer')}
</h1>
</Link>
<NetworkSwitcher />
</div>
<button
data-testid="open-menu"
className="md:hidden text-white"
onClick={() => toggle()}
>
{mainItems.map(routeToNavigationItem)}
{groupedItems && groupedItems.length > 0 && (
<NavigationItem>
<NavigationTrigger isActive={Boolean(isOnOther)}>
{t('Other')}
</NavigationTrigger>
<NavigationContent>
<NavigationList>
{groupedItems.map(routeToNavigationItem)}
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
<Icon name={open ? 'cross' : 'menu'} />
</button>
<Search />
<ThemeSwitcher className="-my-4" />
</header>
);
};
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-vega-dark-100 dark:text-vega-light-200">
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
@@ -44,12 +44,12 @@ export const InfoPanel = ({
text={id}
startChars={visibleChars}
endChars={visibleChars}
className="text-vega-dark-100 dark:text-vega-light-200"
className="text-black dark:text-zinc-200"
/>
) : (
<p
title={id}
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
className="text-black dark:text-zinc-200 truncate ..."
>
{id}
</p>
@@ -70,7 +70,7 @@ export const InfoPanel = ({
</div>
{copy && (
<CopyWithTooltip text={id}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" />
</button>
</CopyWithTooltip>
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../routes/route-names';
@@ -1,6 +1,6 @@
import type { HTMLInputTypeAttribute, SyntheticEvent } from 'react';
import { Input, Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
interface JumpToProps {
label: string;
@@ -11,7 +11,6 @@ import { Routes } from '../../../routes/route-names';
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
assetId: string;
asDialog?: boolean;
showAssetSymbol?: boolean;
};
/**
@@ -19,22 +18,12 @@ export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
* with a link to the assets modal. If the name does not come back
* it will use the ID instead.
*/
export const AssetLink = ({
assetId,
asDialog,
showAssetSymbol = false,
...props
}: AssetLinkProps) => {
export const AssetLink = ({ assetId, asDialog, ...props }: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open);
const navigate = useNavigate();
const label = asset
? showAssetSymbol
? asset?.symbol
: asset?.name
: assetId;
const label = asset?.name ? asset.name : assetId;
return (
<ButtonLink
data-testid="asset-link"
@@ -43,7 +32,7 @@ export const AssetLink = ({
if (asDialog) {
open(assetId, e.target as HTMLElement);
} else {
navigate(`/${Routes.ASSETS}/${asset?.id}`);
navigate(`${Routes.ASSETS}/${asset?.id}`);
}
}}
{...props}
@@ -3,7 +3,7 @@ import { useExplorerMarketQuery } from './__generated__/Market';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import Hash from '../hash';
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
@@ -48,7 +48,7 @@ const MarketLink = ({
<Link
className="underline"
{...props}
to={`/${Routes.MARKETS}/${id}`}
to={`/${Routes.MARKETS}#${id}`}
title={id}
>
{label}
@@ -56,7 +56,7 @@ const MarketLink = ({
);
} else {
return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}>
<Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
<Hash text={id} />
</Link>
);
@@ -1,39 +0,0 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link';
describe('PartyLink', () => {
it('renders Network for 000.000 party', () => {
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const screen = render(<PartyLink id={zeroes} />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders Network for network party', () => {
const screen = render(<PartyLink id="network" />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders ID with no link for invalid party', () => {
const screen = render(<PartyLink id="this-party-is-not-valid" />);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
});
it('links a valid party to the party page', () => {
const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render(
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
);
const el = screen.getByText(aValidParty);
expect(el).toBeInTheDocument();
// The text should be a link that points to the party's page
expect(el.parentElement?.tagName).toEqual('A');
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
});
});
@@ -3,47 +3,19 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/i18n';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
// 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'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{t('Network')}
</span>
);
}
// If the party doesn't look correct, there's no point in linking to id. Just render
// the ID as it was given to us
if (!isValidPartyId(id)) {
return (
<span className="font-mono" data-testid="invalid-party">
{id}
</span>
);
}
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
<Hash text={id} />
</Link>
);
};
@@ -4,14 +4,13 @@ import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
};
/**
* Given a proposal ID, generates an external link over to
* the Governance page for more information
*/
const ProposalLink = ({ id, text }: ProposalLinkProps) => {
const ProposalLink = ({ id }: ProposalLinkProps) => {
const { data } = useExplorerProposalQuery({
variables: { id },
});
@@ -21,7 +20,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
return (
<ExternalLink href={`${base}/proposals/${id}`}>
{text ? text : <Hash text={label} />}
<Hash text={label} />
</ExternalLink>
);
};
@@ -0,0 +1,9 @@
import { AppRouter } from '../../routes';
export const Main = () => {
return (
<main className="p-4">
<AppRouter />
</main>
);
};
@@ -1,108 +1,246 @@
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/markets';
import { PriceMonitoringBoundsInfoPanel } from '@vegaprotocol/markets';
import {
LiquidityInfoPanel,
LiquidityMonitoringParametersInfoPanel,
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import { MarketInfoTable } from '@vegaprotocol/markets';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import isEqual from 'lodash/isEqual';
addDecimalsFormatNumber,
formatNumberPercentage,
getMarketExpiryDateFormatted,
t,
} from '@vegaprotocol/react-helpers';
import type { MarketInfoNoCandlesQuery } from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
import pick from 'lodash/pick';
import {
MarketStateMapping,
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
export const MarketDetails = ({
market,
}: {
market: MarketInfoNoCandlesQuery['market'];
}) => {
const assetSymbol =
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
const assetId = useMemo(
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
[market]
);
const { data: asset } = useAssetDataProvider(assetId ?? '');
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
if (!market) return null;
const settlementData = market.tradableInstrument.instrument.product
.dataSourceSpecForSettlementData.data as DataSourceDefinition;
const terminationData = market.tradableInstrument.instrument.product
.dataSourceSpecForTradingTermination.data as DataSourceDefinition;
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
return signers.map(({ signer }, i) => {
return (
(signer.__typename === 'ETHAddress' && signer.address) ||
(signer.__typename === 'PubKey' && signer.key)
);
});
}
return [];
const keyDetails = {
...pick(market, 'decimalPlaces', 'positionDecimalPlaces', 'tradingMode'),
state: MarketStateMapping[market.state],
};
const assetDecimals =
market.tradableInstrument.instrument.product.settlementAsset.decimals;
const showTwoOracles = isEqual(
getSigners(settlementData),
getSigners(terminationData)
);
const headerClassName = 'font-alpha calt text-xl mt-4 border-b-2 pb-2';
return (
<div>
<h2 className={headerClassName}>{t('Key details')}</h2>
<KeyDetailsInfoPanel market={market} />
<h2 className={headerClassName}>{t('Instrument')}</h2>
<InstrumentInfoPanel market={market} />
<h2 className={headerClassName}>{t('Settlement asset')}</h2>
<SettlementAssetInfoPanel market={market} />
<h2 className={headerClassName}>{t('Metadata')}</h2>
<MetadataInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk model')}</h2>
<RiskModelInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel market={market} />
<h2 className={headerClassName}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel market={market} />
{(market.data?.priceMonitoringBounds || []).map((trigger, i) => (
const panels = [
{
title: t('Key details'),
content: (
<MarketInfoTable
noBorder={false}
data={{
name: market.tradableInstrument.instrument.name,
marketID: market.id,
tradingMode:
keyDetails.tradingMode &&
MarketTradingModeMapping[keyDetails.tradingMode],
marketDecimalPlaces: market.decimalPlaces,
positionDecimalPlaces: market.positionDecimalPlaces,
settlementAssetDecimalPlaces: assetDecimals,
}}
/>
),
},
{
title: t('Instrument'),
content: (
<MarketInfoTable
noBorder={false}
data={{
marketName: market.tradableInstrument.instrument.name,
code: market.tradableInstrument.instrument.code,
productType:
market.tradableInstrument.instrument.product.__typename,
...market.tradableInstrument.instrument.product,
}}
/>
),
},
{
title: t('Settlement asset'),
content: asset ? (
<AssetDetailsTable
asset={asset}
inline={true}
noBorder={false}
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
/>
) : (
<Splash>{t('No data')}</Splash>
),
},
{
title: t('Metadata'),
content: (
<MarketInfoTable
noBorder={false}
data={{
expiryDate: getMarketExpiryDateFormatted(
market.tradableInstrument.instrument.metadata.tags
),
...market.tradableInstrument.instrument.metadata.tags
?.map((tag) => {
const [key, value] = tag.split(':');
return { [key]: value };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
}}
/>
),
},
{
title: t('Risk model'),
content: (
<MarketInfoTable
noBorder={false}
data={market.tradableInstrument.riskModel}
unformatted={true}
omits={[]}
/>
),
},
{
title: t('Risk parameters'),
content: (
<MarketInfoTable
noBorder={false}
data={market.tradableInstrument.riskModel.params}
unformatted={true}
omits={[]}
/>
),
},
{
title: t('Risk factors'),
content: (
<MarketInfoTable
noBorder={false}
data={market.riskFactors}
unformatted={true}
omits={['market', '__typename']}
/>
),
},
...(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}>
{t('Price monitoring bounds %s', [(i + 1).toString()])}
</h2>
<PriceMonitoringBoundsInfoPanel
market={market}
triggerIndex={i + 1}
<MarketInfoTable
noBorder={false}
data={trigger}
decimalPlaces={market.decimalPlaces}
omits={['referencePrice', '__typename']}
/>
<MarketInfoTable
noBorder={false}
data={{ referencePrice: trigger.referencePrice }}
decimalPlaces={assetDecimals}
/>
</>
),
})),
{
title: t('Liquidity monitoring parameters'),
content: (
<MarketInfoTable
noBorder={false}
data={{
triggeringRatio:
market.liquidityMonitoringParameters.triggeringRatio,
...market.liquidityMonitoringParameters.targetStakeParameters,
}}
/>
),
},
{
title: t('Liquidity price range'),
content: (
<MarketInfoTable
noBorder={false}
data={{
liquidityPriceRange: formatNumberPercentage(
new BigNumber(market.lpPriceRange).times(100)
),
LPVolumeMin:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.minus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
LPVolumeMax:
market.data?.midPrice &&
`${addDecimalsFormatNumber(
new BigNumber(1)
.plus(market.lpPriceRange)
.times(market.data.midPrice)
.toString(),
market.decimalPlaces
)} ${assetSymbol}`,
}}
></MarketInfoTable>
),
},
{
title: t('Oracle'),
content: (
<MarketInfoTable
noBorder={false}
data={
market.tradableInstrument.instrument.product.dataSourceSpecBinding
}
>
<Link
className="text-xs hover:underline"
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
>
{t('View settlement data oracle specification')}
</Link>
<Link
className="text-xs hover:underline"
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForTradingTermination.id}`}
>
{t('View termination oracle specification')}
</Link>
</MarketInfoTable>
),
},
];
return (
<>
{panels.map((p) => (
<div className="mb-3">
<h2 className="font-alpha 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,13 +1,13 @@
import type { MarketFieldsFragment } from '@vegaprotocol/markets';
import { t } from '@vegaprotocol/i18n';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid';
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
import { t } from '@vegaprotocol/react-helpers';
import type {
VegaICellRendererParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
} from '@vegaprotocol/ui-toolkit';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { MarketStateMapping } from '@vegaprotocol/types';
@@ -77,24 +77,22 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment>) => {
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
<AgGridColumn
colId="asset"
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset.symbol"
field="tradableInstrument.instrument.product.settlementAsset"
hide={window.innerWidth <= BREAKPOINT_MD}
cellRenderer={({
data,
value,
}: VegaICellRendererParams<
MarketFieldsFragment,
'tradableInstrument.instrument.product.settlementAsset.symbol'
>) => {
const value =
data?.tradableInstrument.instrument.product.settlementAsset;
return value ? (
'tradableInstrument.instrument.product.settlementAsset'
>) =>
value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
@@ -104,8 +102,8 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
</ButtonLink>
) : (
''
);
}}
)
}
/>
<AgGridColumn
flex={2}
@@ -0,0 +1,181 @@
import { NavLink, useLocation } from 'react-router-dom';
import type { Navigable } from '../../routes/router-config';
import routerConfig from '../../routes/router-config';
import classnames from 'classnames';
import { create } from 'zustand';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
} from 'react';
import { Icon } from '@vegaprotocol/ui-toolkit';
import first from 'lodash/first';
import last from 'lodash/last';
import { BREAKPOINT_MD } from '../../config/breakpoints';
type NavStore = {
open: boolean;
toggle: () => void;
hide: () => void;
};
export const useNavStore = create<NavStore>((set, get) => ({
open: false,
toggle: () => set({ open: !get().open }),
hide: () => set({ open: false }),
}));
const NavLinks = ({ links }: { links: Navigable[] }) => {
const navLinks = links.map((r) => (
<li key={r.name}>
<NavLink
to={r.path}
className={({ isActive }) =>
classnames(
'block mb-2 px-2',
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
{
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
isActive,
}
)
}
>
{r.text}
</NavLink>
</li>
));
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
};
export const Nav = () => {
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
const location = useLocation();
const navRef = useRef<HTMLElement>(null);
const btnRef = useRef<HTMLButtonElement>(null);
const focusable = useMemo(
() =>
navRef.current
? [
...(navRef.current.querySelectorAll(
'a, button'
) as NodeListOf<HTMLElement>),
]
: [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[navRef.current] // do not remove `navRef.current` from deps
);
const closeNav = useCallback(() => {
hide();
console.log(focusable);
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
}, [focusable, hide]);
// close navigation when location changes
useEffect(() => {
closeNav();
}, [closeNav, location]);
useLayoutEffect(() => {
if (open) {
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
}
document.body.style.overflow = open ? 'hidden' : '';
const offset =
document.querySelector('header')?.getBoundingClientRect().top || 0;
if (navRef.current) {
navRef.current.style.height = `calc(100vh - ${offset}px)`;
}
// focus current by default
if (navRef.current && open) {
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
}
const closeOnEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeNav();
}
};
// tabbing loop
const focusLast = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
last(focusable)?.focus();
}
};
const focusFirst = (e: FocusEvent) => {
e.preventDefault();
const isNavElement =
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
if (!isNavElement && open) {
first(focusable)?.focus();
}
};
const resetOnDesktop = () => {
focusable.forEach((fe) =>
fe.setAttribute(
'tabindex',
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
)
);
};
window.addEventListener('resize', resetOnDesktop);
first(focusable)?.addEventListener('focusout', focusLast);
last(focusable)?.addEventListener('focusout', focusFirst);
document.addEventListener('keydown', closeOnEsc);
return () => {
window.removeEventListener('resize', resetOnDesktop);
document.removeEventListener('keydown', closeOnEsc);
first(focusable)?.removeEventListener('focusout', focusLast);
last(focusable)?.removeEventListener('focusout', focusFirst);
};
}, [closeNav, focusable, open]);
return (
<nav
ref={navRef}
className={classnames(
'absolute top-0 z-20 overflow-y-auto',
'transition-[right]',
{
'right-[-200vw] h-full': !open,
'right-0 h-[100vh]': open,
},
'w-full p-4 border-neutral-700 dark:border-neutral-300',
'bg-white dark:bg-black',
'md:static md:border-r'
)}
>
<NavLinks links={routerConfig} />
<button
ref={btnRef}
className="absolute top-0 right-0 p-4 md:hidden"
onClick={() => {
closeNav();
}}
>
<Icon name="cross" />
</button>
</nav>
);
};
@@ -83,7 +83,7 @@ const NestedDataListItem = ({
});
const titleClasses = classNames({
'text-xl pl-4 border-l-4 font-alpha calt': hasChildren,
'text-xl pl-4 border-l-4 font-alpha': hasChildren,
'text-base font-medium whitespace-nowrap': !hasChildren,
});
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market';
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market';
@@ -9,11 +9,11 @@ import SizeInMarket from '../size-in-market/size-in-market';
export interface DeterministicOrderDetailsProps {
id: string;
// Version to fetch, with 0 being 'latest' and 1 being 'first'. Defaults to 0
version?: number | null;
version?: number;
}
export const wrapperClasses =
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
/**
* This component renders the *current* details for an order
@@ -28,7 +28,7 @@ export const wrapperClasses =
*/
const DeterministicOrderDetails = ({
id,
version = null,
version = 0,
}: DeterministicOrderDetailsProps) => {
const { data, error } = useExplorerDeterministicOrderQuery({
variables: { orderId: id, version },
@@ -42,7 +42,7 @@ const DeterministicOrderDetails = ({
<h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')}
</h2>
<p className="text-vega-light-400 mb-12">
<p className="text-gray-500 mb-12">
{t('No order created from this transaction')}
</p>
</div>
@@ -101,7 +101,7 @@ const DeterministicOrderDetails = ({
{t('Remaining')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.remaining} marketId={o.market.id} />
{o.remaining}
</h5>
</div>
)}
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n';
import { t } from '@vegaprotocol/react-helpers';
import type * as Schema from '@vegaprotocol/types';
import type { components } from '../../../../types/explorer';
@@ -1,26 +0,0 @@
import { t } from '@vegaprotocol/i18n';
interface CancelSummaryProps {
orderId?: string;
marketId?: string;
}
/**
* Simple component for rendering a reasonable string from an order cancellation
*/
export const CancelSummary = ({ orderId, marketId }: CancelSummaryProps) => {
return <span className="font-bold">{getLabel(orderId, marketId)}</span>;
};
export function getLabel(
orderId: string | undefined,
marketId: string | undefined
): string {
if (!orderId && !marketId) {
return t('All orders');
} else if (marketId && !orderId) {
return t('All in market');
}
return '-';
}
@@ -20,7 +20,7 @@ export const PageHeader = ({
copy = false,
className,
}: PageHeaderProps) => {
const titleClasses = 'text-xl uppercase font-alpha calt';
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
return (
<header className={className}>
<span className={`${titleClasses} block`}>{prefix}</span>
@@ -38,7 +38,7 @@ export const PageHeader = ({
</h2>
{copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" className="" />
</button>
</CopyWithTooltip>
@@ -1 +0,0 @@
export * from './page-actions';
@@ -1,10 +0,0 @@
import type { HTMLAttributes } from 'react';
export const PageActions = ({
children,
...props
}: HTMLAttributes<HTMLDivElement>) => (
<div className="flex flex-row items-start gap-1" {...props}>
{children}
</div>
);
@@ -1,45 +0,0 @@
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { RouteTitle } from '../route-title';
import { PageActions } from './page-actions';
type PageTitleProps = {
/**
* The page title
* (also sets the document title unless overwritten by
* `documentTitle` property)
*/
title: string;
/**
* The react node that consists of CTA buttons, links, etc.
*/
actions?: ReactNode;
/**
* Overwrites the document title
*/
documentTitle?: Parameters<typeof useDocumentTitle>[0];
} & Omit<HTMLAttributes<HTMLHeadingElement>, 'children'>;
export const PageTitle = ({
title,
actions,
documentTitle,
className,
...props
}: PageTitleProps) => {
useDocumentTitle(
documentTitle && documentTitle.length > 0 ? documentTitle : [title]
);
return (
<div
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-8"
data-testid="page-title"
>
<RouteTitle className={classNames('mb-1', className)} {...props}>
{title}
</RouteTitle>
{actions && <PageActions>{actions}</PageActions>}
</div>
);
};

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