Compare commits

..
1558 changed files with 27153 additions and 50929 deletions
-22
View File
@@ -1,22 +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
-6
View File
@@ -18,12 +18,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Use Node.js 16
id: Node
uses: actions/setup-node@v2
with:
node-version: 16.15.1
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@v4 uses: cypress-io/github-action@v4
with: with:
+1 -1
View File
@@ -11,7 +11,7 @@ on:
type: choice type: choice
options: options:
- explorer-e2e - explorer-e2e
- governance-e2e - token-e2e
- trading-e2e - trading-e2e
tags: tags:
description: 'Test tags to run' description: 'Test tags to run'
+1 -1
View File
@@ -10,5 +10,5 @@ jobs:
uses: ./.github/workflows/cypress-run.yml uses: ./.github/workflows/cypress-run.yml
secrets: inherit secrets: inherit
with: with:
projects: '["explorer-e2e","governance-e2e","trading-e2e"]' projects: '["explorer-e2e","token-e2e","trading-e2e"]'
tags: '@smoke @regression @slow' tags: '@smoke @regression @slow'
+2 -2
View File
@@ -42,10 +42,10 @@ jobs:
affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects) affected=$(yarn nx print-affected --base=${{ env.NX_BASE }} --head=${{ env.NX_HEAD }} --select=projects)
echo -n "Affected projects: $affected" echo -n "Affected projects: $affected"
projects="" projects=""
if [[ $affected == *"governance"* ]]; then projects+='"governance-e2e" '; fi if [[ $affected == *"token"* ]]; then projects+='"token-e2e" '; fi
if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi if [[ $affected == *"trading"* ]]; then projects+='"trading-e2e" '; fi
if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi if [[ $affected == *"explorer"* ]]; then projects+='"explorer-e2e" '; fi
if [[ -z "$projects" ]]; then projects+='"governance-e2e" "trading-e2e" "explorer-e2e" '; fi if [[ -z "$projects" ]]; then projects+='"token-e2e" "trading-e2e" "explorer-e2e" '; fi
projects=${projects%?} projects=${projects%?}
projects=[${projects// /,}] projects=[${projects// /,}]
echo PROJECTS=$projects >> $GITHUB_ENV echo PROJECTS=$projects >> $GITHUB_ENV
+3 -6
View File
@@ -64,11 +64,12 @@ jobs:
###### ######
- name: Run Cypress tests - name: Run Cypress tests
run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome --env.grepTags="${{ inputs.tags }}" run: yarn nx run ${{ matrix.project }}:e2e ${{ env.SKIP_CACHE }} --record --key ${{ secrets.CYPRESS_RECORD_KEY }} --browser chrome
working-directory: frontend-monorepo working-directory: frontend-monorepo
env: env:
CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }} CYPRESS_SLACK_WEBHOOK: ${{ secrets.CYPRESS_SLACK_WEBHOOK }}
CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }} CYPRESS_VEGA_WALLET_API_TOKEN: ${{ steps.setup-vega.outputs.token }}
CYPRESS_grepTags: ${{ inputs.tags }}
###### ######
## Upload logs ## Upload logs
@@ -82,12 +83,8 @@ jobs:
mv "${file}" "$(echo ${file} | sed 's|:|-|g')" mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
done< <(find /home/runner/.vegacapsule/testnet/logs -type f) done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
- name: Print logs files
if: ${{ always() }}
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: ${{ failure() }} if: ${{ always() }}
with: with:
name: logs-${{ matrix.project }} name: logs-${{ matrix.project }}
path: /home/runner/.vegacapsule/testnet/logs path: /home/runner/.vegacapsule/testnet/logs
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node id: Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.15.1 node-version: 16.14.0
- name: Install root dependencies - name: Install root dependencies
run: yarn install run: yarn install
- name: Generate queries - name: Generate queries
+6 -8
View File
@@ -8,16 +8,14 @@ jobs:
lint_pr: lint_pr:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Use Node.js 16 - name: Use Node.js 16
id: Node id: Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.15.1 node-version: 16.14.0
- name: Install root dependencies - name: Install commitlint cli and config
run: yarn install run: npm install @commitlint/cli @commitlint/config-conventional
- name: Create config
run: echo "module.exports = {extends:['@commitlint/config-conventional']};" > commitlint.config.js
- name: Check PR title - name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js run: echo "${{ github.event.pull_request.title }}" | npx commitlint
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
id: Node id: Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.15.1 node-version: 16.14.0
- name: Install root dependencies - name: Install root dependencies
run: yarn install run: yarn install
- name: Generate queries - name: Generate queries
+72 -35
View File
@@ -1,20 +1,54 @@
name: Publish docker containers name: Publish docker containers
'on': 'on':
pull_request: 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: jobs:
master: master:
strategy: strategy:
fail-fast: false fail-fast: false
# to be replaced by nix: https://github.com/vegaprotocol/frontend-monorepo/blob/develop/tools/ipfs-deploy.js#L106-L108
matrix: matrix:
app: ${{ fromJson('["explorer"]') }} app: ${{ fromJson(inputs.apps || '["explorer", "token", "trading"]') }}
name: Build the ${{ matrix.app }} image name: Build the ${{ matrix.app }} image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: ${{ inputs.tag }}
- name: Set up QEMU - name: Set up QEMU
id: quemu id: quemu
@@ -26,64 +60,67 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
# https://docs.github.com/en/actions/learn-github-actions/contexts - name: Login to DockerHub
# https://github.com/actions/checkout#Checkout-pull-request-HEAD-commit-instead-of-merge-commit 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 - name: Determine Docker Image tag
id: tags id: tags
run: | run: |
npmVersion=$(cat .nvmrc | head -n 1) hash=$(git rev-parse HEAD|cut -b1-8)
versionTag=${{ startsWith(github.ref, 'refs/tags/') && github.ref_name || github.event.pull_request.head.sha }} versionTag=${{ inputs.tag || startsWith(github.ref, 'refs/tags/') && github.ref_name || '${hash}' }}
echo ::set-output name=npmVersion::${npmVersion}
echo ::set-output name=version::${versionTag} 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 - name: Print config
run: | run: |
git rev-parse --verify HEAD git rev-parse --verify HEAD
git status 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 }}" 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 - name: Build and export to local Docker
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: false push: false
file: dockerfiles/Dockerfile file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: | build-args: APP=${{ matrix.app }}
APP=${{ matrix.app }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
load: true load: true
tags: | tags: vegaprotocol/${{ matrix.app }}:local
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local
- name: Sanity check docker image - name: Sanity check docker image
run: | run: |
echo "Check .env file" docker run --rm vegaprotocol/${{ matrix.app }}:local cat .env
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat .env docker run --rm vegaprotocol/${{ matrix.app }}:local ls -lah
echo "Check ipfs-hash"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local cat ipfs-hash
echo "List html directory"
docker run --rm ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:local ls -lah
- name: Log in to the Container registry - name: Build and push to DockerHub
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: ${{ inputs.publish || startsWith(github.ref, 'refs/tags/') }}
file: dockerfiles/Dockerfile file: dockerfiles/${{ matrix.app =='trading' && 'Dockerfile.next' || 'Dockerfile.cra' }}.dist
build-args: | build-args: APP=${{ matrix.app }}
APP=${{ matrix.app }} platforms: ${{ inputs.archs || 'linux/amd64, linux/arm64' }}
NODE_VERSION=${{ steps.tags.outputs.npmVersion }}
tags: | tags: |
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:latest vegaprotocol/${{ matrix.app }}:latest
ghcr.io/vegaprotocol/frontend/${{ matrix.app }}:${{ steps.tags.outputs.version }} vegaprotocol/${{ matrix.app }}:${{ steps.tags.outputs.version }}
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
-49
View File
@@ -1,49 +0,0 @@
name: Publish libs to npm
on:
workflow_dispatch:
inputs:
project:
description: 'Monorepo project to publish'
required: true
type: choice
options:
- ui-toolkit
- react-helpers
- tailwindcss-config
- types
- utils
- i18n
jobs:
publish:
name: Build & Publish - Tag
runs-on: ubuntu-latest
permissions:
contents: 'read'
actions: 'read'
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: User Node.js 16
id: Node
uses: actions/setup-node@v3
with:
node-version: 16.15.1
- 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: 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
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
id: Node id: Node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 16.15.1 node-version: 16.14.0
- name: Restore node_modules from cache - name: Restore node_modules from cache
uses: actions/cache@v3 uses: actions/cache@v3
with: with:
+1 -1
View File
@@ -1 +1 @@
16.15.1 16.14.0
+1 -1
View File
@@ -161,7 +161,7 @@ In order to setup and run vegawallet for e2e capsule tests, in a separate termin
bash setup-vegawallet.sh 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: Note: The script is only needed if capsule was built for first time or fresh. To run existing wallet service for capsule:
+1 -1
View File
@@ -3,7 +3,7 @@ NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_URL=http://localhost:3028/query NX_VEGA_URL=http://localhost:3028/query
NX_VEGA_ENV=CUSTOM 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 CYPRESS_VEGA_TENDERMINT_URL=http://localhost:26617
@@ -22,6 +22,7 @@
"governance.proposal.updateAsset.minProposerBalance", "governance.proposal.updateAsset.minProposerBalance",
"governance.proposal.updateAsset.minVoterBalance", "governance.proposal.updateAsset.minVoterBalance",
"governance.proposal.updateAsset.requiredParticipation", "governance.proposal.updateAsset.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"market.fee.factors.infrastructureFee", "market.fee.factors.infrastructureFee",
"market.fee.factors.makerFee", "market.fee.factors.makerFee",
"market.liquidity.bondPenaltyParameter", "market.liquidity.bondPenaltyParameter",
@@ -76,7 +77,6 @@
"governance.proposal.updateMarket.requiredParticipationLP", "governance.proposal.updateMarket.requiredParticipationLP",
"governance.proposal.updateNetParam.requiredMajority", "governance.proposal.updateNetParam.requiredMajority",
"governance.proposal.updateNetParam.requiredParticipation", "governance.proposal.updateNetParam.requiredParticipation",
"governance.proposal.updateMarket.minProposerEquityLikeShare",
"validators.vote.required" "validators.vote.required"
], ],
"duration": [ "duration": [
+136 -34
View File
@@ -1,48 +1,150 @@
context('Asset page', { tags: '@regression' }, () => { context('Asset page', { tags: '@regression' }, function () {
const columns = ['symbol', 'name', 'id', 'type', 'status', 'actions']; before('gather system asset information', function () {
const hiddenOnMobile = ['id', 'type', 'status']; cy.get_asset_information().as('assetsInfo');
describe('Verify elements on page', () => { });
before('Navigate to assets page', () => {
cy.visit('/assets'); describe('Verify elements on page', function () {
const assetsNavigation = 'a[href="/assets"]';
const assetHeader = '[data-testid="asset-header"]';
const jsonSection = '.language-json';
before('Navigate to assets page', function () {
cy.visit('/');
cy.get(assetsNavigation).click();
// Check we have enough enough assets
const assetNames = Object.keys(this.assetsInfo);
assert.isAtLeast(
assetNames.length,
5,
'Ensuring we have at least 5 assets to test'
);
}); });
it('should be able to see full assets list', () => { it('should be able to see assets page sections', function () {
cy.getAssets().then((assets) => { const assetNames = Object.keys(this.assetsInfo);
assets.forEach((asset) => { assetNames.forEach((assetName) => {
cy.get(`[row-id="${asset.id}"]`).should('be.visible'); cy.get(assetHeader)
}); .contains(assetName)
}); .should('be.visible')
columns.forEach((col) => { .next()
cy.get(`[col-id="${col}"]`).should('be.visible'); .within(() => {
cy.get(jsonSection).should('not.be.empty');
});
}); });
}); });
it('should be able to see assets page displayed in mobile', () => { it('should be able to see all asset details displayed in JSON', function () {
cy.switchToMobile(); const assetNames = Object.keys(this.assetsInfo);
assetNames.forEach((assetName) => {
cy.get(assetHeader)
.contains(assetName)
.next()
.within(() => {
cy.get(jsonSection)
.invoke('text')
.convert_string_json_to_js_object()
.then((assetsListedInJson) => {
const assetInfo = this.assetsInfo[assetName];
hiddenOnMobile.forEach((col) => { assert.equal(assetsListedInJson.name, assetInfo.node.name);
cy.get(`[col-id="${col}"]`).should('have.length', 0); assert.equal(assetsListedInJson.id, assetInfo.node.id);
}); assert.equal(
assetsListedInJson.decimals,
assetInfo.node.decimals
);
assert.equal(assetsListedInJson.symbol, assetInfo.node.symbol);
assert.equal(
assetsListedInJson.source.__typename,
assetInfo.node.source.__typename
);
cy.getAssets().then((assets) => { if (assetInfo.node.source.__typename == 'ERC20') {
assets.forEach((asset) => { assert.equal(
cy.get(`[row-id="${asset.id}"]`).should('be.visible'); assetsListedInJson.source.contractAddress,
}); assetInfo.node.source.contractAddress
);
}
if (assetInfo.node.source.__typename == 'BuiltinAsset') {
assert.equal(
assetsListedInJson.source.maxFaucetAmountMint,
assetInfo.node.source.maxFaucetAmountMint
);
}
let knownAssetTypes = ['BuiltinAsset', 'ERC20'];
assert.include(
knownAssetTypes,
assetInfo.node.source.__typename,
`Checking that current asset type of ${assetInfo.node.source.__typename} /
is one of: ${knownAssetTypes}: /
If fail then we need to add extra tests for un-encountered asset types`
);
});
});
}); });
}); });
it('should open details page when clicked on "View details"', () => { it('should be able to switch assets between light and dark mode', function () {
cy.getAssets().then((assets) => { const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
assets.forEach((asset) => { const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`) const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
.eq(0) const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
.should('contain.text', 'View details'); const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`) const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
.eq(0) const themeSwitcher = '[data-testid="theme-switcher"]';
.click(); const jsonFields = '.hljs';
cy.getByTestId('asset-header').should('have.text', asset.name); const sideMenuBackground = '.absolute';
cy.go('back');
// 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(assetsNavigation)
.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(assetsNavigation)
.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('should be able to see assets page displayed in mobile', function () {
cy.common_switch_to_mobile_and_click_toggle();
cy.get(assetsNavigation).click();
const assetNames = Object.keys(this.assetsInfo);
assetNames.forEach((assetName) => {
cy.get(assetHeader)
.contains(assetName)
.should('be.visible')
.next()
.within(() => {
cy.get(jsonSection).should('not.be.empty');
});
}); });
}); });
}); });
+13 -7
View File
@@ -1,27 +1,33 @@
context('Blocks page', { tags: '@regression' }, function () { context('Blocks page', { tags: '@regression' }, function () {
before('visit token home page', function () {
cy.visit('/');
});
describe('Verify elements on page', function () { describe('Verify elements on page', function () {
beforeEach(() => { const blockNavigation = 'a[href="/blocks"]';
cy.visit('/blocks');
});
const blockHeight = '[data-testid="block-height"]'; const blockHeight = '[data-testid="block-height"]';
const blockTime = '[data-testid="block-time"]'; const blockTime = '[data-testid="block-time"]';
const blockHeader = '[data-testid="block-header"]'; const blockHeader = '[data-testid="block-header"]';
const previousBlockBtn = '[data-testid="previous-block"]'; const previousBlockBtn = '[data-testid="previous-block"]';
const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]'; const infiniteScrollWrapper = '[data-testid="infinite-scroll-wrapper"]';
beforeEach('navigate to blocks page', function () {
cy.get(blockNavigation).click();
});
it('Blocks page is displayed', function () { it('Blocks page is displayed', function () {
validateBlocksDisplayed(); validateBlocksDisplayed();
}); });
it('Blocks page is displayed on mobile', function () { it('Blocks page is displayed on mobile', function () {
cy.switchToMobile(); cy.common_switch_to_mobile_and_click_toggle();
cy.get(blockNavigation).click();
validateBlocksDisplayed(); validateBlocksDisplayed();
}); });
it('Block validator page is displayed', function () { it('Block validator page is displayed', function () {
waitForBlocksResponse(); 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('[data-testid="block-validator"]').should('not.be.empty');
cy.get(blockTime).should('not.be.empty'); cy.get(blockTime).should('not.be.empty');
//TODO: Add assertion for transactions when txs are added //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 () { it('Navigate to previous block', function () {
waitForBlocksResponse(); waitForBlocksResponse();
cy.get(blockHeight).eq(0).find('a').click({ force: true }); cy.get(blockHeight).eq(0).click();
cy.get(blockHeader) cy.get(blockHeader)
.invoke('text') .invoke('text')
.then(($blockHeaderTxt) => { .then(($blockHeaderTxt) => {
+87 -8
View File
@@ -6,10 +6,18 @@ context('Home Page', function () {
describe('Stats page', { tags: '@smoke' }, function () { describe('Stats page', { tags: '@smoke' }, function () {
const statsValue = '[data-testid="stats-value"]'; 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 () { it('should show connected environment stats', function () {
const statTitles = { const statTitles = {
0: 'Status', 0: 'Status',
1: 'Block height', 1: 'Height',
2: 'Uptime', 2: 'Uptime',
3: 'Total nodes', 3: 'Total nodes',
4: 'Total staked', 4: 'Total staked',
@@ -28,27 +36,27 @@ context('Home Page', function () {
cy.get('[data-testid="stats-title"]') cy.get('[data-testid="stats-title"]')
.each(($list, index) => { .each(($list, index) => {
cy.wrap($list).should('contain.text', statTitles[index]); cy.wrap($list).should('have.text', statTitles[index]);
}) })
.then(($list) => { .then(($list) => {
cy.wrap($list).should('have.length', 16); cy.wrap($list).should('have.length', 16);
}); });
cy.get(statsValue).eq(0).should('contain.text', 'CONNECTED'); cy.get(statsValue).eq(0).should('have.text', 'CONNECTED');
cy.get(statsValue).eq(1).should('not.be.empty'); cy.get(statsValue).eq(1).should('not.be.empty');
cy.get(statsValue) cy.get(statsValue)
.eq(2) .eq(2)
.invoke('text') .invoke('text')
.should('match', /\d+d \d+h \d+m \d+s/i); .should('match', /\d+d \d+h \d+m \d+s/i);
cy.get(statsValue).eq(3).should('contain.text', '2'); cy.get(statsValue).eq(3).should('have.text', '2');
cy.get(statsValue) cy.get(statsValue)
.eq(4) .eq(4)
.invoke('text') .invoke('text')
.should('match', /\d+\.\d\d(?!\d)/i); .should('match', /\d+\.\d\d(?!\d)/i);
cy.get(statsValue).eq(5).should('contain.text', '0'); cy.get(statsValue).eq(5).should('have.text', '0');
cy.get(statsValue).eq(6).should('contain.text', '0'); cy.get(statsValue).eq(6).should('have.text', '0');
cy.get(statsValue).eq(7).should('contain.text', '0'); cy.get(statsValue).eq(7).should('have.text', '0');
cy.get(statsValue).eq(8).should('contain.text', '0'); cy.get(statsValue).eq(8).should('have.text', '0');
cy.get(statsValue).eq(9).should('not.be.empty'); cy.get(statsValue).eq(9).should('not.be.empty');
cy.get(statsValue).eq(10).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(11).should('not.be.empty');
@@ -78,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 () { before('navigate to network parameter page', function () {
cy.fixture('net_parameter_format_lookup').as('networkParameterFormat'); 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 networkParametersHeader = '[data-testid="network-param-header"]';
const tableRows = '[data-testid="key-value-table-row"]'; 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 () { it('should show network parameter heading at top of page', function () {
cy.get(networkParametersHeader) cy.get(networkParametersHeader)
.should('have.text', 'Network Parameters') .should('have.text', 'Network Parameters')
@@ -198,8 +201,55 @@ context('Network parameters page', { tags: '@smoke' }, function () {
}); });
}); });
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('should be able to see network parameters - on mobile', function () { it('should be able to see network parameters - on mobile', function () {
cy.switchToMobile(); cy.common_switch_to_mobile_and_click_toggle();
cy.get(networkParametersNavigation).click();
cy.get_network_parameters().then((network_parameters) => { cy.get_network_parameters().then((network_parameters) => {
network_parameters = Object.entries(network_parameters); network_parameters = Object.entries(network_parameters);
network_parameters.forEach((network_parameter) => { network_parameters.forEach((network_parameter) => {
@@ -123,7 +123,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
.convert_string_json_to_js_object() .convert_string_json_to_js_object()
.get_party_accounts_data_from_js_object() .get_party_accounts_data_from_js_object()
.then((accountsListedInJson) => { .then((accountsListedInJson) => {
cy.getAssets().then((assetsInfo) => { cy.get_asset_information().then((assetsInfo) => {
const assetInfo = const assetInfo =
assetsInfo[accountsListedInJson[assetInTest].asset.name]; assetsInfo[accountsListedInJson[assetInTest].asset.name];
@@ -205,7 +205,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
}); });
Cypress.Commands.add('get_asset_decimals', (assetID) => { Cypress.Commands.add('get_asset_decimals', (assetID) => {
cy.getAssets().then((assetsInfo) => { cy.get_asset_information().then((assetsInfo) => {
const assetDecimals = assetsInfo[assetData[assetID].name].decimals; const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
let decimals = ''; let decimals = '';
for (let i = 0; i < assetDecimals; i++) decimals += '0'; for (let i = 0; i < assetDecimals; i++) decimals += '0';
@@ -1,15 +1,303 @@
context('Validator page', { tags: '@smoke' }, function () { 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 () { 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 () { describe('Verify elements on page', function () {
it('should be able to see validator tiles', function () { before('Ensure at least two validators are present', function () {
cy.getNodes().then((nodes) => { assert.isAtLeast(
nodes.forEach((node) => { this.validators.length,
cy.get(`[validator-id="${node.id}"]`).should('be.visible'); 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;
}); });
});
}); });
}); });
}); });
@@ -21,10 +21,6 @@ Cypress.Commands.add(
} }
); );
Cypress.Commands.add('switchToMobile', () => {
cy.viewport('iphone-x');
});
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () { Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
cy.viewport('iphone-x'); cy.viewport('iphone-x');
cy.visit('/'); cy.visit('/');
+2 -2
View File
@@ -1,7 +1,7 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
NX_VEGA_NETWORKS='{"SANDBOX":"https://sandbox.explorer.vega.xyz","STAGNET1":"https://stagnet1.token.vega.xyz", "STAGNET3":"https://stagnet3.explorer.vega.xyz","VALIDATOR_TESTNET":"https://validator-testnet.fairground.wtf","TESTNET":"https://token.fairground.wtf","MAINNET":"https://token.vega.xyz"}' NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=STAGNET3 NX_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
+2
View File
@@ -1,7 +1,9 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=http://localhost:26617 NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_VEGA_ENV=CUSTOM NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
# App flags # App flags
+6 -6
View File
@@ -1,10 +1,10 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/devnet1/vegawallet-devnet1.toml 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_VEGA_ENV=DEVNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://dev.token.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
+4 -2
View File
@@ -1,8 +1,10 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://be.explorer.vega.xyz NX_TENDERMINT_URL=https://be.explorer.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.explorer.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks/master/mainnet1/mainnet1.toml 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_VEGA_ENV=MAINNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/ NX_BLOCK_EXPLORER=https://be.explorer.vega.xyz/rest/
NX_ETHERSCAN_URL=https://etherscan.io NX_ETHERSCAN_URL=https://etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz NX_VEGA_GOVERNANCE_URL=https://token.vega.xyz
+4 -2
View File
@@ -1,8 +1,10 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz NX_TENDERMINT_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=https://tm.be.mainnet-mirror.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/mainnet-mirror/vegawallet-mainnet-mirror.toml 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_VEGA_ENV=MIRROR
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/ NX_BLOCK_EXPLORER=https://be.mainnet-mirror.vega.xyz/rest/
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz NX_VEGA_GOVERNANCE_URL=https://mainnet-mirror.token.vega.xyz
+4 -3
View File
@@ -1,11 +1,12 @@
# App configuration variables # App configuration variables
NX_VEGA_ENV=SANDBOX NX_VEGA_ENV=SANDBOX
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/sandbox/vegawallet-sandbox.toml NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/sandbox-network.json
NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz NX_VEGA_EXPLORER_URL=https://sandbox.explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_TENDERMINT_URL=https://tm.sandbox.vega.xyz NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
NX_TENDERMINT_WEBSOCKET_URL=wss://be.sandbox.vega.xyz/websocket NX_TENDERMINT_URL=https://tm.n01.sandbox.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.sandbox.vega.xyz/websocket
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz NX_VEGA_GOVERNANCE_URL=https://sandbox.token.vega.xyz
+2 -1
View File
@@ -1,9 +1,10 @@
NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8 NX_ETHEREUM_PROVIDER_URL=https://sepolia.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet1-network.json
NX_VEGA_ENV=STAGNET1 NX_VEGA_ENV=STAGNET1
NX_VEGA_EXPLORER_URL=https://stagnet1.explorer.vega.xyz 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_TOKEN_URL=https://stagnet1.token.vega.xyz
NX_VEGA_WALLET_URL=http://localhost:1789 NX_VEGA_WALLET_URL=http://localhost:1789
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.xyz
+4 -3
View File
@@ -1,7 +1,8 @@
NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz NX_TENDERMINT_URL=https://tm.n00.stagnet3.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.stagnet3.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n00.stagnet3.vega.xyz/websocket
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet3/vegawallet-stagnet3.toml 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_VEGA_ENV=STAGNET3
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.stagnet3.vega.xyz/rest
NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz NX_VEGA_GOVERNANCE_URL=https://stagnet3.token.vega.xyz
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases/
+5 -2
View File
@@ -1,10 +1,13 @@
# App configuration variables # App configuration variables
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest NX_BLOCK_EXPLORER=https://be.testnet.vega.xyz/rest
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.testnet.vega.xyz/websocket/
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml 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_VEGA_ENV=TESTNET
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql NX_VEGA_URL=https://api.n07.testnet.vega.xyz/graphql
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_NETWORKS={}
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf NX_VEGA_GOVERNANCE_URL=https://token.fairground.wtf
-12
View File
@@ -1,12 +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_VEGA_GOVERNANCE_URL=https://validator-testnet.governance.vega.xyz
NX_TENDERMINT_URL=https://tm-be.validators-testnet.vega.rocks
NX_BLOCK_EXPLORER=https://be.validators-testnet.vega.rocks/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.xyz/websocket
+1
View File
@@ -2,5 +2,6 @@
NX_TENDERMINT_URL=http://localhost:26607/ NX_TENDERMINT_URL=http://localhost:26607/
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26607/websocket
NX_VEGA_ENV=CUSTOM NX_VEGA_ENV=CUSTOM
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_BLOCK_EXPLORER= NX_BLOCK_EXPLORER=
NX_ETHERSCAN_URL=https://sepolia.etherscan.io NX_ETHERSCAN_URL=https://sepolia.etherscan.io
-5
View File
@@ -1,5 +0,0 @@
function ReactMarkdown({ children }) {
return <div>{children}</div>;
}
export default ReactMarkdown;
+48 -15
View File
@@ -1,29 +1,62 @@
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment'; import classnames from 'classnames';
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { EnvironmentProvider, NetworkLoader } 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 { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import type { InMemoryCacheConfig } from '@apollo/client';
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client'; import { Footer } from './components/footer/footer';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
const splashLoading = (
<Splash>
<Loader />
</Splash>
);
function App() { function App() {
const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation();
useEffect(() => {
setMenuOpen(false);
}, [location]);
const cacheConfig: InMemoryCacheConfig = {
typePolicies: {
statistics: {
keyFields: false,
},
},
};
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',
{
'h-[100vh] min-h-auto overflow-hidden': menuOpen,
}
);
return ( return (
<TendermintWebsocketProvider> <TendermintWebsocketProvider>
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}> <NetworkLoader cache={cacheConfig}>
<RouterProvider router={router} fallbackElement={splashLoading} /> <div className={layoutClasses}>
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
<Nav menuOpen={menuOpen} />
<Main />
<Footer />
</div>
</NetworkLoader> </NetworkLoader>
</TendermintWebsocketProvider> </TendermintWebsocketProvider>
); );
} }
const Wrapper = () => { const Wrapper = () => {
useInitializeEnv(); return (
return <App />; <EnvironmentProvider>
<App />
</EnvironmentProvider>
);
}; };
export default Wrapper; export default Wrapper;
@@ -1,6 +1,6 @@
import { useAssetDataProvider } from '@vegaprotocol/assets'; import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { AssetLink } from '../links'; import { AssetLink } from '../links';
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
export type AssetBalanceProps = { export type AssetBalanceProps = {
assetId: string; assetId: string;
@@ -17,17 +17,21 @@ const AssetBalance = ({
price, price,
showAssetLink = true, showAssetLink = true,
}: AssetBalanceProps) => { }: AssetBalanceProps) => {
const { data: asset } = useAssetDataProvider(assetId); const { data } = useExplorerAssetQuery({
variables: { id: assetId },
});
const label = const label =
asset && asset.decimals data && data.asset?.decimals
? addDecimalsFormatNumber(price, asset.decimals) ? addDecimalsFormatNumber(price, data.asset.decimals)
: price; : price;
return ( return (
<div className="inline-block"> <div className="inline-block">
<span>{label}</span>{' '} <span>{label}</span>{' '}
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null} {showAssetLink && data?.asset?.id ? (
<AssetLink id={data.asset.id} />
) : null}
</div> </div>
); );
}; };
@@ -1,4 +1,4 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset'; import { useExplorerGovernanceAssetQuery } from './__generated__/Governance-asset';
import AssetBalance from './asset-balance'; import AssetBalance from './asset-balance';
@@ -1,41 +0,0 @@
import { render, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { assetsList } from '../../mocks/assets';
import { AssetsTable } from './assets-table';
describe('AssetsTable', () => {
it('shows loading message on first render', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={null} />
</MemoryRouter>
);
expect(await res.findByText('Loading...')).toBeInTheDocument();
});
it('shows no data message if no assets found', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={[]} />
</MemoryRouter>
);
expect(
await res.findByText('This chain has no assets')
).toBeInTheDocument();
});
it('shows a table/list with all the assets', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={assetsList} />
</MemoryRouter>
);
await waitFor(() => {
const rowA1 = res.container.querySelector('[row-id="123"]');
expect(rowA1).toBeInTheDocument();
const rowA2 = res.container.querySelector('[row-id="456"]');
expect(rowA2).toBeInTheDocument();
});
});
});
@@ -1,105 +0,0 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
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 { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
import type { VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
};
export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate();
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.columnApi.setColumnsVisible(
['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD
);
};
useLayoutEffect(() => {
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
return (
<AgGrid
ref={ref}
rowData={data}
getRowId={({ data }: { data: AssetFieldsFragment }) => data.id}
overlayNoRowsTemplate={t('This chain has no assets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data }: RowClickedEvent) => {
navigate(data.id);
}}
>
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn
flex="2"
headerName={t('ID')}
field="id"
hide={window.innerWidth < BREAKPOINT_MD}
/>
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
/>
<AgGridColumn
headerName={t('Status')}
field="status"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
/>
<AgGridColumn
colId="actions"
headerName=""
sortable={false}
filter={false}
resizable={false}
wrapText={true}
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
) : (
''
)
}
/>
</AgGrid>
);
};
@@ -2,11 +2,11 @@ import React from 'react';
import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response'; import type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { Routes } from '../../routes/route-names'; import { Routes } from '../../routes/route-names';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { TimeAgo } from '../time-ago'; import { TimeAgo } from '../time-ago';
import { TableWithTbody, TableRow, TableCell } from '../table'; import { TableWithTbody, TableRow, TableCell } from '../table';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
interface BlockProps { interface BlockProps {
block: BlockMeta; block: BlockMeta;
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import React from 'react'; import React from 'react';
import type { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response'; import type { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from './block-data'; import { BlockData } from './block-data';
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader'; 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 type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from './block-data'; import { BlockData } from './block-data';
import EmptyList from '../empty-list/empty-list'; import EmptyList from '../empty-list/empty-list';
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket'; import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import { ButtonLink } from '@vegaprotocol/ui-toolkit'; import { ButtonLink } from '@vegaprotocol/ui-toolkit';
interface BlocksRefetchProps { interface BlocksRefetchProps {
@@ -1,56 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import {
Button,
Dialog,
Icon,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
type JsonViewerDialogProps = {
title: string;
content: unknown;
open: boolean;
onChange: (isOpen: boolean) => void;
trigger?: HTMLElement;
};
export const JsonViewerDialog = ({
title,
content,
open,
onChange,
trigger,
}: JsonViewerDialogProps) => {
return (
<Dialog
size="medium"
title={title}
icon={<Icon name="info-sign"></Icon>}
open={open}
onChange={(isOpen) => onChange(isOpen)}
onCloseAutoFocus={(e) => {
/**
* This mimics radix's default behaviour that focuses the dialog's
* trigger after closing itself
*/
if (trigger) {
e.preventDefault();
trigger.focus();
}
}}
>
<div className="pr-8 mb-8 max-h-[70vh] overflow-y-scroll">
<SyntaxHighlighter size="smaller" data={content} />
</div>
<div className="w-1/4">
<Button
data-testid="close-asset-details-dialog"
fill={true}
size="sm"
onClick={() => onChange(false)}
>
{t('Close')}
</Button>
</div>
</Dialog>
);
};
@@ -19,12 +19,12 @@ const EmptyList = ({ heading, label }: EmptyListProps) => {
<div className="mt-4"> <div className="mt-4">
{heading ? ( {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} {heading}
</h1> </h1>
) : null} ) : null}
{label ? ( {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} ) : null}
</div> </div>
</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,60 +1,39 @@
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { Link } from '@vegaprotocol/ui-toolkit';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { ENV } from '../../config/env';
export const Footer = () => { export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment(); const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL, setNodeSwitcherOpen } =
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false); useEnvironment();
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize),
[screenSize]
);
return ( return (
<> <footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200"> <div className="flex justify-between gap-2 align-middle">
<div className="flex justify-between gap-2 align-middle"> <div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
{GIT_COMMIT_HASH && ( {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">
<p data-testid="git-commit-hash"> {t('Version')}:{' '}
{t('Version')}:{' '} <Link
<Link href={
href={ GIT_ORIGIN_URL
GIT_ORIGIN_URL ? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}` : undefined
: undefined }
} target={GIT_ORIGIN_URL ? '_blank' : undefined}
target={GIT_ORIGIN_URL ? '_blank' : undefined} >
> {GIT_COMMIT_HASH}
{GIT_COMMIT_HASH} </Link>
</Link> </p>
</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="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div> </div>
</footer>
<NodeSwitcherDialog <div className="flex pl-2 content-center">
open={nodeSwitcherOpen} {VEGA_URL && <NodeUrl url={VEGA_URL} />}
setOpen={setNodeSwitcherOpen} <Link className="ml-2" onClick={setNodeSwitcherOpen}>
/> {t('Change')}
</> </Link>
</div>
</div>
</footer>
); );
}; };
@@ -13,18 +13,19 @@ jest.mock('../search', () => ({
})); }));
const renderComponent = () => ( const renderComponent = () => (
<MemoryRouter initialEntries={['/txs']}> <MemoryRouter>
<Header /> <Header menuOpen={false} setMenuOpen={jest.fn()} />
</MemoryRouter> </MemoryRouter>
); );
describe('Header', () => { describe('Header', () => {
it('should render navigation', () => { it('should render heading', () => {
render(renderComponent()); render(renderComponent());
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer'); expect(screen.getByTestId('explorer-header')).toHaveTextContent(
'Vega Explorer'
);
}); });
it('should render search', () => { it('should render search', () => {
render(renderComponent()); render(renderComponent());
@@ -1,111 +1,47 @@
import { matchPath, useLocation, useMatch } from 'react-router-dom'; import classnames from 'classnames';
import { import { Link } from 'react-router-dom';
ThemeSwitcher, import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
Navigation, import { t } from '@vegaprotocol/react-helpers';
NavigationList,
NavigationItem,
NavigationLink,
NavigationBreakpoint,
NavigationTrigger,
NavigationContent,
} from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n';
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'; import { Search } from '../search';
import { Routes } from '../../routes/route-names';
import type { Dispatch, SetStateAction } from 'react';
import { NetworkSwitcher } from '@vegaprotocol/environment';
const routeToNavigationItem = (r: Navigable) => ( interface ThemeToggleProps {
<NavigationItem key={r.handle.name}> menuOpen: boolean;
<NavigationLink to={r.path}>{r.handle.text}</NavigationLink> setMenuOpen: Dispatch<SetStateAction<boolean>>;
</NavigationItem> }
);
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]);
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
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 ( return (
<Navigation <header className={headerClasses}>
appName="Explorer" <div className="flex h-full items-center sm:items-stretch gap-4">
theme="system" <Link to={Routes.HOME}>
breakpoints={[490, 900]} <h1
actions={ className="text-white text-3xl font-alpha uppercase calt mb-0"
<> data-testid="explorer-header"
<ThemeSwitcher /> >
{!isHome && <Search />} {t('Vega Explorer')}
</> </h1>
} </Link>
onResize={(width, el) => { <NetworkSwitcher />
if (width < 1157) { </div>
// switch to magnifying glass trigger when width < 1157 <button
el.classList.remove('nav-search-full'); data-testid="open-menu"
el.classList.add('nav-search-compact'); className="md:hidden text-white"
} else { onClick={() => setMenuOpen(!menuOpen)}
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]}
> >
{mainItems.map(routeToNavigationItem)} <Icon name={menuOpen ? 'cross' : 'menu'} />
{groupedItems && groupedItems.length > 0 && ( </button>
<NavigationItem> <Search />
<NavigationTrigger isActive={Boolean(isOnOther)}> <ThemeSwitcher className="-my-4" />
{t('Other')} </header>
</NavigationTrigger>
<NavigationContent>
<NavigationList>
{groupedItems.map(routeToNavigationItem)}
</NavigationList>
</NavigationContent>
</NavigationItem>
)}
</NavigationList>
</Navigation>
); );
}; };
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return ( return (
<div className="flex flex-col text-center "> <div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3> <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} {subtitle}
{tooltipInfo ? ( {tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center"> <Tooltip description={tooltipInfo} align="center">
<span> <span>
<Icon <Icon
name="info-sign" 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> </span>
</Tooltip> </Tooltip>
@@ -44,12 +44,12 @@ export const InfoPanel = ({
text={id} text={id}
startChars={visibleChars} startChars={visibleChars}
endChars={visibleChars} endChars={visibleChars}
className="text-vega-dark-100 dark:text-vega-light-200" className="text-black dark:text-zinc-200"
/> />
) : ( ) : (
<p <p
title={id} title={id}
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..." className="text-black dark:text-zinc-200 truncate ..."
> >
{id} {id}
</p> </p>
@@ -70,7 +70,7 @@ export const InfoPanel = ({
</div> </div>
{copy && ( {copy && (
<CopyWithTooltip text={id}> <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" /> <Icon name="duplicate" />
</button> </button>
</CopyWithTooltip> </CopyWithTooltip>
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import React from 'react'; import React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Routes } from '../../routes/route-names'; import { Routes } from '../../routes/route-names';
@@ -1,6 +1,6 @@
import type { HTMLInputTypeAttribute, SyntheticEvent } from 'react'; import type { HTMLInputTypeAttribute, SyntheticEvent } from 'react';
import { Input, Button } from '@vegaprotocol/ui-toolkit'; import { Input, Button } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
interface JumpToProps { interface JumpToProps {
label: string; label: string;
@@ -0,0 +1,8 @@
query ExplorerAsset($id: ID!) {
asset(id: $id) {
id
name
status
decimals
}
}
@@ -0,0 +1,51 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerAssetQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
export const ExplorerAssetDocument = gql`
query ExplorerAsset($id: ID!) {
asset(id: $id) {
id
name
status
decimals
}
}
`;
/**
* __useExplorerAssetQuery__
*
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerAssetQuery` 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 } = useExplorerAssetQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
}
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
}
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
@@ -1,37 +1,63 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing'; import type { MockedResponse } from '@apollo/client/testing';
import { render, waitFor } from '@testing-library/react'; import { render } from '@testing-library/react';
import { AssetLink } from './asset-link'; import AssetLink from './asset-link';
import { mockAssetA1 } from '../../../mocks/assets'; import { ExplorerAssetDocument } from './__generated__/Asset';
function renderComponent(id: string, mock: MockedResponse[]) { function renderComponent(id: string, mock: MockedResponse[]) {
return ( return (
<MockedProvider mocks={mock} addTypename={false}> <MockedProvider mocks={mock}>
<MemoryRouter> <MemoryRouter>
<AssetLink assetId={id} /> <AssetLink id={id} />
</MemoryRouter> </MemoryRouter>
</MockedProvider> </MockedProvider>
); );
} }
describe('AssetLink', () => { describe('Asset link component', () => {
it('renders the asset id when not found and makes the button disabled', async () => { it('Renders the ID at first', () => {
const res = render(renderComponent('123', [])); const res = render(renderComponent('123', []));
expect(res.getByText('123')).toBeInTheDocument(); expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByTestId('asset-link')).toBeDisabled();
await waitFor(async () => {
expect(await res.queryByText('A ONE')).toBeFalsy();
});
}); });
it('renders the asset name when found and make the button enabled', async () => { it('Renders the asset name when the query returns a result', async () => {
const res = render(renderComponent('123', [mockAssetA1])); const mock = {
expect(res.getByText('123')).toBeInTheDocument(); request: {
query: ExplorerAssetDocument,
variables: {
id: '123',
},
},
result: {
data: {
asset: {
id: '123',
name: 'test-label',
status: 'irrelevant-test-data',
decimals: 18,
},
},
},
};
await waitFor(async () => { const res = render(renderComponent('123', [mock]));
expect(await res.findByText('A ONE')).toBeInTheDocument(); expect(res.getByText('123')).toBeInTheDocument();
expect(await res.findByTestId('asset-link')).not.toBeDisabled(); expect(await res.findByText('test-label')).toBeInTheDocument();
}); });
it('Leaves the asset id when the asset is not found', async () => {
const mock = {
request: {
query: ExplorerAssetDocument,
variables: {
id: '123',
},
},
error: new Error('No such asset'),
};
const res = render(renderComponent('123', [mock]));
expect(await res.findByText('123')).toBeInTheDocument();
}); });
}); });
@@ -1,54 +1,36 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { useExplorerAssetQuery } from './__generated__/Asset';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import Hash from '../hash'; import Hash from '../hash';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import {
useAssetDataProvider,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../../routes/route-names';
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & { export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
assetId: string; id: string;
asDialog?: boolean;
showAssetSymbol?: boolean;
}; };
/** /**
* Given an asset ID, it will fetch the asset name and show that, * Given an asset ID, it will fetch the asset name and show that,
* with a link to the assets modal. If the name does not come back * with a link to the assets list. If the name does not come back
* it will use the ID instead. * it will use the ID instead.
*/ */
export const AssetLink = ({ const AssetLink = ({ id, ...props }: AssetLinkProps) => {
assetId, const { data } = useExplorerAssetQuery({
asDialog, variables: { id },
showAssetSymbol = false, });
...props
}: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open); let label: string = id;
const navigate = useNavigate();
const label = asset if (data?.asset?.name) {
? showAssetSymbol label = data.asset.name;
? asset?.symbol }
: asset?.name
: assetId;
return ( return (
<ButtonLink <Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
data-testid="asset-link"
disabled={!asset}
onClick={(e) => {
if (asDialog) {
open(assetId, e.target as HTMLElement);
} else {
navigate(`${Routes.ASSETS}/${asset?.id}`);
}
}}
{...props}
>
<Hash text={label} /> <Hash text={label} />
</ButtonLink> </Link>
); );
}; };
export default AssetLink;
@@ -2,4 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
export { default as PartyLink } from './party-link/party-link'; export { default as PartyLink } from './party-link/party-link';
export { default as NodeLink } from './node-link/node-link'; export { default as NodeLink } from './node-link/node-link';
export { default as MarketLink } from './market-link/market-link'; export { default as MarketLink } from './market-link/market-link';
export * from './asset-link/asset-link'; export { default as AssetLink } from './asset-link/asset-link';
@@ -2,7 +2,6 @@ query ExplorerMarket($id: ID!) {
market(id: $id) { market(id: $id) {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -8,7 +8,7 @@ export type ExplorerMarketQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null }; export type ExplorerMarketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', decimals: number } } } } } | null };
export const ExplorerMarketDocument = gql` export const ExplorerMarketDocument = gql`
@@ -16,7 +16,6 @@ export const ExplorerMarketDocument = gql`
market(id: $id) { market(id: $id) {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -55,7 +55,6 @@ describe('Market link component', () => {
market: { market: {
id: '123', id: '123',
decimalPlaces: 5, decimalPlaces: 5,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data', state: 'irrelevant-test-data',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -3,7 +3,7 @@ import { useExplorerMarketQuery } from './__generated__/Market';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react'; import type { ComponentProps } from 'react';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import Hash from '../hash'; import Hash from '../hash';
export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & { export type MarketLinkProps = Partial<ComponentProps<typeof Link>> & {
@@ -48,7 +48,7 @@ const MarketLink = ({
<Link <Link
className="underline" className="underline"
{...props} {...props}
to={`/${Routes.MARKETS}/${id}`} to={`/${Routes.MARKETS}#${id}`}
title={id} title={id}
> >
{label} {label}
@@ -56,7 +56,7 @@ const MarketLink = ({
); );
} else { } else {
return ( return (
<Link className="underline" {...props} to={`/${Routes.MARKETS}/${id}`}> <Link className="underline" {...props} to={`/${Routes.MARKETS}#${id}`}>
<Hash text={id} /> <Hash text={id} />
</Link> </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 type { ComponentProps } from 'react';
import Hash from '../hash'; 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>> & { export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string; id: string;
truncate?: boolean;
}; };
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => { const PartyLink = ({ id, ...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>
);
}
return ( return (
<Link <Link
className="underline font-mono" className="underline font-mono"
{...props} {...props}
to={`/${Routes.PARTIES}/${id}`} to={`/${Routes.PARTIES}/${id}`}
> >
<Hash text={truncate ? truncateMiddle(id) : id} /> <Hash text={id} />
</Link> </Link>
); );
}; };
@@ -4,14 +4,13 @@ import { ENV } from '../../../config/env';
import Hash from '../hash'; import Hash from '../hash';
export type ProposalLinkProps = { export type ProposalLinkProps = {
id: string; id: string;
text?: string;
}; };
/** /**
* Given a proposal ID, generates an external link over to * Given a proposal ID, generates an external link over to
* the Governance page for more information * the Governance page for more information
*/ */
const ProposalLink = ({ id, text }: ProposalLinkProps) => { const ProposalLink = ({ id }: ProposalLinkProps) => {
const { data } = useExplorerProposalQuery({ const { data } = useExplorerProposalQuery({
variables: { id }, variables: { id },
}); });
@@ -21,7 +20,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
return ( return (
<ExternalLink href={`${base}/proposals/${id}`}> <ExternalLink href={`${base}/proposals/${id}`}>
{text ? text : <Hash text={label} />} <Hash text={label} />
</ExternalLink> </ExternalLink>
); );
}; };
@@ -0,0 +1,9 @@
import { AppRouter } from '../../routes';
export const Main = () => {
return (
<main className="p-4 overflow-scroll">
<AppRouter />
</main>
);
};
@@ -1,128 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { MarketInfoWithData } from '@vegaprotocol/market-info';
import { LiquidityInfoPanel } from '@vegaprotocol/market-info';
import { LiquidityMonitoringParametersInfoPanel } from '@vegaprotocol/market-info';
import {
InstrumentInfoPanel,
KeyDetailsInfoPanel,
LiquidityPriceRangeInfoPanel,
MetadataInfoPanel,
OracleInfoPanel,
RiskFactorsInfoPanel,
RiskModelInfoPanel,
RiskParametersInfoPanel,
SettlementAssetInfoPanel,
} from '@vegaprotocol/market-info';
import { MarketInfoTable } from '@vegaprotocol/market-info';
import { Link } from 'react-router-dom';
export const MarketDetails = ({ market }: { market: MarketInfoWithData }) => {
if (!market) return null;
const panels = [
{
title: t('Key details'),
content: <KeyDetailsInfoPanel noBorder={false} market={market} />,
},
{
title: t('Instrument'),
content: <InstrumentInfoPanel noBorder={false} market={market} />,
},
{
title: t('Settlement asset'),
content: <SettlementAssetInfoPanel market={market} noBorder={false} />,
},
{
title: t('Metadata'),
content: <MetadataInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk model'),
content: <RiskModelInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk parameters'),
content: <RiskParametersInfoPanel noBorder={false} market={market} />,
},
{
title: t('Risk factors'),
content: <RiskFactorsInfoPanel noBorder={false} market={market} />,
},
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
(trigger, i) => ({
title: t(`Price monitoring trigger ${i + 1}`),
content: <MarketInfoTable noBorder={false} data={trigger} />,
})
),
...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({
title: t(`Price monitoring bound ${i + 1}`),
content: (
<>
<MarketInfoTable
noBorder={false}
data={trigger}
decimalPlaces={market.decimalPlaces}
omits={['referencePrice', '__typename']}
/>
<MarketInfoTable
noBorder={false}
data={{ referencePrice: trigger.referencePrice }}
decimalPlaces={
market.tradableInstrument.instrument.product.settlementAsset
.decimals
}
/>
</>
),
})),
{
title: t('Liquidity monitoring parameters'),
content: (
<LiquidityMonitoringParametersInfoPanel
noBorder={false}
market={market}
/>
),
},
{
title: t('Liquidity'),
content: <LiquidityInfoPanel market={market} noBorder={false} />,
},
{
title: t('Liquidity price range'),
content: (
<LiquidityPriceRangeInfoPanel market={market} noBorder={false} />
),
},
{
title: t('Oracle'),
content: (
<OracleInfoPanel noBorder={false} market={market}>
<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>
</OracleInfoPanel>
),
},
];
return (
<>
{panels.map((p) => (
<div key={p.title} className="mb-3">
<h2 className="font-alpha calt text-xl">{p.title}</h2>
{p.content}
</div>
))}
</>
);
};
@@ -1,132 +0,0 @@
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
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 { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
import type {
VegaICellRendererParams,
VegaValueGetterParams,
} from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { MarketStateMapping } from '@vegaprotocol/types';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { RowClickedEvent } from 'ag-grid-community';
import { Link, useNavigate } from 'react-router-dom';
type MarketsTableProps = {
data: MarketFieldsFragment[] | null;
};
export const MarketsTable = ({ data }: MarketsTableProps) => {
const openAssetDetailsDialog = useAssetDetailsDialogStore(
(state) => state.open
);
const navigate = useNavigate();
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible(
['id', 'state', 'asset'],
window.innerWidth > BREAKPOINT_MD
);
};
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
return (
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: MarketFieldsFragment }) => data.id}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if ((event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON') {
navigate(data.id);
}
}}
>
<AgGridColumn
colId="code"
headerName={t('Code')}
field="tradableInstrument.instrument.code"
/>
<AgGridColumn
colId="name"
headerName={t('Name')}
field="tradableInstrument.instrument.name"
/>
<AgGridColumn
headerName={t('Status')}
field="state"
hide={window.innerWidth <= BREAKPOINT_MD}
valueGetter={({
data,
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
return data?.state ? MarketStateMapping[data?.state] : '-';
}}
/>
<AgGridColumn
colId="asset"
headerName={t('Settlement asset')}
field="tradableInstrument.instrument.product.settlementAsset"
hide={window.innerWidth <= BREAKPOINT_MD}
cellRenderer={({
value,
}: VegaICellRendererParams<
MarketFieldsFragment,
'tradableInstrument.instrument.product.settlementAsset'
>) =>
value ? (
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value.id, e.target as HTMLElement);
}}
>
{value.symbol}
</ButtonLink>
) : (
''
)
}
/>
<AgGridColumn
flex={2}
headerName={t('Market ID')}
field="id"
hide={window.innerWidth <= BREAKPOINT_MD}
/>
<AgGridColumn
colId="actions"
headerName=""
field="id"
cellRenderer={({
value,
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
value ? (
<Link className="underline" to={value}>
{t('View details')}
</Link>
) : (
''
)
}
/>
</AgGrid>
);
};
@@ -0,0 +1,45 @@
import { NavLink } from 'react-router-dom';
import routerConfig from '../../routes/router-config';
import classnames from 'classnames';
interface NavProps {
menuOpen: boolean;
}
export const Nav = ({ menuOpen }: NavProps) => {
return (
<nav className="relative">
<div
className={classnames(
'absolute top-0 z-50 md:static',
'w-full p-4 md:border-r border-neutral-700 dark:border-neutral-300',
'bg-white dark:bg-black',
'transition-[right]',
{
'right-0 h-[100vh]': menuOpen,
'right-[200vw] h-full': !menuOpen,
}
)}
>
{routerConfig.map((r) => (
<NavLink
key={r.name}
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>
))}
</div>
</nav>
);
};
@@ -83,7 +83,7 @@ const NestedDataListItem = ({
}); });
const titleClasses = classNames({ 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, 'text-base font-medium whitespace-nowrap': !hasChildren,
}); });
@@ -117,7 +117,7 @@ const NestedDataListItem = ({
)} )}
</h4> </h4>
{!hasChildren && ( {!hasChildren && (
<code className="text-vega-light-100 mb-2 last:mb-0 dark:text-vega-dark-100 break-all"> <code className="text-vega-light-400 mb-2 last:mb-0 dark:text-vega-dark-400 break-all">
{JSON.stringify(value, null, ' ')} {JSON.stringify(value, null, ' ')}
</code> </code>
)} )}
@@ -19,7 +19,6 @@ fragment ExplorerDeterministicOrderFields on Order {
market { market {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -3,7 +3,7 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } }; export type ExplorerDeterministicOrderFieldsFragment = { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } };
export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
orderId: Types.Scalars['ID']; orderId: Types.Scalars['ID'];
@@ -11,7 +11,7 @@ export type ExplorerDeterministicOrderQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } }; export type ExplorerDeterministicOrderQuery = { __typename?: 'Query', orderByID: { __typename?: 'Order', id: string, type?: Types.OrderType | null, reference: string, status: Types.OrderStatus, version: string, createdAt: any, updatedAt?: any | null, expiresAt?: any | null, timeInForce: Types.OrderTimeInForce, price: string, side: Types.Side, remaining: string, size: string, rejectionReason?: Types.OrderRejectionReason | null, party: { __typename?: 'Party', id: string }, market: { __typename?: 'Market', id: string, decimalPlaces: number, state: Types.MarketState, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } } };
export const ExplorerDeterministicOrderFieldsFragmentDoc = gql` export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
fragment ExplorerDeterministicOrderFields on Order { fragment ExplorerDeterministicOrderFields on Order {
@@ -35,7 +35,6 @@ export const ExplorerDeterministicOrderFieldsFragmentDoc = gql`
market { market {
id id
decimalPlaces decimalPlaces
positionDecimalPlaces
tradableInstrument { tradableInstrument {
instrument { instrument {
name name
@@ -61,7 +61,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
__typename: 'Market', __typename: 'Market',
id: '789', id: '789',
state: 'STATUS_ACTIVE', state: 'STATUS_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: '5', decimalPlaces: '5',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -89,7 +88,6 @@ function renderExistingAmend(id: string, version: number, amend: Amend) {
market: { market: {
id: '789', id: '789',
decimalPlaces: 5, decimalPlaces: 5,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data', state: 'irrelevant-test-data',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -1,4 +1,4 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order'; import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import { MarketLink } from '../links'; import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market'; import PriceInMarket from '../price-in-market/price-in-market';
@@ -1,10 +1,9 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import { useExplorerDeterministicOrderQuery } from './__generated__/Order'; import { useExplorerDeterministicOrderQuery } from './__generated__/Order';
import { MarketLink } from '../links'; import { MarketLink } from '../links';
import PriceInMarket from '../price-in-market/price-in-market'; import PriceInMarket from '../price-in-market/price-in-market';
import { Time } from '../time'; import { Time } from '../time';
import { sideText, statusText, tifFull, tifShort } from './lib/order-labels'; import { sideText, statusText, tifFull, tifShort } from './lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
export interface DeterministicOrderDetailsProps { export interface DeterministicOrderDetailsProps {
id: string; id: string;
@@ -13,7 +12,7 @@ export interface DeterministicOrderDetailsProps {
} }
export const wrapperClasses = 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 * This component renders the *current* details for an order
@@ -42,7 +41,7 @@ const DeterministicOrderDetails = ({
<h2 className="text-3xl font-bold mb-4 display-5"> <h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')} {t('Order not found')}
</h2> </h2>
<p className="text-vega-light-400 mb-12"> <p className="text-gray-500 mb-12">
{t('No order created from this transaction')} {t('No order created from this transaction')}
</p> </p>
</div> </div>
@@ -91,7 +90,7 @@ const DeterministicOrderDetails = ({
<div className="mb-12 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2> <h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0"> <h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.size} marketId={o.market.id} /> {o.size}
</h5> </h5>
</div> </div>
@@ -101,7 +100,7 @@ const DeterministicOrderDetails = ({
{t('Remaining')} {t('Remaining')}
</h2> </h2>
<h5 className="text-lg font-medium text-gray-500 mb-0"> <h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.remaining} marketId={o.market.id} /> {o.remaining}
</h5> </h5>
</div> </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 * as Schema from '@vegaprotocol/types';
import type { components } from '../../../../types/explorer'; 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 '-';
}
@@ -38,7 +38,6 @@ const mock = {
__typename: 'Market', __typename: 'Market',
id: '789', id: '789',
state: 'STATE_ACTIVE', state: 'STATE_ACTIVE',
positionDecimalPlaces: 2,
decimalPlaces: 2, decimalPlaces: 2,
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -1,7 +1,6 @@
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order'; import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
import PriceInMarket from '../price-in-market/price-in-market'; import PriceInMarket from '../price-in-market/price-in-market';
import { sideText } from '../order-details/lib/order-labels'; import { sideText } from '../order-details/lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
// Note: Edited has no style currently // Note: Edited has no style currently
export type OrderSummaryModifier = 'cancelled' | 'edited'; export type OrderSummaryModifier = 'cancelled' | 'edited';
@@ -42,8 +41,7 @@ const OrderSummary = ({ id, modifier }: OrderSummaryProps) => {
return ( return (
<div data-testid="order-summary" className={getClassName(modifier)}> <div data-testid="order-summary" className={getClassName(modifier)}>
<span>{sideText[order.side]}</span>&nbsp; <span>{sideText[order.side]}</span>&nbsp;
<SizeInMarket marketId={order.market.id} size={order.size} /> <span>{order.size}</span>&nbsp;<i>@</i>&nbsp;
&nbsp;<i>@</i>&nbsp;
<PriceInMarket marketId={order.market.id} price={order.price} /> <PriceInMarket marketId={order.market.id} price={order.price} />
</div> </div>
); );
@@ -90,7 +90,6 @@ describe('Order TX Summary component', () => {
market: { market: {
id: '123', id: '123',
decimalPlaces: 2, decimalPlaces: 2,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data', state: 'irrelevant-test-data',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -112,14 +111,13 @@ describe('Order TX Summary component', () => {
const res = renderComponent(o, [mock]); const res = renderComponent(o, [mock]);
expect(res.queryByTestId('order-summary')).toBeInTheDocument(); expect(res.queryByTestId('order-summary')).toBeInTheDocument();
expect(res.getByText('Buy')).toBeInTheDocument(); expect(res.getByText('Buy')).toBeInTheDocument();
// Initially renders price and size unformatted
expect(res.getByText('333')).toBeInTheDocument();
expect(res.getByText('10')).toBeInTheDocument(); expect(res.getByText('10')).toBeInTheDocument();
// Initially renders price alone
expect(res.getByText('333')).toBeInTheDocument();
// After fetch renders formatted price and asset quotename // After fetch renders formatted price and asset quotename
expect(await res.findByText('3.33')).toBeInTheDocument(); expect(await res.findByText('3.33')).toBeInTheDocument();
expect(await res.findByText('TEST')).toBeInTheDocument(); expect(await res.findByText('TEST')).toBeInTheDocument();
expect(await res.getByText('0.10')).toBeInTheDocument();
}); });
}); });
@@ -2,7 +2,6 @@ import type { components } from '../../../types/explorer';
import PriceInMarket from '../price-in-market/price-in-market'; import PriceInMarket from '../price-in-market/price-in-market';
import { sideText } from '../order-details/lib/order-labels'; import { sideText } from '../order-details/lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
export type OrderSummaryProps = { export type OrderSummaryProps = {
order: components['schemas']['v1OrderSubmission']; order: components['schemas']['v1OrderSubmission'];
@@ -30,12 +29,7 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
return ( return (
<div data-testid="order-summary"> <div data-testid="order-summary">
<span>{sideText[order.side]}</span>&nbsp; <span>{sideText[order.side]}</span>&nbsp;
{order.size ? ( <span>{order.size}</span>&nbsp;<i className="text-xs">@</i>&nbsp;
<SizeInMarket size={order.size} marketId={order.marketId} />
) : (
'-'
)}
&nbsp;<i className="text-xs">@</i>&nbsp;
<PriceInMarket <PriceInMarket
marketId={order.marketId} marketId={order.marketId}
price={order.price} price={order.price}
@@ -20,7 +20,7 @@ export const PageHeader = ({
copy = false, copy = false,
className, className,
}: PageHeaderProps) => { }: PageHeaderProps) => {
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt'; const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha';
return ( return (
<header className={className}> <header className={className}>
<span className={`${titleClasses} block`}>{prefix}</span> <span className={`${titleClasses} block`}>{prefix}</span>
@@ -38,7 +38,7 @@ export const PageHeader = ({
</h2> </h2>
{copy && ( {copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}> <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="" /> <Icon name="duplicate" className="" />
</button> </button>
</CopyWithTooltip> </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>
);
};
@@ -8,7 +8,7 @@ interface PanelProps {
export const Panel = ({ children, className }: PanelProps) => ( export const Panel = ({ children, className }: PanelProps) => (
<div <div
className={classNames( className={classNames(
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5', 'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
className className
)} )}
> >
@@ -37,7 +37,6 @@ const fullMock = {
market: { market: {
id: '123', id: '123',
decimalPlaces: 2, decimalPlaces: 2,
positionDecimalPlaces: 2,
state: 'irrelevant-test-data', state: 'irrelevant-test-data',
tradableInstrument: { tradableInstrument: {
instrument: { instrument: {
@@ -1,5 +1,4 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/i18n';
import isUndefined from 'lodash/isUndefined'; import isUndefined from 'lodash/isUndefined';
import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market'; import { useExplorerMarketQuery } from '../links/market-link/__generated__/Market';
import get from 'lodash/get'; import get from 'lodash/get';
@@ -1,224 +0,0 @@
import type { ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/datagrid';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { NetworkParams, useNetworkParams } from '@vegaprotocol/react-helpers';
import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
type ProposalTermsDialog = {
open: boolean;
title: string;
content: unknown;
};
type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null;
};
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Token);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD
);
gridRef.current?.columnApi.setColumnWidth(
'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80
);
};
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
title: '',
content: null,
});
return (
<>
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
data.id || data.rationale.title
}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if (
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
) {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data.id)
);
window.open(proposalPage, '_blank');
}
}}
>
<AgGridColumn
colId="title"
headerName={t('Title')}
field="rationale.title"
flex={2}
wrapText={true}
/>
<AgGridColumn
colId="type"
maxWidth={180}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Type')}
field="terms.change.__typename"
/>
<AgGridColumn
maxWidth={100}
headerName={t('State')}
field="state"
valueFormatter={({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
}}
/>
<AgGridColumn
colId="voting"
maxWidth={100}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Voting')}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
}}
/>
<AgGridColumn
colId="cDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Closing date')}
field="terms.closingDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="eDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Enactment date')}
field="terms.enactmentDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="actions"
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
maxWidth={221}
sortable={false}
filter={false}
resizable={false}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button
className="underline max-md:hidden"
onClick={openDialog}
>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
}}
/>
</AgGrid>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
title={dialog.title}
content={dialog.content}
/>
</>
);
};
@@ -1,5 +1,5 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/react-helpers';
import { Button } from '@vegaprotocol/ui-toolkit'; import React from 'react';
import { StatusMessage } from '../status-message'; import { StatusMessage } from '../status-message';
interface RenderFetchedProps { interface RenderFetchedProps {
@@ -8,7 +8,6 @@ interface RenderFetchedProps {
loading: boolean | undefined; loading: boolean | undefined;
className?: string; className?: string;
errorMessage?: string; errorMessage?: string;
refetch?: () => void;
} }
export const RenderFetched = ({ export const RenderFetched = ({
@@ -17,7 +16,6 @@ export const RenderFetched = ({
children, children,
className, className,
errorMessage = t('Error retrieving data'), errorMessage = t('Error retrieving data'),
refetch,
}: RenderFetchedProps) => { }: RenderFetchedProps) => {
if (loading) { if (loading) {
return ( return (
@@ -26,20 +24,7 @@ export const RenderFetched = ({
} }
if (error) { if (error) {
return ( return <StatusMessage className={className}>{errorMessage}</StatusMessage>;
<>
<StatusMessage className={className}>{errorMessage}</StatusMessage>
{refetch && (
<Button
onClick={() => {
refetch();
}}
>
{t('Try again')}
</Button>
)}
</>
);
} }
return children; return children;
@@ -13,7 +13,7 @@ export const RouteTitle = ({
...props ...props
}: RouteTitleProps) => { }: RouteTitleProps) => {
const classes = classnames( const classes = classnames(
'font-alpha calt', 'font-alpha',
'text-4xl', 'text-4xl',
'uppercase', 'uppercase',
'mb-8', 'mb-8',
@@ -0,0 +1,32 @@
import { t } from '@vegaprotocol/react-helpers';
import React from 'react';
interface RouteErrorBoundaryProps {
children: React.ReactElement;
}
export class RouteErrorBoundary extends React.Component<
RouteErrorBoundaryProps,
{ hasError: boolean }
> {
constructor(props: RouteErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
override componentDidCatch(error: Error) {
console.log(`Error caught in App error boundary ${error.message}`, error);
}
override render() {
if (this.state.hasError) {
return <h1>{t('Something went wrong')}</h1>;
}
return this.props.children;
}
}
@@ -1,70 +1,189 @@
import { import {
determineType, detectTypeByFetching,
detectTypeFromQuery,
getSearchType,
isBlock, isBlock,
isHexadecimal,
isNetworkParty, isNetworkParty,
isNonHex,
SearchTypes, SearchTypes,
isHash, toHex,
toNonHex,
} from './detect-search'; } from './detect-search';
import { DATA_SOURCES } from '../../config';
global.fetch = jest.fn(); global.fetch = jest.fn();
describe('Detect Search', () => { describe('Detect Search', () => {
it.each([ it("should detect that it's a hexadecimal", () => {
['0000000000000000000000000000000000000000000000000000000000000000', true], const expected = true;
['0000000000000000000000000000000000000000000000000000000000000001', true], const testString =
[ '0x073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
'LOOONG0000000000000000000000000000000000000000000000000000000000000000', const actual = isHexadecimal(testString);
false, expect(actual).toBe(expected);
], });
['xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', false],
['something else', false], it("should detect that it's not hexadecimal", () => {
])("should detect that it's a hash", (input, expected) => { const expected = true;
expect(isHash(input)).toBe(expected); const testString =
'073ceaab59e5f2dd0561dec4883e7ee5bc7165cd4de34717a3ab8f2cbe3007f9';
const actual = isNonHex(testString);
expect(actual).toBe(expected);
}); });
it("should detect that it's a network party", () => { it("should detect that it's a network party", () => {
expect(isNetworkParty('network')).toBe(true); const expected = true;
expect(isNetworkParty('web')).toBe(false); const testString = 'network';
const actual = isNetworkParty(testString);
expect(actual).toBe(expected);
}); });
it("should detect that it's a block", () => { it("should detect that it's a block", () => {
expect(isBlock('123')).toBe(true); const expected = true;
expect(isBlock('x123')).toBe(false); const testString = '3188';
const actual = isBlock(testString);
expect(actual).toBe(expected);
}); });
it.each([ it('should convert from non-hex to hex', () => {
[ const expected = '0x123';
'0000000000000000000000000000000000000000000000000000000000000000', const testString = '123';
SearchTypes.Transaction, const actual = toHex(testString);
], expect(actual).toBe(expected);
[ });
'0000000000000000000000000000000000000000000000000000000000000001',
SearchTypes.Party, it('should convert from hex to non-hex', () => {
], const expected = '123';
['123', SearchTypes.Block], const testString = '0x123';
['network', SearchTypes.Party], const actual = toNonHex(testString);
['something else', SearchTypes.Unknown], expect(actual).toBe(expected);
])( });
"detectTypeByFetching should call fetch with non-hex query it's a transaction",
async (input, type) => { it("should detect type client side from query if it's a hexadecimal", () => {
// @ts-ignore issue related to polyfill const expected = [SearchTypes.Party, SearchTypes.Transaction];
fetch.mockImplementation( const testString =
jest.fn(() => '0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
Promise.resolve({ const actual = detectTypeFromQuery(testString);
ok: expect(actual).toStrictEqual(expected);
input === });
'0000000000000000000000000000000000000000000000000000000000000000',
json: () => it("should detect type client side from query if it's a non hex", () => {
Promise.resolve({ const expected = [SearchTypes.Party, SearchTypes.Transaction];
transaction: { const testString =
hash: input, '4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
}, const actual = detectTypeFromQuery(testString);
}), expect(actual).toStrictEqual(expected);
}) });
)
); it("should detect type client side from query if it's a network party", () => {
const result = await determineType(input); const expected = [SearchTypes.Party];
expect(result).toBe(type); const testString = 'network';
} const actual = detectTypeFromQuery(testString);
); expect(actual).toStrictEqual(expected);
});
it("should detect type client side from query if it's a block (number)", () => {
const expected = [SearchTypes.Block];
const testString = '23432';
const actual = detectTypeFromQuery(testString);
expect(actual).toStrictEqual(expected);
});
it("detectTypeByFetching should call fetch with non-hex query it's a transaction", async () => {
const query = '0xabc';
const type = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await detectTypeByFetching(query);
expect(fetch).toHaveBeenCalledWith(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${toNonHex(query)}`
);
expect(result).toBe(type);
});
it("detectTypeByFetching should call fetch with non-hex query it's a party", async () => {
const query = 'abc';
const type = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await detectTypeByFetching(query);
expect(result).toBe(type);
});
it('getSearchType should return party from fetch response', async () => {
const query =
'0x4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Party;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: false,
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return transaction from fetch response', async () => {
const query =
'4624293CFE3D8B67A0AB448BAFF8FBCF1A1B770D9D5F263761D3D6CBEA94D97F';
const expected = SearchTypes.Transaction;
// @ts-ignore issue related to polyfill
fetch.mockImplementation(
jest.fn(() =>
Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
transaction: {
hash: query,
},
}),
})
)
);
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return undefined from transaction response', async () => {
const query = 'u';
const expected = undefined;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return block if query is number', async () => {
const query = '123';
const expected = SearchTypes.Block;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
it('getSearchType should return party if query is network', async () => {
const query = 'network';
const expected = SearchTypes.Party;
const result = await getSearchType(query);
expect(result).toBe(expected);
});
}); });
@@ -1,4 +1,3 @@
import { remove0x } from '@vegaprotocol/utils';
import { DATA_SOURCES } from '../../config'; import { DATA_SOURCES } from '../../config';
import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response'; import type { BlockExplorerTransaction } from '../../routes/types/block-explorer-response';
@@ -7,20 +6,15 @@ export enum SearchTypes {
Party = 'party', Party = 'party',
Block = 'block', Block = 'block',
Order = 'order', Order = 'order',
Unknown = 'unknown',
} }
export const HASH_LENGTH = 64; export const TX_LENGTH = 64;
export const isHash = (value: string) =>
/[0-9a-fA-F]+/.test(remove0x(value)) &&
remove0x(value).length === HASH_LENGTH;
export const isHexadecimal = (search: string) => export const isHexadecimal = (search: string) =>
search.startsWith('0x') && search.length === 2 + HASH_LENGTH; search.startsWith('0x') && search.length === 2 + TX_LENGTH;
export const isNonHex = (search: string) => export const isNonHex = (search: string) =>
!search.startsWith('0x') && search.length === HASH_LENGTH; !search.startsWith('0x') && search.length === TX_LENGTH;
export const isBlock = (search: string) => !Number.isNaN(Number(search)); export const isBlock = (search: string) => !Number.isNaN(Number(search));
@@ -29,44 +23,121 @@ export const isNetworkParty = (search: string) => search === 'network';
export const toHex = (query: string) => export const toHex = (query: string) =>
isHexadecimal(query) ? query : `0x${query}`; isHexadecimal(query) ? query : `0x${query}`;
export const toNonHex = remove0x; export const toNonHex = (query: string) =>
isNonHex(query) ? query : `${query.replace('0x', '')}`;
/** export const detectTypeFromQuery = (
* Determine the type of the given query query: string
*/ ): SearchTypes[] | undefined => {
export const determineType = async (query: string): Promise<SearchTypes> => { const i = query.toLowerCase();
const value = query.toLowerCase();
if (isHash(value)) { if (isHexadecimal(i) || isNonHex(i)) {
// it can be either `SearchTypes.Party` or `SearchTypes.Transaction` return [SearchTypes.Party, SearchTypes.Transaction];
if (await isTransactionHash(value)) { } else if (isNetworkParty(i)) {
return SearchTypes.Transaction; return [SearchTypes.Party];
} else { } else if (isBlock(i)) {
return SearchTypes.Party; return [SearchTypes.Block];
}
} else if (isNetworkParty(value)) {
return SearchTypes.Party;
} else if (isBlock(value)) {
return SearchTypes.Block;
} }
return SearchTypes.Unknown;
return undefined;
}; };
/** export const detectTypeByFetching = async (
* Checks if given input is a transaction hash by querying the transactions query: string
* endpoint ): Promise<SearchTypes | undefined> => {
*/ const hash = toNonHex(query);
export const isTransactionHash = async (input: string): Promise<boolean> => {
const hash = remove0x(input);
const request = await fetch( const request = await fetch(
`${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}` `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
); );
if (request?.ok) { if (request?.ok) {
const body: BlockExplorerTransaction = await request.json(); const body: BlockExplorerTransaction = await request.json();
if (body?.transaction) { if (body?.transaction) {
return true; return SearchTypes.Transaction;
} }
} }
return false; return SearchTypes.Party;
};
// Code commented out because the current solution to detect a hex is temporary (by process of elimination)
// export const detectTypeByFetching = async (
// query: string,
// type: SearchTypes
// ): Promise<SearchTypes | undefined> => {
// const TYPES = [SearchTypes.Party, SearchTypes.Transaction];
//
// if (!TYPES.includes(type)) {
// throw new Error('Search type provided not recognised');
// }
//
// if (type === SearchTypes.Transaction) {
// const hash = toNonHex(query);
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions/${hash}`
// );
//
// if (request?.ok) {
// const body: BlockExplorerTransaction = await request.json();
//
// if (body?.transaction) {
// return SearchTypes.Transaction;
// }
// }
// } else if (type === SearchTypes.Party) {
// const party = toNonHex(query);
//
// const request = await fetch(
// `${DATA_SOURCES.blockExplorerUrl}/transactions?limit=1&filters[tx.submitter]=${party}`
// );
//
// if (request.ok) {
// const body: BlockExplorerTransactions = await request.json();
//
// if (body?.transactions?.length) {
// return SearchTypes.Party;
// }
// }
// }
//
// return undefined;
// };
// export const getSearchType = async (
// query: string
// ): Promise<SearchTypes | undefined> => {
// const searchTypes = detectTypeFromQuery(query);
// const hasResults = searchTypes?.length;
//
// if (hasResults) {
// if (hasResults > 1) {
// const promises = searchTypes.map((type) =>
// detectTypeByFetching(query, type)
// );
// const results = await Promise.all(promises);
// return results.find((result) => result !== undefined);
// }
//
// return searchTypes[0];
// }
//
// return undefined;
// };
export const getSearchType = async (
query: string
): Promise<SearchTypes | undefined> => {
const searchTypes = detectTypeFromQuery(query);
const hasResults = searchTypes?.length;
if (hasResults) {
if (hasResults > 1) {
return await detectTypeByFetching(query);
}
return searchTypes[0];
}
return undefined;
}; };

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