Commit 6fbf652a authored by isstuev's avatar isstuev
parents e94d00e5 f54a17da
......@@ -31,6 +31,9 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SU
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
......@@ -18,6 +18,8 @@ module.exports = {
'plugin:regexp/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
],
plugins: [
'es5',
......@@ -27,6 +29,7 @@ module.exports = {
'react-hooks',
'jsx-a11y',
'eslint-plugin-import-helpers',
'jest',
],
parser: '@typescript-eslint/parser',
parserOptions: {
......
......@@ -23,6 +23,9 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......@@ -45,7 +48,7 @@ jobs:
- name: Add outputs
run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}"
echo "::set-output name=short-sha::${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}"
id: output-step
- name: Build and push
......@@ -56,21 +59,22 @@ jobs:
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
GIT_COMMIT_SHA=${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
deploy_frontend:
name: Deploy frontend app
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
globalEnv: review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }}
frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -11,7 +11,7 @@
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -77,7 +77,7 @@
},
"presentation": {
"reveal": "never",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -91,7 +91,7 @@
"detail": "run eslint",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"revealProblems": "onProblem",
},
"icon": {
......@@ -108,11 +108,11 @@
"type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: local for current file",
"label": "pw: local",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -127,11 +127,11 @@
"type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for current file",
"label": "pw: docker",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -146,11 +146,11 @@
"type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for all files",
"label": "pw: docker all",
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -167,11 +167,11 @@
"type": "npm",
"script": "test:jest",
"problemMatcher": [],
"label": "test: jest",
"label": "jest",
"detail": "run jest tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -186,11 +186,11 @@
"type": "npm",
"script": "test:jest:watch",
"problemMatcher": [],
"label": "test: jest: watch",
"label": "jest: watch all",
"detail": "run jest tests in watch mode",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"focus": true,
},
......@@ -206,11 +206,11 @@
"type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [],
"label": "test: jest: watch curent file",
"label": "jest: watch",
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -230,7 +230,7 @@
"detail": "build docker image",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
"focus": true,
......@@ -251,7 +251,7 @@
"detail": "run docker container for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -271,7 +271,7 @@
"detail": "format svg files with svgo",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......
......@@ -64,8 +64,6 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
......@@ -73,6 +71,9 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
### App configuration
......
......@@ -89,6 +89,8 @@ const config = Object.freeze({
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)',
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
},
......
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Astar (EVM)','url':'https://blockscout.com/astar','group':'mainnets','type':'astar'},{'title':'Shiden (EVM)','url':'https://blockscout.com/shiden','group':'mainnets','type':'astar'},{'title':'Klaytn Mainnet (Cypress)','url':'https://klaytn-mainnet.aws-k8s.blockscout.com/','group':'mainnets','type':'klaytn'},{'title':'Goerli','url':'https://blockscout.com/eth/goerli/','group':'testnets','type':'goerli'},{'title':'Optimism Goerli','url':'https://blockscout.com/optimism/goerli/','group':'testnets','type':'optimism_goerli'},{'title':'Optimism Bedrock Alpha','url':'https://blockscout.com/optimism/bedrock-alpha','group':'testnets','type':'optimism_bedrock_alpha'},{'title':'Gnosis Chiado','url':'https://blockscout.com/gnosis/chiado/','group':'testnets','type':'gnosis_chiado'},{'title':'Shibuya (EVM)','url':'https://blockscout.com/shibuya','group':'testnets','type':'shibuya'},{'title':'Optimism Opcraft','url':'https://blockscout.com/optimism/opcraft','group':'other','type':'optimism_opcraft'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'other','type':'optimism_gnosis'},{'title':'ARTIS-Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'other','type':'poa_core'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'other','type':'poa_sokol'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx'}}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]
# network config
NEXT_PUBLIC_NETWORK_NAME=Ethereum
......
......@@ -2,8 +2,9 @@
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction'}}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
# network config
NEXT_PUBLIC_NETWORK_NAME=POA
......
# app config
NEXT_PUBLIC_APP_HOST=blockscout.com
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3100
NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_APP_ENV=testing
......
......@@ -160,14 +160,14 @@ postgres:
resources:
limits:
memory:
_default: "4Gi"
_default: "6Gi"
cpu:
_default: "3"
_default: "4"
requests:
memory:
_default: "4Gi"
_default: "6Gi"
cpu:
_default: "3"
_default: "4"
environment:
POSTGRES_USER:
......@@ -473,7 +473,7 @@ frontend:
NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io','paths':{'tx':'/tx'}}]"
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
# network config
NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum
......
......@@ -65,7 +65,7 @@ geth:
frontend:
environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str]
_default: ENC[AES256_GCM,data:yShwsa6ajoFXg/6QSgEARkZRVVrwrdsR69NSmyvBH2O5EUQ0OvsWpW64,iv:K/HT6C9pYCK63LNyF3HERFc79vDS4cB0H4pINIlNhh0=,tag:X0HqeAP01diTvDOwoEP6lw==,type:str]
NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
......@@ -78,8 +78,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-16T11:55:15Z"
mac: ENC[AES256_GCM,data:wd7HZEGH1fJO1ufaUeBtcjUaHS21rcOoiGaQPNptEyEPj6q/60rJ1YQmGqkgi3DNVnGly5FQyYjMIIY3/YqXqTZI6MBhJp4RmpCELbqqzQbAFvbomYURmqG/umeT2+kMrSIF/PXrt4d51e1cod2+H4OY9V09VerH9L07D0nTd48=,iv:dDeTSqvmwps4oQKRVgDqmMf/uxf7Egb+jufwTKtm6F4=,tag:CqOCA4XW7d3C5D4dflIFug==,type:str]
lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -3,7 +3,7 @@ global:
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: true
enabled: false
image:
_default: blockscout/blockscout:latest
replicas:
......@@ -117,7 +117,7 @@ blockscout:
_default: 'true'
postgres:
enabled: true
enabled: false
image: postgres:13.8
port: 5432
......@@ -145,7 +145,7 @@ postgres:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
enabled: false
image:
_default: ethereum/client-go:stable
replicas:
......@@ -200,7 +200,7 @@ geth:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
enabled: false
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
......@@ -347,19 +347,19 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
_default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
_default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
_default: ethereum
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core
_default: goerli
NEXT_PUBLIC_NETWORK_ID:
_default: 77
_default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
_default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA
_default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
......@@ -370,6 +370,7 @@ frontend:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
......@@ -380,5 +381,11 @@ frontend:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: [{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2 19.4c3.972 0 7.2-3.228 7.2-7.2S16.172 5 12.2 5A7.206 7.206 0 0 0 5 12.2c0 3.972 3.228 7.2 7.2 7.2Zm-5.574-4.332 2.926-.023c.325 1.173.871 2.311 1.614 3.333a6.28 6.28 0 0 1-4.54-3.31Zm3.67-4.842h3.646a9.853 9.853 0 0 1 .023 3.867l-3.67.023a9.544 9.544 0 0 1 0-3.89Zm1.823 7.885a9.323 9.323 0 0 1-1.591-3.066l3.193-.023a9.813 9.813 0 0 1-1.602 3.089Zm.929.302a10.24 10.24 0 0 0 1.637-3.403l3.124-.023a6.286 6.286 0 0 1-4.761 3.426ZM18.47 12.2c0 .65-.105 1.277-.279 1.858l-3.286.023c.232-1.277.22-2.578-.023-3.855h3.263c.209.615.325 1.289.325 1.974Zm-.72-2.903h-3.09a10.675 10.675 0 0 0-1.613-3.31 6.244 6.244 0 0 1 4.703 3.31Zm-4.053 0H10.54a9.593 9.593 0 0 1 1.58-3.008 9.595 9.595 0 0 1 1.58 3.008Zm-2.532-3.275a10.317 10.317 0 0 0-1.59 3.275H6.648a6.249 6.249 0 0 1 4.517-3.275Zm-1.811 4.204a10.685 10.685 0 0 0-.012 3.89l-3.1.023a6.173 6.173 0 0 1 .012-3.914h3.1Z" fill="currentColor"/>
<path d="M12.2 19.4c3.972 0 7.2-3.228 7.2-7.2S16.172 5 12.2 5A7.206 7.206 0 0 0 5 12.2c0 3.972 3.228 7.2 7.2 7.2Zm-5.574-4.332 2.926-.023a10.424 10.424 0 0 0 1.614 3.333 6.28 6.28 0 0 1-4.54-3.31Zm3.67-4.842h3.646a9.853 9.853 0 0 1 .023 3.867l-3.67.023a9.544 9.544 0 0 1 0-3.89Zm1.823 7.885a9.323 9.323 0 0 1-1.591-3.066l3.193-.023a9.813 9.813 0 0 1-1.602 3.089Zm.929.302a10.24 10.24 0 0 0 1.637-3.403l3.124-.023a6.286 6.286 0 0 1-4.761 3.426ZM18.47 12.2c0 .65-.105 1.277-.279 1.858l-3.286.023c.232-1.277.22-2.578-.023-3.855h3.263a6.17 6.17 0 0 1 .325 1.974Zm-.72-2.903h-3.09a10.675 10.675 0 0 0-1.613-3.31 6.244 6.244 0 0 1 4.703 3.31Zm-4.053 0H10.54a9.593 9.593 0 0 1 1.58-3.008 9.595 9.595 0 0 1 1.58 3.008Zm-2.532-3.275a10.317 10.317 0 0 0-1.59 3.275H6.648a6.249 6.249 0 0 1 4.517-3.275Zm-1.811 4.204a10.685 10.685 0 0 0-.012 3.89l-3.1.023a6.173 6.173 0 0 1 .012-3.914h3.1Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.004 2.61 13.2 8.432l1.443-3.434 6.36-2.386Z" fill="#E2761B"/>
<path d="m2.988 2.61 7.741 5.876-1.372-3.489-6.369-2.386Zm15.208 13.492L16.117 19.3l4.447 1.229 1.279-4.356-3.647-.07Zm-16.032.071 1.271 4.356L7.882 19.3l-2.078-3.198-3.64.071Z" fill="#E4761B"/>
<path d="m7.631 10.7-1.24 1.882 4.417.197-.157-4.765L7.63 10.7Zm8.73 0L13.3 7.959l-.101 4.82 4.408-.197-1.248-1.883Zm-8.479 8.6 2.651-1.3-2.29-1.795-.36 3.095ZM13.46 18l2.658 1.3-.368-3.095L13.459 18Z" fill="#E4761B"/>
<path d="M16.117 19.3 13.458 18l.212 1.741-.023.733 2.47-1.174Zm-8.235 0 2.47 1.174-.015-.733.196-1.74-2.65 1.299Z" fill="#D7C1B3"/>
<path d="M10.392 15.055 8.18 14.4l1.561-.717.65 1.37Zm3.208 0 .65-1.37 1.57.716-2.22.654Z" fill="#233447"/>
<path d="m7.882 19.3.377-3.198-2.455.071L7.882 19.3Zm7.859-3.198.376 3.198 2.079-3.127-2.455-.07Zm1.867-3.52-4.408.197.408 2.276.65-1.37 1.57.716 1.78-1.82Zm-9.428 1.82 1.569-.718.643 1.37.416-2.275-4.416-.197 1.788 1.82Z" fill="#CD6116"/>
<path d="m6.392 12.582 1.85 3.623L8.18 14.4l-1.788-1.82Zm9.435 1.82-.078 1.803 1.859-3.623-1.78 1.82Zm-5.02-1.623-.415 2.276.518 2.686.117-3.537-.22-1.425Zm2.393 0-.212 1.417.094 3.545.526-2.686-.408-2.276Z" fill="#E4751F"/>
<path d="m13.608 15.055-.526 2.686.377.26 2.29-1.796.078-1.804-2.22.654ZM8.18 14.4l.063 1.804L10.533 18l.377-.26-.518-2.685L8.18 14.4Z" fill="#F6851B"/>
<path d="m13.647 20.474.023-.733-.196-.173h-2.957l-.18.173.016.733-2.47-1.174.862.709 1.749 1.22h3.004l1.757-1.22.862-.709-2.47 1.174Z" fill="#C0AD9E"/>
<path d="m13.459 18-.377-.26H10.91l-.377.26-.196 1.741.18-.173h2.957l.196.173-.211-1.74Z" fill="#161616"/>
<path d="M21.333 8.81 22 5.595l-.996-2.985-7.545 5.623L16.36 10.7l4.102 1.206.91-1.064-.392-.283.628-.575-.487-.378.628-.48-.416-.316ZM2 5.595l.666 3.213-.423.315.627.48-.478.379.627.575-.392.283.902 1.064 4.102-1.206 2.902-2.465L2.988 2.61 2 5.596Z" fill="#763D16"/>
<path d="M20.462 11.904 16.361 10.7l1.247 1.883-1.86 3.623 2.448-.032h3.647l-1.38-4.269ZM7.632 10.7l-4.103 1.205-1.365 4.27h3.64l2.439.03-1.851-3.622 1.24-1.883Zm5.568 2.08.259-4.545 1.192-3.237H9.357l1.176 3.237.275 4.545.094 1.433.008 3.529h2.172l.016-3.529.102-1.433Z" fill="#F6851B"/>
</svg>
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M15.938 13.125h-2.5a.312.312 0 0 0-.313.313v2.5c0 .172.14.312.313.312h2.5c.172 0 .312-.14.312-.313v-2.5a.312.312 0 0 0-.313-.312Zm-3.125-2.5h-1.876a.312.312 0 0 0-.312.313v1.874c0 .173.14.313.313.313h1.874c.173 0 .313-.14.313-.313v-1.874a.312.312 0 0 0-.313-.313Zm5.625 5.625h-1.875a.312.312 0 0 0-.313.313v1.875c0 .172.14.312.313.312h1.875c.172 0 .312-.14.312-.313v-1.875a.312.312 0 0 0-.313-.312Zm0-5.625h-1.25a.312.312 0 0 0-.313.313v1.25c0 .172.14.312.313.312h1.25c.172 0 .312-.14.312-.313v-1.25a.312.312 0 0 0-.313-.312Zm-6.25 6.25h-1.25a.312.312 0 0 0-.313.313v1.25c0 .172.14.312.313.312h1.25c.172 0 .312-.14.312-.313v-1.25a.312.312 0 0 0-.313-.312ZM17.5 1.25h-5.625a1.25 1.25 0 0 0-1.25 1.25v5.625a1.25 1.25 0 0 0 1.25 1.25H17.5a1.25 1.25 0 0 0 1.25-1.25V2.5a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.312.312 0 0 1-.313.312h-2.5a.313.313 0 0 1-.312-.313v-2.5a.312.312 0 0 1 .313-.312h2.5a.313.313 0 0 1 .312.313v2.5ZM8.125 1.25H2.5A1.25 1.25 0 0 0 1.25 2.5v5.625a1.25 1.25 0 0 0 1.25 1.25h5.625a1.25 1.25 0 0 0 1.25-1.25V2.5a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.312.312 0 0 1-.313.312h-2.5a.312.312 0 0 1-.312-.313v-2.5a.312.312 0 0 1 .313-.312h2.5a.312.312 0 0 1 .312.313v2.5Zm1.25 4.062H2.5a1.25 1.25 0 0 0-1.25 1.25V17.5a1.25 1.25 0 0 0 1.25 1.25h5.625a1.25 1.25 0 0 0 1.25-1.25v-5.625a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.313.313 0 0 1-.313.312h-2.5a.312.312 0 0 1-.312-.313v-2.5a.313.313 0 0 1 .313-.312h2.5a.312.312 0 0 1 .312.313v2.5Z"/>
</svg>
......@@ -13,7 +13,7 @@ interface Props extends ChakraProviderProps {
export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) :
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
localStorageManager;
return (
......
export default function getPlaceholderWithError(text: string, errorText?: string) {
return `${ text }${ errorText ? ' - ' + errorText : '' }`;
}
import _clamp from 'lodash/clamp';
import React from 'react';
const MAX_DELAY = 500;
const MIN_DELAY = 50;
export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] {
const [ num, setNum ] = React.useState(initialValue);
const queue = React.useRef<number>(0);
const timeoutId = React.useRef(0);
const incrementDelayed = React.useCallback(() => {
if (queue.current === 0) {
return;
}
queue.current--;
setNum(prev => prev + 1);
timeoutId.current = 0;
}, []);
const increment = React.useCallback((inc: number) => {
if (inc < 1) {
return;
}
queue.current += inc;
if (!timeoutId.current) {
timeoutId.current = window.setTimeout(incrementDelayed, 0);
}
}, [ incrementDelayed ]);
React.useEffect(() => {
if (queue.current > 0 && !timeoutId.current) {
const delay = _clamp(MAX_DELAY / queue.current * 1.5, MIN_DELAY, MAX_DELAY);
timeoutId.current = window.setTimeout(incrementDelayed, delay);
}
}, [ incrementDelayed, num ]);
React.useEffect(() => {
return () => {
window.clearTimeout(timeoutId.current);
};
}, []);
return [ num, increment ];
}
......@@ -2,6 +2,7 @@ import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -36,19 +37,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_
export default function useNewTxsSocket() {
const router = useRouter();
const [ num, setNum ] = React.useState(0);
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const { topic, event } = getSocketParams(router);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) {
setNum((prev) => prev + response.transaction);
setNum(response.transaction);
}
if (assertIsNewPendingTxResponse(response)) {
setNum((prev) => prev + response.pending_transaction);
setNum(response.pending_transaction);
}
}, []);
}, [ setNum ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.');
......
......@@ -12,6 +12,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://explorer.anyblock.tools',
// paths: {
// tx: '/ethereum/ethereum/goerli/transaction',
// address: '/ethereum/ethereum/goerli/address'
// },
// },
// {
......@@ -19,6 +20,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://goerli.etherscan.io/',
// paths: {
// tx: '/tx',
// address: '/address',
// },
// },
// ]).replaceAll('"', '\'');
......
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params: PageParams) {
const networkTitle = getNetworkTitle();
return {
title: params ? `${ params.id } - ${ networkTitle }` : '',
description: params ?
`View the account balance, transactions, and other data for ${ params.id } on the ${ networkTitle }` :
'',
};
}
export type PageParams = {
id: string;
}
......@@ -6,7 +6,7 @@ export const SocketContext = React.createContext<Socket | null>(null);
interface SocketProviderProps {
children: React.ReactNode;
url: string;
url?: string;
options?: Partial<SocketConnectOption>;
}
......@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options);
socketInstance.connect();
setSocket(socketInstance);
......
export const base = {
chart_data: [
{
date: '2022-11-28',
tx_count: 26815,
},
{
date: '2022-11-27',
tx_count: 34784,
},
{
date: '2022-11-26',
tx_count: 77527,
},
{
date: '2022-11-25',
tx_count: 39687,
},
{
date: '2022-11-24',
tx_count: 40752,
},
{
date: '2022-11-23',
tx_count: 32569,
},
{
date: '2022-11-22',
tx_count: 34449,
},
{
date: '2022-11-21',
tx_count: 106047,
},
{
date: '2022-11-20',
tx_count: 107713,
},
{
date: '2022-11-19',
tx_count: 96311,
},
{
date: '2022-11-18',
tx_count: 30828,
},
{
date: '2022-11-17',
tx_count: 27422,
},
{
date: '2022-11-16',
tx_count: 75898,
},
{
date: '2022-11-15',
tx_count: 84084,
},
{
date: '2022-11-14',
tx_count: 62266,
},
{
date: '2022-11-13',
tx_count: 22338,
},
{
date: '2022-11-12',
tx_count: 86764,
},
{
date: '2022-11-11',
tx_count: 79493,
},
{
date: '2022-11-10',
tx_count: 92887,
},
{
date: '2022-11-09',
tx_count: 43691,
},
{
date: '2022-11-08',
tx_count: 74197,
},
{
date: '2022-11-07',
tx_count: 58131,
},
{
date: '2022-11-06',
tx_count: 62477,
},
{
date: '2022-11-05',
tx_count: 82897,
},
{
date: '2022-11-04',
tx_count: 91725,
},
{
date: '2022-11-03',
tx_count: 83667,
},
{
date: '2022-11-02',
tx_count: 63743,
},
{
date: '2022-11-01',
tx_count: 152059,
},
{
date: '2022-10-31',
tx_count: 62519,
},
{
date: '2022-10-30',
tx_count: 48569,
},
{
date: '2022-10-29',
tx_count: 36789,
},
],
};
import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
gas_prices: {
average: 48.0,
fast: 67.5,
slow: 48.0,
},
gas_used_today: '4108680603',
market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064,
static_gas_price: '10',
total_addresses: '19667249',
total_blocks: '30215608',
total_gas_used: '0',
total_transactions: '82258122',
transactions_today: '26815',
};
......@@ -61,7 +61,6 @@ export const base: Transaction = {
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
......@@ -80,6 +79,9 @@ export const withContractCreation: Transaction = {
public_tags: [],
watchlist_names: [],
},
tx_types: [
'contract_creation',
],
};
export const withTokenTransfer: Transaction = {
......@@ -100,6 +102,9 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
tx_types: [
'token_transfer',
],
};
export const withDecodedRevertReason: Transaction = {
......
......@@ -4,16 +4,21 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import appConfig from 'configs/app/config';
import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const directionContext = useScrollDirection();
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
......@@ -57,7 +62,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<Component { ...pageProps }/>
<ScrollDirectionContext.Provider value={ directionContext }>
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<Component { ...pageProps }/>
</SocketProvider>
</ScrollDirectionContext.Provider>
<ReactQueryDevtools/>
</QueryClientProvider>
</AppContextProvider>
......
......@@ -10,11 +10,11 @@ class MyDocument extends Document {
<Html lang="en">
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap"
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address';
const AddressPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Address/>
</>
);
};
export default AddressPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/counters`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/token-balances`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
......@@ -88,6 +88,14 @@ const config: PlaywrightTestConfig = {
colorScheme: 'dark',
},
},
{
name: 'dark color mode mobile',
grep: /\+@dark-mode-mobile/,
use: {
...devices['iPhone 13 Pro'],
colorScheme: 'dark',
},
},
],
};
......
......@@ -2,13 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme';
type Props = {
children: React.ReactNode;
withSocket?: boolean;
}
const TestApp = ({ children }: Props) => {
const TestApp = ({ children, withSocket }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
......@@ -21,7 +24,9 @@ const TestApp = ({ children }: Props) => {
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
{ children }
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children }
</SocketProvider>
</QueryClientProvider>
</ChakraProvider>
);
......
import type { TestFixture } from '@playwright/test';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ArgsType = any;
type Channel = [string, string, string];
export interface SocketServerFixture {
createSocket: ReturnType;
}
export const PORT = 3200;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<ReturnType, ArgsType> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: PORT });
const connectionPromise = new Promise<WebSocket>((resolve) => {
socketServer.on('connection', (socket: WebSocket) => {
resolve(socket);
});
});
await use(() => connectionPromise);
socketServer.close();
};
export const joinChannel = async(socket: WebSocket, channelName: string) => {
return new Promise<[string, string, string]>((resolve, reject) => {
socket.on('message', (msg) => {
try {
const payload: Array<string> = JSON.parse(msg.toString());
if (channelName === payload[2] && payload[3] === 'phx_join') {
socket.send(JSON.stringify([
payload[0],
payload[1],
payload[2],
'phx_reply',
{ response: {}, status: 'ok' },
]));
resolve([ payload[0], payload[1], payload[2] ]);
}
} catch (error) {
reject(error);
}
});
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
msg,
payload,
]));
}
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -3,17 +3,39 @@ import {
createMultiStyleConfigHelpers,
defineStyle,
} from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleControl = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
borderColor: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
_indeterminate: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
},
};
});
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
});
const baseStyle = definePartsStyle({
const baseStyle = definePartsStyle((props) => ({
label: baseStyleLabel,
});
control: runIfFn(baseStyleControl, props),
}));
const Checkbox = defineMultiStyleConfig({
baseStyle,
......
......@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
display: 'flex',
fontSize: 'md',
marginEnd: '3',
mb: '2',
......
......@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } =
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
width: 'fit-content',
});
const baseStyleContainer = defineStyle({
width: 'fit-content',
});
const baseStyle = definePartsStyle({
label: baseStyleLabel,
container: baseStyleContainer,
});
const Radio = defineMultiStyleConfig({
......
import { switchAnatomy as parts } from '@chakra-ui/anatomy';
import { defineStyle, createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleTrack = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
};
});
const baseStyle = definePartsStyle((props) => ({
track: baseStyleTrack(props),
}));
const Switch = defineMultiStyleConfig({
baseStyle,
});
export default Switch;
......@@ -36,8 +36,7 @@ const sizes = {
fontSize: 'sm',
},
td: {
px: 4,
py: 6,
p: 4,
},
}),
sm: definePartsStyle({
......@@ -48,7 +47,7 @@ const sizes = {
},
td: {
px: '10px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......@@ -61,7 +60,7 @@ const sizes = {
},
td: {
px: '6px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......
......@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
borderRadius: 'sm',
...transitionProps,
});
......
......@@ -13,6 +13,7 @@ import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Switch from './Switch';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -36,6 +37,7 @@ const components = {
Radio,
Skeleton,
Spinner,
Switch,
Tabs,
Table,
Tag,
......
......@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_disabled: {
opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none',
borderColor: 'transparent',
cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
},
_invalid: {
borderColor: getColor(theme, ec),
......
import type { AddressTag, WatchlistName } from './addressParams';
import type { TokenInfo } from './tokenInfo';
export interface Address {
block_number_balance_updated_at: number | null;
coin_balance: string | null;
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
hash: string;
implementation_address: string | null;
implementation_name: string | null;
is_contract: boolean;
is_verified: boolean;
name: string | null;
private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null;
tokenInfo: TokenInfo | null;
watchlist_names: Array<WatchlistName> | null;
}
export interface AddressCounters {
transaction_count: string;
token_transfer_count: string;
gas_usage_count: string;
validation_count: string | null;
}
export interface AddressTokenBalance {
token: TokenInfo;
token_id: string | null;
value: string;
}
export type Stats = {
export type HomeStats = {
total_blocks: string;
total_addresses: string;
total_transactions: string;
......@@ -12,3 +12,7 @@ export type Stats = {
market_cap: string;
network_utilization_percentage: number;
}
export type Stats = {
total_blocks: string;
}
......@@ -3,6 +3,7 @@ export enum QueryKeys {
profile = 'profile',
txsValidate = 'txs-validated',
txsPending = 'txs-pending',
homeStats='homeStats',
stats='stats',
tx = 'tx',
txInternals = 'tx-internals',
......@@ -16,5 +17,8 @@ export enum QueryKeys {
chartsMarket = 'charts-market',
indexBlocks='indexBlocks',
indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url'
jsonRpcUrl='json-rpc-url',
address='address',
addressCounters='address-counters',
addressTokenBalances='address-token-balances',
}
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
......@@ -5,9 +7,9 @@ export enum StatsSectionId {
'transactions',
'gas',
}
export type StatsSectionIds = keyof typeof StatsSectionId;
export type StatsSection = { id: StatsSectionIds; value: string }
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId {
'all',
'oneMonth',
......@@ -15,5 +17,11 @@ export enum StatsIntervalId {
'sixMonths',
'oneYear',
}
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export type StatsInterval = { id: StatsIntervalIds; value: string }
export type StatsChart = {
visible?: boolean;
id: string;
title: string;
description: string;
apiMethodURL: string;
}
......@@ -17,7 +17,8 @@ export interface NetworkExplorer {
title: string;
baseUrl: string;
paths: {
tx: string;
tx?: string;
address?: string;
};
}
......
import { Box, Flex, Text, Icon, Button, Grid, Select } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address as TAddress, AddressCounters, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import metamaskIcon from 'icons/metamask.svg';
import qrCodeIcon from 'icons/qr_code.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
interface Props {
addressQuery: UseQueryResult<TAddress>;
}
const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const countersQuery = useQuery<unknown, unknown, AddressCounters>(
[ QueryKeys.addressCounters, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/counters`),
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
const tokenBalancesQuery = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/token-balances`),
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <Box>loading</Box>;
}
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) {
return <Box>error</Box>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
return (
<Box>
<Flex alignItems="center">
<AddressIcon hash={ addressQuery.data.hash }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
</Text>
<CopyToClipboard text={ addressQuery.data.hash }/>
<Icon as={ metamaskIcon } boxSize={ 6 } ml={ 2 }/>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ starOutlineIcon } boxSize={ 5 }/>
</Button>
<Button variant="outline" size="sm" ml={ 2 }>
<Icon as={ qrCodeIcon } boxSize={ 5 }/>
</Button>
</Flex>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text>Verify with other explorers</Text>
{ explorers.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
}) }
</Flex>
) }
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
>
{ tokenBalancesQuery.data.length > 0 ? (
<>
{ /* TODO will be fixed later when we implement select with custom menu */ }
<Select
size="sm"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
>
{ tokenBalancesQuery.data.map((token) =>
<option key={ token.token.address } value={ token.token.address }>{ token.token.symbol }</option>) }
</Select>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ walletIcon } boxSize={ 5 }/>
</Button>
</>
) : (
'-'
) }
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
{ Number(countersQuery.data.transaction_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfer_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
>
{ BigNumber(countersQuery.data.gas_usage_count).toFormat() }
</DetailsInfoItem>
{ countersQuery.data.validation_count && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
{ Number(countersQuery.data.validation_count).toLocaleString() }
</DetailsInfoItem>
) }
</Grid>
</Box>
);
};
export default React.memo(AddressDetails);
......@@ -15,9 +15,9 @@ import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: ApiKey;
......@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
token: data?.api_key || '',
......@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>
{ getPlaceholderWithError('Application name for API key (e.g Web3 project)', errors.name?.message) }
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name?.message }/>
</FormLabel>
</FormControl>
);
......@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text> API key for <Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
<Text> API key for <Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
);
}, [ data.name ]);
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent';
const API_URL = '/node-api/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
......@@ -19,8 +28,52 @@ test('base view +@mobile', async({ mount, page }) => {
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('new item from socket', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
test('socket error', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL),
const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close();
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,6 @@ import {
Box,
Button,
FormControl,
FormLabel,
Input,
Textarea,
useColorModeValue,
......@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: CustomAbi;
......@@ -37,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isValid }, handleSubmit, setError } = useForm<Inputs>({
const { control, formState: { errors, isValid, isDirty }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Project name', errors.name?.message) }</FormLabel>
<InputPlaceholder text="Project name" error={ errors.name?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px"
isInvalid={ Boolean(errors.abi) }
/>
<FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
......@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
return (
<Text>Custom ABI for<Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
<Text>Custom ABI for<Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
);
}, [ data.name ]);
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/stats';
const BLOCKS_API_URL = '/node-api/index/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
test('new item', async({ mount, page, createSocket }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestBlocks/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
});
......@@ -5,7 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
......@@ -31,8 +31,8 @@ const LatestBlocks = () => {
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
......@@ -109,10 +109,10 @@ const LatestBlocks = () => {
}
return (
<>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading>
<Box width={{ base: '100%', lg: '280px' }}>
<Heading as="h4" size="sm" mb={ 4 }>Latest Blocks</Heading>
{ content }
</>
</Box>
);
};
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import LatestTxs from './LatestTxs';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp>
<LatestTxs/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
const hooksConfig = {
router: {
pathname: ROUTES.network_index.pattern,
query: {},
},
};
test('new item', async({ mount, page, createSocket }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
});
......@@ -27,7 +27,7 @@ const LatestTransactions = () => {
if (isLoading) {
content = (
<>
<Skeleton h="56px" w="100%"/>
<Skeleton h="56px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</>
);
......@@ -53,10 +53,10 @@ const LatestTransactions = () => {
}
return (
<>
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</>
</Box>
);
};
......
import { Alert, Spinner, Text, Link, useColorModeValue, useTheme } from '@chakra-ui/react';
import { Alert, Text, Link, useColorModeValue, useTheme } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react';
......@@ -18,18 +18,14 @@ const LatestTxsNotice = ({ className }: Props) => {
content = 'Connection is lost. Please reload page';
} else if (!num) {
content = (
<>
<Spinner size="sm" mr={ 3 }/>
<Text>scanning new transactions ...</Text>
</>
<Text>scanning new transactions...</Text>
);
} else {
const txsUrl = link('txs');
content = (
<>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link href={ txsUrl }>View all</Link>
<Link href={ txsUrl }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</>
);
}
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import Stats from './Stats';
const API_URL = '/node-api/stats';
test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Stats/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
......@@ -26,8 +26,8 @@ let itemsCount = 5;
const Stats = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
async() => await fetch(`/node-api/stats`),
);
......@@ -79,10 +79,10 @@ const Stats = () => {
return (
<Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: 'none' }}
gridTemplateRows={{ lg: 'none', base: `repeat(${ itemsCount }, 1fr)` }}
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: undefined }}
gridGap="10px"
marginTop="32px"
marginTop="24px"
>
{ content }
</Grid>
......
import { Flex, Icon, Center, Text, LightMode } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
......@@ -9,30 +9,21 @@ type Props = {
const StatsItem = ({ icon, title, value }: Props) => {
return (
<LightMode>
<Flex
backgroundColor="blue.50"
padding={ 5 }
borderRadius="16px"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
>
<Center
backgroundColor="green.100"
borderRadius="12px"
w={ 10 }
h={ 10 }
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
>
<Icon as={ icon } boxSize={ 7 } color="black"/>
</Center>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md">{ value }</Text>
</Flex>
<Flex
backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Icon as={ icon } boxSize={ 7 }/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md">{ value }</Text>
</Flex>
</LightMode>
</Flex>
);
};
......
......@@ -7,16 +7,16 @@ const StatsItemSkeleton = () => {
return (
<Flex
backgroundColor={ bgColor }
padding={ 5 }
borderRadius="16px"
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Skeleton
w="40px"
h="40px"
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
......
......@@ -3,24 +3,27 @@ import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { ChainIndicatorId } from './types';
import type { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import useIsMobile from 'lib/hooks/useIsMobile';
interface Props {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
value: (stats: HomeStats) => string;
icon: React.ReactNode;
isSelected: boolean;
onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<Stats>;
stats: UseQueryResult<HomeStats>;
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
......@@ -58,11 +61,11 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
borderRadius="md"
cursor="pointer"
onClick={ handleClick }
bgColor={ isSelected ? bgColor : 'inherit' }
bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
_hover={{
bgColor,
activeBgColor,
zIndex: 1,
}}
>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/stats';
const TX_CHART_API_URL = '/node-api/stats/charts/transactions';
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(TX_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<ChainIndicators/>
</TestApp>,
);
await page.waitForResponse(STATS_API_URL),
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } });
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,7 @@ import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@ch
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
......@@ -35,13 +35,15 @@ const ChainIndicators = () => {
const queryResult = useFetchChartData(indicator);
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'gray.900');
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) {
return null;
......@@ -68,7 +70,7 @@ const ChainIndicators = () => {
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={ bgColor }
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 12 }
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
......@@ -97,7 +99,7 @@ const ChainIndicators = () => {
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
......
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries';
import type { TimeChartData } from 'ui/shared/chart/types';
......@@ -10,7 +10,7 @@ export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
value: (stats: HomeStats) => string;
icon: React.ReactNode;
hint?: string;
api: {
......
import { Flex, Tag } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import AddressDetails from 'ui/address/AddressDetails';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AddressPageContent = () => {
const router = useRouter();
const fetch = useFetch();
const addressQuery = useQuery<unknown, unknown, Address>(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const tags = [
...(addressQuery.data?.private_tags || []),
...(addressQuery.data?.public_tags || []),
...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
return (
<Page>
<Flex alignItems="center" columnGap={ 3 }>
<PageTitle text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }/>
{ tags.length > 0 && (
<Flex mb={ 6 } columnGap={ 2 }>
{ tags }
</Flex>
) }
</Flex>
<AddressDetails addressQuery={ addressQuery }/>
</Page>
);
};
export default AddressPageContent;
......@@ -57,7 +57,7 @@ const ApiKeysPage: React.FC = () => {
const description = (
<AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="https://docs.blockscout.com/for-users/api#api-keys">“How to use a Blockscout API key”</Link>.
<Link href="https://docs.blockscout.com/for-users/api#api-keys" target="_blank">“How to use a Blockscout API key”</Link>.
</AccountPageDescription>
);
......@@ -107,7 +107,12 @@ const ApiKeysPage: React.FC = () => {
<>
{ description }
{ Boolean(data.length) && list }
<Stack marginTop={ 8 } spacing={ 5 } direction={{ base: 'column', lg: 'row' }}>
<Stack
marginTop={ 8 }
spacing={ 5 }
direction={{ base: 'column', lg: 'row' }}
align={{ base: 'start', lg: 'center' }}
>
<Button
size="lg"
onClick={ apiKeyModalProps.onOpen }
......
......@@ -24,7 +24,7 @@ const BlockPageContent = () => {
return (
<Page>
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page>
);
};
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/blocks', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
await page.route('/node-api/stats/charts/transactions', (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<Home/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, LightMode } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestTxs from 'ui/home/LatestTxs';
import Stats from 'ui/home/Stats';
import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
const Home = () => {
return (
<Page hasSearch={ false }>
<Page isHomePage>
<Box
w="100%"
backgroundImage="radial-gradient(farthest-corner at 0 0, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)"
backgroundImage={ appConfig.homepage.plateGradient }
backgroundColor="blue.400"
borderRadius="24px"
padding={{ base: '24px 40px', lg: '48px' }}
padding={{ base: '24px', lg: '48px' }}
minW={{ base: 'unset', lg: '900px' }}
>
<Heading
as="h1"
size={{ base: 'lg', ld: 'xl' }}
fontWeight={{ base: 600, lg: 500 }}
color="white"
mb={{ base: 6, lg: 8 }}
>
Welcome to Blockscout explorer
</Heading>
<LightMode><SearchBar isHomepage/></LightMode>
<Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between">
<Heading
as="h1"
size={{ base: 'md', lg: 'xl' }}
lineHeight={{ base: '32px', lg: '50px' }}
fontWeight={ 500 }
color="white"
>
Welcome to Blockscout explorer
</Heading>
<Flex
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
</Flex>
</Flex>
<LightMode>
<SearchBar isHomepage/>
</LightMode>
</Box>
<Stats/>
<ChainIndicators/>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }}>
<Box mr={{ base: 0, lg: 12 }} mb={{ base: 8, lg: 0 }} width={{ base: '100%', lg: '280px' }}><LatestBlocks/></Box>
<Box flexGrow={ 1 }><LatestTxs/></Box>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/>
<LatestTxs/>
</Flex>
</Page>
);
......
......@@ -23,7 +23,7 @@ const MyProfile = () => {
}
return (
<VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch">
<VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
......
......@@ -4,19 +4,42 @@ import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import ChartsWidgetsList from '../stats/ChartsWidgetsList';
import NumberWidgetsList from '../stats/NumberWidgetsList';
import StatsFilters from '../stats/StatsFilters';
import WidgetsList from '../stats/WidgetsList';
import useStats from '../stats/useStats';
const Stats = () => {
const {
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
} = useStats();
return (
<Page>
<PageTitle text="Ethereum Stats"/>
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters/>
<NumberWidgetsList/>
</Box>
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters
section={ section }
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts }
/>
</Box>
<WidgetsList/>
<ChartsWidgetsList
charts={ displayedCharts }
/>
</Page>
);
};
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment