mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
@@ -24,6 +24,7 @@ export { default as getGasButton } from './getGasButton';
|
||||
export { default as googleAnalytics } from './googleAnalytics';
|
||||
export { default as growthBook } from './growthBook';
|
||||
export { default as marketplace } from './marketplace';
|
||||
export { default as megaEth } from './megaEth';
|
||||
export { default as metasuites } from './metasuites';
|
||||
export { default as mixpanel } from './mixpanel';
|
||||
export { default as mudFramework } from './mudFramework';
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'MegaETH chain';
|
||||
|
||||
const config: Feature<{ socketUrl: { metrics: string } }> = (() => {
|
||||
const socketUrlMetrics = getEnvValue('NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS');
|
||||
|
||||
if (socketUrlMetrics) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
socketUrl: {
|
||||
metrics: socketUrlMetrics,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
+28
-55
@@ -1,5 +1,5 @@
|
||||
# Set of ENVs for Ethereum network explorer
|
||||
# https://eth.blockscout.com
|
||||
# Set of ENVs for MEGA Testnet network explorer
|
||||
# https://megaeth-testnet.blockscout.com
|
||||
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=mega_eth"
|
||||
|
||||
# Local ENVs
|
||||
@@ -9,70 +9,43 @@ NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://user-ops-indexer-base-mainnet.k8s-prod-2.blockscout.com
|
||||
|
||||
## CUSTOM COLOR THEME
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(223,217,217,1)','transparent'],'text_color':['rgba(16,17,18,0.80)','rgba(222,217,217,1)'],'border':['none','1px solid rgba(105,103,103,1)'],'search':{'border_width':['0px','2px']}}
|
||||
NEXT_PUBLIC_COLOR_THEME_OVERRIDES={'bg':{'primary':{'_light':{'value':'rgba(254,253,253)'},'_dark':{'value':'rgba(25,25,26)'}}},'text':{'primary':{'_light':{'value':'rgba(16,17,18,0.80)'},'_dark':{'value':'rgba(222,217,217)'}},'secondary':{'_light':{'value':'rgba(138,136,136)'},'_dark':{'value':'rgba(133,133,133)'}}},'hover':{'_light':{'value':'rgba(104,200,158)'},'_dark':{'value':'rgba(104,200,158)'}},'selected':{'control':{'text':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(247,250,252)'}},'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'option':{'bg':{'_light':{'value':'rgba(84,75,75)'},'_dark':{'value':'rgba(87,87,87)'}}}},'icon':{'primary':{'_light':{'value':'rgba(138,136,136)'},'_dark':{'value':'rgba(133,133,133)'}},'secondary':{'_light':{'value':'rgba(176,176,176)'},'_dark':{'value':'rgba(105,103,103)'}}},'button':{'primary':{'_light':{'value':'rgba(105,103,103)'},'_dark':{'value':'rgba(133,133,133)'}}},'link':{'primary':{'_light':{'value':'rgba(57,146,108)'},'_dark':{'value':'rgba(57,146,108)'}}},'graph':{'line':{'_light':{'value':'rgba(105,103,103)'},'_dark':{'value':'rgba(57,146,108)'}},'gradient':{'start':{'_light':{'value':'rgba(105,103,103,0.3)'},'_dark':{'value':'rgba(57,146,108,0.3)'}},'stop':{'_light':{'value':'rgba(105,103,103,0)'},'_dark':{'value':'rgba(57,146,108,0)'}}}},'stats':{'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'topbar':{'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'navigation':{'text':{'selected':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(247,250,252)'}}},'bg':{'selected':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}}},'tabs':{'text':{'primary':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(222,217,217)'}}}}}
|
||||
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dark
|
||||
NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS=wss://testnet-dashboard.megaeth.com/metrics
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'humanpassport', 'trustblock', 'smartmuv', 'blockscoutbadges', 'etherscore', 'gitpoap', 'drops', 'humanode', 'deepdao']
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
|
||||
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=eth.blockscout.com
|
||||
NEXT_PUBLIC_API_HOST=megaeth-testnet.blockscout.com
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_COLOR_THEME_DEFAULT=dark
|
||||
NEXT_PUBLIC_COLOR_THEME_OVERRIDES={'bg':{'primary':{'_light':{'value':'rgba(254,253,253)'},'_dark':{'value':'rgba(25,25,26)'}}},'text':{'primary':{'_light':{'value':'rgba(16,17,18,0.80)'},'_dark':{'value':'rgba(222,217,217)'}},'secondary':{'_light':{'value':'rgba(138,136,136)'},'_dark':{'value':'rgba(133,133,133)'}}},'hover':{'_light':{'value':'rgba(104,200,158)'},'_dark':{'value':'rgba(104,200,158)'}},'selected':{'control':{'text':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(247,250,252)'}},'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'option':{'bg':{'_light':{'value':'rgba(84,75,75)'},'_dark':{'value':'rgba(87,87,87)'}}}},'icon':{'primary':{'_light':{'value':'rgba(138,136,136)'},'_dark':{'value':'rgba(133,133,133)'}},'secondary':{'_light':{'value':'rgba(176,176,176)'},'_dark':{'value':'rgba(105,103,103)'}}},'button':{'primary':{'_light':{'value':'rgba(105,103,103)'},'_dark':{'value':'rgba(133,133,133)'}}},'link':{'primary':{'_light':{'value':'rgba(57,146,108)'},'_dark':{'value':'rgba(57,146,108)'}}},'graph':{'line':{'_light':{'value':'rgba(105,103,103)'},'_dark':{'value':'rgba(57,146,108)'}},'gradient':{'start':{'_light':{'value':'rgba(105,103,103,0.3)'},'_dark':{'value':'rgba(57,146,108,0.3)'}},'stop':{'_light':{'value':'rgba(105,103,103,0)'},'_dark':{'value':'rgba(57,146,108,0)'}}}},'stats':{'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'topbar':{'bg':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}},'navigation':{'text':{'selected':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(247,250,252)'}}},'bg':{'selected':{'_light':{'value':'rgba(242,239,239)'},'_dark':{'value':'rgba(255,255,255,0.06)'}}}},'tabs':{'text':{'primary':{'_light':{'value':'rgba(25,25,26)'},'_dark':{'value':'rgba(222,217,217)'}}}}}
|
||||
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
|
||||
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth.json
|
||||
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/eth-mainnet.json
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
|
||||
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
|
||||
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
|
||||
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xd01175f1efa23f36c5579b3c13e2bbd0885017643a7efef5cbcb6b474384dfa8
|
||||
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
|
||||
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap']
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
|
||||
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Merit Badge <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=eth">here</a></p>
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/b27a4aecaad513fa033e37430a4f9a47/raw/3a2fa70068ea27c3e6d58dc4cdbeb732968d62f3/revokescout-banner.html
|
||||
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://eth.blockscout.com/apps/revokescout?chainId=1
|
||||
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
|
||||
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
|
||||
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
|
||||
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=1
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(223,217,217,1)','transparent'],'text_color':['rgba(16,17,18,0.80)','rgba(222,217,217,1)'],'border':['none','1px solid rgba(105,103,103,1)'],'search':{'border_width':['0px','2px']}}
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_METASUITES_ENABLED=true
|
||||
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}]
|
||||
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools']
|
||||
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/ethereum/token/price'}},{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/eth/pools'}},{'title':'Etherscan','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/etherscan.png','baseUrl':'https://etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}, {'title':'Blockchair','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/blockchair.png','baseUrl':'https://blockchair.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address','token':'/ethereum/erc-20/token','block':'/ethereum/block'}},{'title':'Sentio','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/sentio.png','baseUrl':'https://app.sentio.xyz/','paths':{'tx':'/tx/1','address':'/contract/1'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/mainnet'}}, {'title':'0xPPL','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/0xPPL.png','baseUrl':'https://0xppl.com','paths':{'tx':'/Ethereum/tx','address':'/','token':'/c/Ethereum'}}, {'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/ethereum/transaction','address':'/ethereum/address'}}, {'title':'BlobScan','baseUrl':'https://blobscan.com','paths':{'blob':'/blob'}} ]
|
||||
NEXT_PUBLIC_NETWORK_ID=1
|
||||
NEXT_PUBLIC_NETWORK_NAME=Ethereum
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.eth.gateway.fm
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=Ethereum
|
||||
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
|
||||
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/mega-eth-light.svg
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/mega-eth-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_ID=6342
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/mega-eth-light.svg
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/mega-eth-dark.svg
|
||||
NEXT_PUBLIC_NETWORK_NAME=MEGA Testnet
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://carrot.megaeth.com/rpc
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=MEGA Testnet
|
||||
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/eth.jpg
|
||||
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://eth.drpc.org?ref=559183','text':'Public RPC'}]
|
||||
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://merits.blockscout.com
|
||||
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
|
||||
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/mega-eth.png
|
||||
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
|
||||
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://eth.blockscout.com
|
||||
NEXT_PUBLIC_STATS_API_HOST=https://megaeth-testnet.blockscout.com
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
|
||||
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'OpenSea','collection_url':'https://opensea.io/assets/ethereum/{hash}','instance_url':'https://opensea.io/assets/ethereum/{hash}/{id}','logo_url':'https://opensea.io/static/images/logos/opensea-logo.svg'},{'name':'Rarible','collection_url':'https://rarible.com/collection/{hash}/items','instance_url':'https://rarible.com/token/{hash}:{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/rarible.png'},{'name':'Blur','collection_url':'https://blur.io/eth/collection/{hash}','instance_url':'https://blur.io/eth/asset/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/blur.png'},{'name':'MagicEden','collection_url':'https://magiceden.io/collections/ethereum/{hash}','instance_url':'https://magiceden.io/item-details/ethereum/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/magiceden.png'}]
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','geas']
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'deepdao', 'humanpassport', 'trustblock', 'bankless']
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={'img_url': 'https://gist.githubusercontent.com/maxaleks/0a02a0f15d018674171dc03e13f85cdd/raw/7ef0a5a3f7ec48bc202634d7aa6dd6fd71c8d0c5/promo.svg', 'text': 'Try Multichain!', 'bg_color': {'light': 'rgb(250, 245, 255)', 'dark': 'rgb(68, 51, 122)'}, 'text_color': {'light': 'rgb(107, 70, 193)', 'dark': 'rgb(233, 216, 253)'}, 'link_url': 'https://www.blockscout.com?utm_source=1&utm_medium=side-menu-banner'}
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
@@ -373,6 +373,12 @@ const rollupSchema = yup
|
||||
}),
|
||||
});
|
||||
|
||||
const megaEthSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS: yup.string().test(urlTest),
|
||||
});
|
||||
|
||||
const apiDocsScheme = yup
|
||||
.object()
|
||||
.shape({
|
||||
@@ -1170,6 +1176,7 @@ const schema = yup
|
||||
.concat(apiDocsScheme)
|
||||
.concat(mixpanelSchema)
|
||||
.concat(tacSchema)
|
||||
.concat(megaEthSchema)
|
||||
.concat(address3rdPartyWidgetsConfigSchema)
|
||||
.concat(addressMetadataSchema)
|
||||
.concat(userOpsSchema)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS=wss://example.com
|
||||
@@ -156,6 +156,11 @@ module.exports = {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/uptime':
|
||||
if (!process.env.NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/validators':
|
||||
if (!process.env.NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE) {
|
||||
return null;
|
||||
|
||||
@@ -804,6 +804,16 @@ For blockchains that use the Celo platform. _Note_, that once the Celo mainnet b
|
||||
|
||||
|
||||
|
||||
### MegaETH chain
|
||||
|
||||
For blockchains that use the MegaETH platform.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS | `string` | Public WebSocket endpoint for streaming statistics data, used to display information on the uptime dashboard page. | - | - | `wss://testnet-dashboard.megaeth.com/metrics` | upcoming |
|
||||
|
||||
|
||||
|
||||
### Ton Application Chain (TAC)
|
||||
|
||||
For Ton Application Chains, this feature enables additional views, such as a list of cross-chain operations and a detailed page for a specific cross-chain operation, as well as extra fields on the transaction page.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.816 4.847a.884.884 0 0 1 .793.879v18.213h19.453c.472 0 .854.384.855.855l-.005.088a.856.856 0 0 1-.85.767H4.814l-.09.01a.885.885 0 0 1-.883-.885V5.726c0-.488.396-.884.884-.884l.09.005Zm19.217 1.101A2.191 2.191 0 0 1 26 8.127l-.01.224a2.193 2.193 0 0 1-1.957 1.956l-.223.01a1.43 1.43 0 0 1-.114-.004l-2.764 5.58c.286.369.46.83.46 1.335l-.01.224a2.193 2.193 0 0 1-1.956 1.955l-.224.012a2.191 2.191 0 0 1-2.19-2.19l.007-.164c.016-.216.063-.425.137-.62l-2.074-1.54c-.3.218-.658.363-1.047.405l-.237.013c-.239 0-.469-.04-.684-.11l-2.353 2.94c.14.288.221.61.221.953l-.01.223a2.193 2.193 0 0 1-1.956 1.956l-.224.01a2.192 2.192 0 0 1-2.179-1.966l-.01-.224a2.19 2.19 0 0 1 2.189-2.189l.154.006c.206.014.403.058.588.125l2.323-2.903a2.176 2.176 0 0 1-.24-.824l-.009-.187c0-1.21.98-2.19 2.19-2.19l.223.011a2.191 2.191 0 0 1 1.967 2.179l-.008.19a2.567 2.567 0 0 1-.024.176l2.338 1.736a2.19 2.19 0 0 1 .908-.196l.19.008c.037.003.073.01.11.014l2.702-5.452a2.178 2.178 0 0 1-.58-1.347l-.004-.135c0-1.21.98-2.19 2.19-2.19l.223.011ZM8.792 18.565a.54.54 0 1 0 .002 1.082.54.54 0 0 0-.002-1.082ZM19.094 16.7a.542.542 0 0 0-.432.53l.01.109c.052.246.27.431.531.431l.108-.01a.542.542 0 0 0 .432-.53l-.014-.124a.538.538 0 0 0-.159-.265l-.023-.01a.832.832 0 0 1-.16-.104.527.527 0 0 0-.184-.037l-.11.01Zm-5.296-4.106a.54.54 0 1 0 0 1.08.54.54 0 0 0 0-1.08ZM23.81 7.587a.54.54 0 1 0 0 1.081.54.54 0 0 0 0-1.081Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.063 14.904a.99.99 0 0 1 1.118.844 7.742 7.742 0 0 0 13.949 3.457.11.11 0 0 0-.089-.175h-1.907a.99.99 0 0 1 0-1.98h4.311a.99.99 0 0 1 .99.99v4.311a.99.99 0 0 1-1.98 0V21.04a.11.11 0 0 0-.193-.07 9.719 9.719 0 0 1-8.659 3.353A9.72 9.72 0 0 1 5.22 16.02a.99.99 0 0 1 .843-1.116Zm8.76-2.165a2.068 2.068 0 1 1 0 4.137 2.068 2.068 0 0 1 0-4.137Zm-.004 2.046a.03.03 0 0 0-.006.002.024.024 0 0 0-.006.004.028.028 0 0 0-.004.007.04.04 0 0 0-.002.006v.002H14.8l.045.002v-.002l-.001-.002-.001-.006a.022.022 0 0 0-.005-.007.024.024 0 0 0-.007-.004l-.002-.001-.003-.001h-.007ZM9.936 6.514a9.72 9.72 0 0 1 14.49 7.079.99.99 0 1 1-1.961.272 7.741 7.741 0 0 0-13.95-3.456.11.11 0 0 0 .09.174h1.907a.99.99 0 0 1 0 1.98H6.2a.99.99 0 0 1-.99-.99V7.262a.99.99 0 0 1 1.98 0v1.311a.11.11 0 0 0 .072.104.11.11 0 0 0 .122-.033 9.722 9.722 0 0 1 2.552-2.13Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 950 B |
@@ -256,6 +256,46 @@ export default function useNavItems(): ReturnType {
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
const statsNavItem = (() => {
|
||||
const uptimeItem = {
|
||||
text: 'Uptime',
|
||||
nextRoute: { pathname: '/uptime' as const },
|
||||
icon: 'refresh_menu',
|
||||
isActive: pathname.startsWith('/uptime'),
|
||||
};
|
||||
|
||||
if (config.features.stats.isEnabled && config.features.megaEth.isEnabled) {
|
||||
return {
|
||||
text: 'Charts & stats',
|
||||
icon: 'stats',
|
||||
isActive: pathname.startsWith('/stats') || pathname.startsWith('/uptime'),
|
||||
subItems: [
|
||||
{
|
||||
text: `${ config.chain.name } stats`,
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'graph',
|
||||
isActive: pathname.startsWith('/stats/'),
|
||||
},
|
||||
uptimeItem,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.features.stats.isEnabled) {
|
||||
if (config.features.megaEth.isEnabled) {
|
||||
return uptimeItem;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
text: 'Charts & stats',
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'stats',
|
||||
isActive: pathname.startsWith('/stats'),
|
||||
};
|
||||
})();
|
||||
|
||||
const apiNavItem: NavItem | null = config.features.apiDocs.isEnabled ? {
|
||||
text: 'API',
|
||||
nextRoute: { pathname: '/api-docs' as const },
|
||||
@@ -310,12 +350,7 @@ export default function useNavItems(): ReturnType {
|
||||
icon: 'apps',
|
||||
isActive: pathname.startsWith('/app'),
|
||||
} : null,
|
||||
config.features.stats.isEnabled ? {
|
||||
text: 'Charts & stats',
|
||||
nextRoute: { pathname: '/stats' as const },
|
||||
icon: 'stats',
|
||||
isActive: pathname.startsWith('/stats'),
|
||||
} : null,
|
||||
statsNavItem,
|
||||
apiNavItem,
|
||||
{
|
||||
text: 'Other',
|
||||
|
||||
@@ -26,6 +26,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/clusters/[name]': 'Regular page',
|
||||
'/stats': 'Root page',
|
||||
'/stats/[id]': 'Regular page',
|
||||
'/uptime': 'Root page',
|
||||
'/api-docs': 'Regular page',
|
||||
'/search-results': 'Regular page',
|
||||
'/auth/profile': 'Root page',
|
||||
|
||||
@@ -29,6 +29,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/clusters/[name]': '%cluster_name% cluster | %app_name%',
|
||||
'/stats': DEFAULT_TEMPLATE,
|
||||
'/stats/[id]': DEFAULT_TEMPLATE,
|
||||
'/uptime': DEFAULT_TEMPLATE,
|
||||
'/api-docs': DEFAULT_TEMPLATE,
|
||||
'/search-results': DEFAULT_TEMPLATE,
|
||||
'/auth/profile': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -25,6 +25,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/apps/[id]': '%network_name% marketplace app',
|
||||
'/stats': '%network_name% stats - %network_name% network insights',
|
||||
'/stats/[id]': '%network_name% stats - %id% chart',
|
||||
'/uptime': '%network_name% uptime',
|
||||
'/api-docs': '%network_name% API docs - %network_name% developer tools',
|
||||
'/search-results': '%network_name% search result for %q%',
|
||||
'/auth/profile': '%network_name% - my profile',
|
||||
|
||||
@@ -24,6 +24,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/clusters/[name]': 'Cluster details',
|
||||
'/stats': 'Stats',
|
||||
'/stats/[id]': 'Stats chart',
|
||||
'/uptime': 'Uptime',
|
||||
'/api-docs': 'REST API',
|
||||
'/search-results': 'Search results',
|
||||
'/auth/profile': 'Profile',
|
||||
|
||||
@@ -14,6 +14,7 @@ function generateCspPolicy() {
|
||||
descriptors.growthBook(),
|
||||
descriptors.helia(),
|
||||
descriptors.marketplace(),
|
||||
descriptors.megaEth(),
|
||||
descriptors.mixpanel(),
|
||||
descriptors.monaco(),
|
||||
descriptors.multichain(),
|
||||
|
||||
@@ -9,6 +9,7 @@ export { googleReCaptcha } from './googleReCaptcha';
|
||||
export { growthBook } from './growthBook';
|
||||
export { helia } from './helia';
|
||||
export { marketplace } from './marketplace';
|
||||
export { megaEth } from './megaEth';
|
||||
export { mixpanel } from './mixpanel';
|
||||
export { monaco } from './monaco';
|
||||
export { multichain } from './multichain';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type CspDev from 'csp-dev';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const feature = config.features.megaEth;
|
||||
|
||||
export function megaEth(): CspDev.DirectiveDescriptor {
|
||||
if (!feature.isEnabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'connect-src': [
|
||||
feature.socketUrl.metrics,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -315,3 +315,11 @@ export const notOpSuperchain: Guard = () => async() => {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const megaEth: Guard = () => async() => {
|
||||
if (!config.features.megaEth.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,6 +25,7 @@ export const login = factory([ guards.login ]);
|
||||
export const dev = factory([ guards.dev ]);
|
||||
export const publicTagsSubmit = factory([ guards.publicTagsSubmit ]);
|
||||
export const pools = factory([ guards.pools ]);
|
||||
export const megaEth = factory([ guards.megaEth ]);
|
||||
export const zetaChainCCTX = factory([ guards.zetaChainCCTX ]);
|
||||
|
||||
// ROLLUPS
|
||||
|
||||
Vendored
+1
@@ -85,6 +85,7 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/txn-withdrawals">
|
||||
| StaticRoute<"/txs">
|
||||
| DynamicRoute<"/txs/kettle/[hash]", { "hash": string }>
|
||||
| StaticRoute<"/uptime">
|
||||
| DynamicRoute<"/validators/[id]", { "id": string }>
|
||||
| StaticRoute<"/validators">
|
||||
| StaticRoute<"/verified-contracts">
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import Uptime from 'ui/megaEth/uptime/Uptime';
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/uptime">
|
||||
<Uptime/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { megaEth as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
Vendored
+2
@@ -83,6 +83,7 @@
|
||||
| "gear"
|
||||
| "globe-b"
|
||||
| "globe"
|
||||
| "graph"
|
||||
| "heart_filled"
|
||||
| "heart_outline"
|
||||
| "hexagon"
|
||||
@@ -136,6 +137,7 @@
|
||||
| "publictags_slim"
|
||||
| "publictags"
|
||||
| "qr_code"
|
||||
| "refresh_menu"
|
||||
| "refresh"
|
||||
| "repeat"
|
||||
| "restAPI"
|
||||
|
||||
@@ -16,7 +16,7 @@ const PRESETS = {
|
||||
garnet: 'https://explorer.garnetchain.com',
|
||||
gnosis: 'https://gnosis.blockscout.com',
|
||||
immutable: 'https://explorer.immutable.com',
|
||||
mega_eth: 'https://eth.blockscout.com', // FIXME: change host when instance is ready
|
||||
mega_eth: 'https://megaeth-testnet.blockscout.com',
|
||||
mekong: 'https://mekong.blockscout.com',
|
||||
neon_devnet: 'https://neon-devnet.blockscout.com',
|
||||
optimism: 'https://optimism.blockscout.com',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
export interface UptimeRealTimeData {
|
||||
end_block: number;
|
||||
instant_mgas_per_second: number;
|
||||
instant_mini_block_interval: number;
|
||||
instant_tps: number;
|
||||
latest_mini_block_id: number;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UptimeHistoryItem {
|
||||
value: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface UptimeHistoryFull {
|
||||
historical_gas_per_second_3h: Array<UptimeHistoryItem>;
|
||||
historical_gas_per_second_7d: Array<UptimeHistoryItem>;
|
||||
historical_gas_per_second_24h: Array<UptimeHistoryItem>;
|
||||
historical_tps_3h: Array<UptimeHistoryItem>;
|
||||
historical_tps_7d: Array<UptimeHistoryItem>;
|
||||
historical_tps_24h: Array<UptimeHistoryItem>;
|
||||
historical_mini_block_interval_3h: Array<UptimeHistoryItem>;
|
||||
historical_mini_block_interval_7d: Array<UptimeHistoryItem>;
|
||||
historical_mini_block_interval_24h: Array<UptimeHistoryItem>;
|
||||
}
|
||||
|
||||
export type UptimeSocketData = {
|
||||
realtime: UptimeRealTimeData;
|
||||
timestamp: number;
|
||||
type: 'realtime_metrics';
|
||||
} | {
|
||||
data: UptimeHistoryFull;
|
||||
type: 'history_full';
|
||||
} | {
|
||||
data: Partial<Record<keyof UptimeHistoryFull, UptimeHistoryItem>>;
|
||||
type: 'history_delta';
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { test, expect } from 'playwright/lib';
|
||||
import { port as socketPort } from 'playwright/utils/socket';
|
||||
import { MINUTE, SECOND } from 'toolkit/utils/consts';
|
||||
|
||||
import Uptime from './Uptime';
|
||||
|
||||
const now = new Date('2022-11-11T12:00:00Z').getTime();
|
||||
|
||||
const REALTIME_DATA = {
|
||||
realtime: {
|
||||
end_block: 14416819,
|
||||
instant_tps: 213,
|
||||
instant_mgas_per_second: 50.91,
|
||||
instant_mini_block_interval: 10.53,
|
||||
latest_mini_block_id: 1208627478,
|
||||
updated_at: '2025-08-19T10:04:53.167705977Z',
|
||||
},
|
||||
timestamp: 1755597893167,
|
||||
type: 'realtime_metrics',
|
||||
};
|
||||
|
||||
const HISTORY_FULL_DATA = {
|
||||
data: {
|
||||
historical_gas_per_second_3h: [
|
||||
{ timestamp: (now - 5 * MINUTE) / SECOND, value: 78093010 },
|
||||
{ timestamp: (now - 4 * MINUTE) / SECOND, value: 120212720 },
|
||||
{ timestamp: (now - 3 * MINUTE) / SECOND, value: 49049412 },
|
||||
{ timestamp: (now - 2 * MINUTE) / SECOND, value: 70900270 },
|
||||
{ timestamp: (now - MINUTE) / SECOND, value: 97225784 },
|
||||
],
|
||||
historical_gas_per_second_24h: [],
|
||||
historical_gas_per_second_7d: [],
|
||||
historical_tps_3h: [
|
||||
{ timestamp: (now - 5 * MINUTE) / SECOND, value: 196 },
|
||||
{ timestamp: (now - 4 * MINUTE) / SECOND, value: 203.2 },
|
||||
{ timestamp: (now - 3 * MINUTE) / SECOND, value: 197.6 },
|
||||
{ timestamp: (now - 2 * MINUTE) / SECOND, value: 215.4 },
|
||||
{ timestamp: (now - MINUTE) / SECOND, value: 218.2 },
|
||||
],
|
||||
historical_tps_24h: [],
|
||||
historical_tps_7d: [],
|
||||
historical_mini_block_interval_3h: [
|
||||
{ timestamp: (now - 5 * MINUTE) / SECOND, value: 10.537408 },
|
||||
{ timestamp: (now - 4 * MINUTE) / SECOND, value: 10.593221 },
|
||||
{ timestamp: (now - 3 * MINUTE) / SECOND, value: 10.537408 },
|
||||
{ timestamp: (now - 2 * MINUTE) / SECOND, value: 10.593221 },
|
||||
{ timestamp: (now - MINUTE) / SECOND, value: 10.570825 },
|
||||
],
|
||||
historical_mini_block_interval_24h: [],
|
||||
historical_mini_block_interval_7d: [],
|
||||
},
|
||||
type: 'history_full',
|
||||
};
|
||||
|
||||
const HISTORY_DELTA_DATA = {
|
||||
data: {
|
||||
historical_gas_per_second_3h: { timestamp: now / SECOND, value: 87762230 },
|
||||
historical_tps_3h: { timestamp: now / SECOND, value: 211.8 },
|
||||
historical_mini_block_interval_3h: { timestamp: now / SECOND, value: 10.593221 },
|
||||
},
|
||||
type: 'history_delta',
|
||||
};
|
||||
|
||||
const WEBSOCKET_URL = `ws://${ config.app.host }:${ socketPort }`;
|
||||
|
||||
test('default view', async({ render, createSocket, mockEnvs, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_MEGA_ETH_SOCKET_URL_METRICS', WEBSOCKET_URL ],
|
||||
]);
|
||||
const component = await render(<Uptime/>, undefined, { withSocket: true });
|
||||
const socket = await createSocket();
|
||||
socket.send(JSON.stringify(HISTORY_FULL_DATA));
|
||||
socket.send(JSON.stringify(REALTIME_DATA));
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-TPS-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-MGass-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Block_time_ms-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await component.locator('.ChartOverlay').first().hover({ position: { x: 400, y: 200 } });
|
||||
|
||||
socket.send(JSON.stringify(HISTORY_DELTA_DATA));
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
import UptimeCharts from './UptimeCharts';
|
||||
import UptimeStats from './UptimeStats';
|
||||
import UptimeStatus from './UptimeStatus';
|
||||
import useUptimeSocketData from './useUptimeSocketData';
|
||||
|
||||
const Uptime = () => {
|
||||
|
||||
const { realtimeData, historyData, status, onReconnect } = useUptimeSocketData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title="Uptime"
|
||||
contentAfter={ <UptimeStatus status={ status } onReconnect={ onReconnect }/> }
|
||||
/>
|
||||
<UptimeStats realtimeData={ realtimeData }/>
|
||||
<UptimeCharts historyData={ historyData }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Uptime);
|
||||
@@ -0,0 +1,220 @@
|
||||
import { Box, Flex, Grid, GridItem } from '@chakra-ui/react';
|
||||
import * as d3 from 'd3';
|
||||
import React from 'react';
|
||||
|
||||
import type { UptimeHistoryFull, UptimeHistoryItem } from 'types/api/megaEth';
|
||||
import type { AxesConfigFn } from 'ui/shared/chart/types';
|
||||
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { DAY, HOUR, SECOND } from 'toolkit/utils/consts';
|
||||
import ChartWidget from 'ui/shared/chart/ChartWidget';
|
||||
import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect';
|
||||
|
||||
const INTERVALS = [
|
||||
{ id: '3h', title: '3h' } as const,
|
||||
{ id: '24h', title: '24h' } as const,
|
||||
{ id: '7d', title: '7d' } as const,
|
||||
];
|
||||
|
||||
type IntervalId = (typeof INTERVALS)[number]['id'];
|
||||
|
||||
const TIME_FORMAT = '%e %b %Y, %H:%M:%S';
|
||||
|
||||
const AXES_CONFIG_BASE: AxesConfigFn = ({ isEnlarged, isMobile }) => ({
|
||||
y: {
|
||||
scale: { min: 0 },
|
||||
},
|
||||
x: {
|
||||
ticks: isEnlarged && !isMobile ? 8 : 5,
|
||||
},
|
||||
});
|
||||
|
||||
const AXES_CONFIG_LONG: AxesConfigFn = () => ({
|
||||
y: {
|
||||
scale: { min: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
const filterByInterval = (interval: IntervalId, now: number) => ({ date }: { date: Date }) => {
|
||||
switch (interval) {
|
||||
case '3h':
|
||||
return date.getTime() > now - 3 * HOUR;
|
||||
case '24h':
|
||||
return date.getTime() > now - 24 * HOUR;
|
||||
case '7d':
|
||||
return date.getTime() > now - 7 * DAY;
|
||||
}
|
||||
};
|
||||
|
||||
// Algorithm provided by MegaETH team
|
||||
const smoothData = (data: Array<UptimeHistoryItem>, windowSize: number): Array<UptimeHistoryItem> => {
|
||||
const result: Array<UptimeHistoryItem> = [];
|
||||
const halfWindow = Math.floor(windowSize / 2);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const windowStart = Math.max(0, i - halfWindow);
|
||||
const windowEnd = Math.min(data.length, i + halfWindow + 1);
|
||||
const windowValues = data.slice(windowStart, windowEnd);
|
||||
|
||||
const validValues = windowValues.filter((point) => point.value !== null);
|
||||
const average = validValues.length > 0 ?
|
||||
validValues.reduce((sum, point) => sum + (point.value || 0), 0) / validValues.length :
|
||||
null;
|
||||
|
||||
const value = average || data[i].value || 0;
|
||||
|
||||
result.push({ ...data[i], value });
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
historyData: UptimeHistoryFull | null;
|
||||
}
|
||||
|
||||
const UptimeCharts = ({ historyData }: Props) => {
|
||||
const [ interval, setInterval ] = React.useState<IntervalId>('3h');
|
||||
|
||||
const axesConfig = React.useMemo(() => {
|
||||
switch (interval) {
|
||||
case '3h':
|
||||
case '24h':
|
||||
return AXES_CONFIG_BASE;
|
||||
case '7d':
|
||||
return AXES_CONFIG_LONG;
|
||||
}
|
||||
}, [ interval ]);
|
||||
|
||||
const tpsItems = React.useMemo(() => {
|
||||
if (!historyData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (() => {
|
||||
switch (interval) {
|
||||
case '3h':
|
||||
return historyData.historical_tps_3h;
|
||||
case '7d':
|
||||
return historyData.historical_tps_7d;
|
||||
case '24h':
|
||||
return historyData.historical_tps_24h;
|
||||
}
|
||||
})();
|
||||
const now = Date.now();
|
||||
|
||||
const smoothedData = smoothData(data, 7);
|
||||
return smoothedData
|
||||
.map(({ value, timestamp }) => {
|
||||
const date = new Date(timestamp * SECOND);
|
||||
return { date, value: Number(value.toFixed(0)), dateLabel: d3.timeFormat(TIME_FORMAT)(date) };
|
||||
})
|
||||
.filter(filterByInterval(interval, now));
|
||||
}, [ historyData, interval ]);
|
||||
|
||||
const gasItems = React.useMemo(() => {
|
||||
if (!historyData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (() => {
|
||||
switch (interval) {
|
||||
case '3h':
|
||||
return historyData.historical_gas_per_second_3h;
|
||||
case '7d':
|
||||
return historyData.historical_gas_per_second_7d;
|
||||
case '24h':
|
||||
return historyData.historical_gas_per_second_24h;
|
||||
}
|
||||
})();
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const smoothedData = smoothData(data, 7);
|
||||
|
||||
return smoothedData
|
||||
.map(({ value, timestamp }) => {
|
||||
const date = new Date(timestamp * SECOND);
|
||||
return { date, value: Number((value / 1_000_000).toFixed(2)), dateLabel: d3.timeFormat(TIME_FORMAT)(date) };
|
||||
})
|
||||
.filter(filterByInterval(interval, now));
|
||||
}, [ historyData, interval ]);
|
||||
|
||||
const blockIntervalItems = React.useMemo(() => {
|
||||
if (!historyData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = (() => {
|
||||
switch (interval) {
|
||||
case '3h':
|
||||
return historyData.historical_mini_block_interval_3h;
|
||||
case '7d':
|
||||
return historyData.historical_mini_block_interval_7d;
|
||||
case '24h':
|
||||
return historyData.historical_mini_block_interval_24h;
|
||||
}
|
||||
})();
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
const smoothedData = smoothData(data, 7);
|
||||
|
||||
return smoothedData
|
||||
.map(({ value, timestamp }) => {
|
||||
const date = new Date(timestamp * SECOND);
|
||||
return { date, value: Number(value.toFixed(1)), dateLabel: d3.timeFormat(TIME_FORMAT)(date) };
|
||||
})
|
||||
.filter(filterByInterval(interval, now));
|
||||
}, [ historyData, interval ]);
|
||||
|
||||
const handleIntervalChange = React.useCallback((newInterval: IntervalId) => {
|
||||
setInterval(newInterval);
|
||||
}, [ setInterval ]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
mb={ 6 }
|
||||
>
|
||||
<Heading level="2">Real-time statistics</Heading>
|
||||
<TagGroupSelect<IntervalId> items={ INTERVALS } onChange={ handleIntervalChange } value={ interval } tagSize="lg"/>
|
||||
</Flex>
|
||||
<Grid
|
||||
templateColumns={{ base: '1fr', lg: 'repeat(2, 1fr)' }}
|
||||
gap={ 4 }
|
||||
>
|
||||
<GridItem colSpan={{ base: 1, lg: 2 }} minH={{ base: '220px', lg: '320px' }}>
|
||||
<ChartWidget
|
||||
title="TPS"
|
||||
items={ tpsItems }
|
||||
isLoading={ false }
|
||||
isError={ false }
|
||||
axesConfig={ axesConfig }
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem minH={{ base: '220px', lg: '320px' }}>
|
||||
<ChartWidget
|
||||
title="MGas/s"
|
||||
items={ gasItems }
|
||||
isLoading={ false }
|
||||
isError={ false }
|
||||
axesConfig={ axesConfig }
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem minH={{ base: '220px', lg: '320px' }}>
|
||||
<ChartWidget
|
||||
title="Block time (ms)"
|
||||
items={ blockIntervalItems }
|
||||
isLoading={ false }
|
||||
isError={ false }
|
||||
axesConfig={ axesConfig }
|
||||
/>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UptimeCharts);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { UptimeRealTimeData } from 'types/api/megaEth';
|
||||
|
||||
import StatsWidget from 'ui/shared/stats/StatsWidget';
|
||||
|
||||
interface Props {
|
||||
realtimeData: UptimeRealTimeData | null;
|
||||
}
|
||||
|
||||
const UptimeStats = ({ realtimeData }: Props) => {
|
||||
|
||||
return (
|
||||
<Box
|
||||
columnGap={ 2 }
|
||||
rowGap={ 2 }
|
||||
mb={ 8 }
|
||||
display="grid"
|
||||
gridTemplateColumns={{ base: '1fr', lg: 'repeat(4, 1fr)' }}
|
||||
>
|
||||
<StatsWidget
|
||||
label="Current TPS"
|
||||
hint="Number of transactions processed per second on the network"
|
||||
value={ realtimeData ? Number(realtimeData.instant_tps).toLocaleString() : '-' }
|
||||
/>
|
||||
<StatsWidget
|
||||
label="MGas/s"
|
||||
hint="Number of computational gas consumed per second on the network"
|
||||
value={ realtimeData ? Number(realtimeData.instant_mgas_per_second).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '-' }
|
||||
/>
|
||||
<StatsWidget
|
||||
label="Block height"
|
||||
hint="Number of blocks created since the genesis block"
|
||||
value={ realtimeData ? Number(realtimeData.latest_mini_block_id).toLocaleString() : '-' }
|
||||
/>
|
||||
<StatsWidget
|
||||
label="Block time"
|
||||
hint="Time taken by the sequencer to produce a new block"
|
||||
valuePostfix=" ms"
|
||||
value={ realtimeData ? Number(realtimeData.instant_mini_block_interval).toLocaleString(undefined, { maximumFractionDigits: 2 }) : '-' }
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UptimeStats);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { chakra, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { Button } from 'toolkit/chakra/button';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
import StatusTag from 'ui/shared/statusTag/StatusTag';
|
||||
|
||||
import type { Status } from './useUptimeSocketData';
|
||||
|
||||
interface Props {
|
||||
status: Status;
|
||||
onReconnect: () => void;
|
||||
}
|
||||
|
||||
const UptimeStatus = ({ status, onReconnect }: Props) => {
|
||||
|
||||
const statusTag = (() => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return <StatusTag type="ok" text="Connected"/>;
|
||||
case 'disconnected':
|
||||
case 'error':
|
||||
return <StatusTag type="error" text="Disconnected"/>;
|
||||
case 'initial':
|
||||
return <StatusTag type="pending" text="Initializing"/>;
|
||||
}
|
||||
|
||||
return null;
|
||||
})();
|
||||
|
||||
return (
|
||||
<HStack ml="auto" columnGap={ 3 }>
|
||||
{ statusTag }
|
||||
<Button variant="link" gap={ 1 } onClick={ onReconnect } disabled={ status === 'connected' }>
|
||||
<IconSvg name="refresh" boxSize={ 5 }/>
|
||||
<chakra.span hideBelow="lg" fontSize="sm">Refresh</chakra.span>
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(UptimeStatus);
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { UptimeHistoryFull, UptimeRealTimeData, UptimeSocketData } from 'types/api/megaEth';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { SECOND } from 'toolkit/utils/consts';
|
||||
|
||||
const megaEthFeature = config.features.megaEth;
|
||||
|
||||
export type Status = 'initial' | 'connected' | 'disconnected' | 'error';
|
||||
|
||||
export default function useUptimeSocketData() {
|
||||
const websocketRef = React.useRef<WebSocket | null>(null);
|
||||
const heartbeatRef = React.useRef<number | undefined>(undefined);
|
||||
const [ status, setStatus ] = React.useState<Status>('initial');
|
||||
const [ realtimeData, setRealtimeData ] = React.useState<UptimeRealTimeData | null>(null);
|
||||
const [ historyData, setHistoryData ] = React.useState<UptimeHistoryFull | null>(null);
|
||||
|
||||
const connect = React.useCallback(() => {
|
||||
if (!megaEthFeature.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
websocketRef.current = new WebSocket(megaEthFeature.socketUrl.metrics);
|
||||
|
||||
websocketRef.current.onmessage = async(event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as UptimeSocketData;
|
||||
switch (data.type) {
|
||||
case 'realtime_metrics':
|
||||
setRealtimeData(data.realtime);
|
||||
break;
|
||||
case 'history_full':
|
||||
setHistoryData(data.data);
|
||||
break;
|
||||
case 'history_delta':
|
||||
setHistoryData((prev) => {
|
||||
if (!prev) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.entries(data.data).reduce((acc, [ key, value ]) => {
|
||||
acc[key as keyof UptimeHistoryFull] = [ ...(acc[key as keyof UptimeHistoryFull] || []), value ];
|
||||
|
||||
return acc;
|
||||
}, { ...prev });
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
websocketRef.current.onopen = () => {
|
||||
setStatus('connected');
|
||||
|
||||
heartbeatRef.current = window.setInterval(() => {
|
||||
if (websocketRef.current?.readyState === WebSocket.OPEN) {
|
||||
websocketRef.current?.send('ping');
|
||||
}
|
||||
}, 10 * SECOND);
|
||||
};
|
||||
websocketRef.current.onerror = (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
setStatus('error');
|
||||
};
|
||||
websocketRef.current.onclose = (event) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('WebSocket closed', event);
|
||||
setStatus('disconnected');
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onReconnect = React.useCallback(() => {
|
||||
connect();
|
||||
}, [ connect ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
connect();
|
||||
|
||||
return () => {
|
||||
websocketRef.current?.close(4000, 'Component unmounted');
|
||||
window.clearInterval(heartbeatRef.current);
|
||||
};
|
||||
}, [ connect ]);
|
||||
|
||||
return { status, realtimeData, historyData, onReconnect };
|
||||
}
|
||||
@@ -38,7 +38,7 @@ test('base view +@dark-mode +@mobile', async({ render, mockApiResponse, page })
|
||||
const component = await render(<Chart/>, { hooksConfig });
|
||||
await page.waitForResponse(chartApiUrl);
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Charttitle-fullscreen"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Chart_title-fullscreen"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ test('base view +@dark-mode +@mobile', async({ render, mockApiResponse, mockEnvs
|
||||
const component = await render(<GasTracker/>);
|
||||
await page.waitForResponse(chartApiUrl);
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Averagegasprice-small"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Average_gas_price-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -55,11 +55,12 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
|
||||
const handleSelect = React.useCallback((x0: number, x1: number) => {
|
||||
const startDate = scale.invert(x0);
|
||||
const endDate = scale.invert(x1);
|
||||
const xStep = dayjs(data[0].items[1].date).diff(dayjs(data[0].items[0].date));
|
||||
|
||||
if (Math.abs(dayjs(startDate).diff(endDate, 'day')) > SELECTION_THRESHOLD) {
|
||||
if (Math.abs(dayjs(startDate).diff(endDate)) > SELECTION_THRESHOLD * xStep) {
|
||||
onSelect([ dayjs.min(dayjs(startDate), dayjs(endDate)).toDate(), dayjs.max(dayjs(startDate), dayjs(endDate)).toDate() ]);
|
||||
}
|
||||
}, [ onSelect, scale ]);
|
||||
}, [ onSelect, scale, data ]);
|
||||
|
||||
const cleanUp = React.useCallback(() => {
|
||||
isActive.current = false;
|
||||
@@ -92,19 +93,19 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
|
||||
const [ x ] = d3.pointer(event, anchorEl);
|
||||
isActive.current = true;
|
||||
startX.current = x;
|
||||
})
|
||||
}, { passive: true })
|
||||
.on('mousemove.selectionX', (event: MouseEvent) => {
|
||||
if (isActive.current) {
|
||||
const [ x ] = d3.pointer(event, anchorEl);
|
||||
startX.current && drawSelection(startX.current, x);
|
||||
endX.current = x;
|
||||
}
|
||||
})
|
||||
}, { passive: true })
|
||||
.on('mouseup.selectionX', handelMouseUp)
|
||||
.on('touchstart.selectionX', (event: TouchEvent) => {
|
||||
const pointers = d3.pointers(event, anchorEl);
|
||||
isActive.current = pointers.length === 2;
|
||||
})
|
||||
}, { passive: true })
|
||||
.on('touchmove.selectionX', (event: TouchEvent) => {
|
||||
if (isActive.current) {
|
||||
const pointers = d3.pointers(event, anchorEl);
|
||||
@@ -116,8 +117,8 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
|
||||
endX.current = pointers[1][0];
|
||||
}
|
||||
}
|
||||
})
|
||||
.on('touchend.selectionX', handelMouseUp);
|
||||
}, { passive: true })
|
||||
.on('touchend.selectionX', handelMouseUp, { passive: true });
|
||||
|
||||
d3.select('body').on('mouseup.selectionX', function(event) {
|
||||
const isOutside = startX.current !== undefined && event.target !== anchorD3.node();
|
||||
|
||||
@@ -125,22 +125,22 @@ const ChartTooltip = ({
|
||||
.on('touchmove.tooltip', (event: TouchEvent) => event.preventDefault()) // prevent scrolling
|
||||
.on(`touchstart.tooltip`, (event: TouchEvent) => {
|
||||
isMultiTouch = event.touches.length > 1;
|
||||
})
|
||||
}, { passive: true })
|
||||
.on(`touchend.tooltip`, (event: TouchEvent) => {
|
||||
if (isMultiTouch && event.touches.length === 0) {
|
||||
isMultiTouch = false;
|
||||
}
|
||||
})
|
||||
}, { passive: true })
|
||||
.on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => {
|
||||
if (!isMultiTouch) {
|
||||
trackerId.current = createPointerTracker(event);
|
||||
}
|
||||
})
|
||||
}, { passive: true })
|
||||
.on('pointermove.tooltip', (event: PointerEvent) => {
|
||||
if (event.pointerType === 'mouse' && !isMultiTouch && trackerId.current === undefined) {
|
||||
trackerId.current = createPointerTracker(event);
|
||||
}
|
||||
});
|
||||
}, { passive: true });
|
||||
|
||||
return () => {
|
||||
anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null);
|
||||
|
||||
@@ -35,7 +35,7 @@ test('base view +@dark-mode', async({ render, page }) => {
|
||||
const component = await render(<ChartWidget { ...props } href={{ pathname: '/stats/[id]', query: { id: 'test' } }}/>);
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Native_coin_circulating_supply-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
@@ -84,7 +84,7 @@ test('small values', async({ render, page }) => {
|
||||
|
||||
const component = await render(<ChartWidget { ...modifiedProps }/>);
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Native_coin_circulating_supply-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -109,7 +109,7 @@ test('small variations in big values', async({ render, page }) => {
|
||||
|
||||
const component = await render(<ChartWidget { ...modifiedProps }/>);
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Native_coin_circulating_supply-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -125,7 +125,7 @@ test('incomplete day', async({ render, page }) => {
|
||||
|
||||
const component = await render(<ChartWidget { ...modifiedProps }/>);
|
||||
await page.waitForFunction(() => {
|
||||
return document.querySelector('path[data-name="chart-Nativecoincirculatingsupply-small"]')?.getAttribute('opacity') === '1';
|
||||
return document.querySelector('path[data-name="chart-Native_coin_circulating_supply-small"]')?.getAttribute('opacity') === '1';
|
||||
});
|
||||
await expect(component).toHaveScreenshot();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { chakra, Flex } from '@chakra-ui/react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import type { TimeChartItem } from './types';
|
||||
import type { AxesConfigFn, TimeChartItem } from './types';
|
||||
|
||||
import { route, type Route } from 'nextjs-routes';
|
||||
|
||||
@@ -27,6 +27,7 @@ export type Props = {
|
||||
emptyText?: string;
|
||||
noAnimation?: boolean;
|
||||
href?: Route;
|
||||
axesConfig?: AxesConfigFn;
|
||||
};
|
||||
|
||||
const ChartWidget = ({
|
||||
@@ -40,6 +41,7 @@ const ChartWidget = ({
|
||||
emptyText,
|
||||
noAnimation,
|
||||
href,
|
||||
axesConfig,
|
||||
}: Props) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { zoomRange, handleZoom, handleZoomReset } = useZoom();
|
||||
@@ -57,6 +59,7 @@ const ChartWidget = ({
|
||||
handleZoom={ handleZoom }
|
||||
zoomRange={ zoomRange }
|
||||
noAnimation={ noAnimation }
|
||||
axesConfig={ axesConfig }
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Center, Flex, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TimeChartItem } from './types';
|
||||
import type { AxesConfigFn, TimeChartItem } from './types';
|
||||
import type { Resolution } from '@blockscout/stats-types';
|
||||
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
@@ -23,6 +23,7 @@ export type Props = {
|
||||
isEnlarged?: boolean;
|
||||
noAnimation?: boolean;
|
||||
resolution?: Resolution;
|
||||
axesConfig?: AxesConfigFn;
|
||||
};
|
||||
|
||||
const ChartWidgetContent = ({
|
||||
@@ -37,6 +38,7 @@ const ChartWidgetContent = ({
|
||||
isEnlarged,
|
||||
noAnimation,
|
||||
resolution,
|
||||
axesConfig,
|
||||
}: Props) => {
|
||||
const hasItems = items && items.length > 2;
|
||||
|
||||
@@ -83,6 +85,7 @@ const ChartWidgetContent = ({
|
||||
isEnlarged={ isEnlarged }
|
||||
noAnimation={ noAnimation }
|
||||
resolution={ resolution }
|
||||
axesConfig={ axesConfig }
|
||||
/>
|
||||
<ChartWatermarkIcon w="162px" h="15%"/>
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useToken } from '@chakra-ui/react';
|
||||
import * as d3 from 'd3';
|
||||
import { defaultsDeep } from 'es-toolkit/compat';
|
||||
import React from 'react';
|
||||
|
||||
import { Resolution } from '@blockscout/stats-types';
|
||||
import type { ChartMargin, TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
|
||||
import type { AxesConfigFn, ChartMargin, TimeChartData, TimeChartItem } from 'ui/shared/chart/types';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
@@ -26,6 +27,7 @@ interface Props {
|
||||
margin?: ChartMargin;
|
||||
noAnimation?: boolean;
|
||||
resolution?: Resolution;
|
||||
axesConfig?: AxesConfigFn;
|
||||
}
|
||||
|
||||
const DEFAULT_CHART_MARGIN = { bottom: 20, left: 10, right: 20, top: 10 };
|
||||
@@ -40,10 +42,11 @@ const ChartWidgetGraph = ({
|
||||
noAnimation,
|
||||
resolution,
|
||||
zoomRange,
|
||||
axesConfig: axesConfigProps,
|
||||
}: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [ color ] = useToken('colors', useColorModeValue('theme.graph.line._light', 'theme.graph.line._dark'));
|
||||
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
|
||||
const chartId = `chart-${ title.replace(/[^\w\s]/g, '').split(' ').join('_') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
|
||||
|
||||
const overlayRef = React.useRef<SVGRectElement>(null);
|
||||
|
||||
@@ -53,8 +56,8 @@ const ChartWidgetGraph = ({
|
||||
items
|
||||
.filter((item) => item.date >= range[0] && item.date <= range[1])
|
||||
.map((item) => ({
|
||||
...item,
|
||||
dateLabel: getDateLabel(item.date, item.date_to, resolution),
|
||||
...item,
|
||||
})),
|
||||
[ items, range, resolution ]);
|
||||
|
||||
@@ -62,7 +65,7 @@ const ChartWidgetGraph = ({
|
||||
|
||||
const margin: ChartMargin = React.useMemo(() => ({ ...DEFAULT_CHART_MARGIN, ...marginProps }), [ marginProps ]);
|
||||
const axesConfig = React.useMemo(() => {
|
||||
return {
|
||||
return defaultsDeep(axesConfigProps?.({ isEnlarged, isMobile }), {
|
||||
x: {
|
||||
ticks: isEnlarged && !isMobile ? 8 : 4,
|
||||
},
|
||||
@@ -70,8 +73,8 @@ const ChartWidgetGraph = ({
|
||||
ticks: isEnlarged ? 6 : 3,
|
||||
nice: true,
|
||||
},
|
||||
};
|
||||
}, [ isEnlarged, isMobile ]);
|
||||
});
|
||||
}, [ axesConfigProps, isEnlarged, isMobile ]);
|
||||
|
||||
const {
|
||||
ref,
|
||||
|
||||
@@ -40,14 +40,14 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
|
||||
|
||||
// disable current tracker when entering multi touch mode
|
||||
touches.length > 1 && untrack(sourceEvent);
|
||||
})
|
||||
}, { passive: true })
|
||||
.on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => {
|
||||
if (sourceEvent.pointerId !== id) {
|
||||
return;
|
||||
}
|
||||
|
||||
untrack(sourceEvent);
|
||||
})
|
||||
}, { passive: true })
|
||||
.on(`pointermove.${ id }`, (sourceEvent) => {
|
||||
if (sourceEvent.pointerId !== id) {
|
||||
return;
|
||||
@@ -56,7 +56,7 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
|
||||
tracker.prev = tracker.point;
|
||||
tracker.point = d3.pointer(sourceEvent, target);
|
||||
move?.(tracker);
|
||||
})
|
||||
}, { passive: true })
|
||||
.on(`pointerout.${ id }`, (e) => {
|
||||
if (e.pointerId !== id) {
|
||||
return;
|
||||
@@ -64,7 +64,7 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
|
||||
tracker.sourceEvent = e;
|
||||
tracker.point = null;
|
||||
out?.(tracker);
|
||||
});
|
||||
}, { passive: true });
|
||||
|
||||
start?.(tracker);
|
||||
|
||||
|
||||
@@ -38,9 +38,14 @@ export interface AxisConfig {
|
||||
ticks?: number;
|
||||
nice?: boolean;
|
||||
noLabel?: boolean;
|
||||
scale?: {
|
||||
min?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AxesConfig {
|
||||
x?: AxisConfig;
|
||||
y?: AxisConfig;
|
||||
}
|
||||
|
||||
export type AxesConfigFn = (props: { isEnlarged?: boolean; isMobile?: boolean }) => AxesConfig;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { maxBy, uniq } from 'es-toolkit';
|
||||
|
||||
import type { AxesConfig, AxisConfig, TimeChartData } from '../types';
|
||||
|
||||
import { MONTH, YEAR } from 'toolkit/utils/consts';
|
||||
import { DAY, MONTH, YEAR } from 'toolkit/utils/consts';
|
||||
|
||||
export const DEFAULT_MAXIMUM_SIGNIFICANT_DIGITS = 2;
|
||||
export const DEFAULT_MAXIMUM_FRACTION_DIGITS = 3;
|
||||
@@ -51,8 +51,10 @@ const tickFormatterX = (axis: d3.Axis<d3.NumberValue>) => (d: d3.AxisDomain) =>
|
||||
format = d3.timeFormat('%Y');
|
||||
} else if (span > 4 * MONTH) {
|
||||
format = d3.timeFormat('%b \'%y');
|
||||
} else {
|
||||
} else if (span > 2 * DAY) {
|
||||
format = d3.timeFormat('%d %b');
|
||||
} else {
|
||||
format = d3.timeFormat('%H:%M');
|
||||
}
|
||||
|
||||
return format(d as Date);
|
||||
@@ -64,10 +66,10 @@ function getAxisParamsY(data: Data, config?: AxisConfig) {
|
||||
const max = d3.max(data, ({ items }) => d3.max(items, ({ value }) => value)) ?? 0;
|
||||
const scale = config?.nice ?
|
||||
d3.scaleLinear()
|
||||
.domain([ min, max ])
|
||||
.domain([ config?.scale?.min ?? min, max ])
|
||||
.nice(config?.ticks ?? DEFAULT_TICKS_NUM) :
|
||||
d3.scaleLinear()
|
||||
.domain([ min, max ]);
|
||||
.domain([ config?.scale?.min ?? min, max ]);
|
||||
|
||||
const ticks = scale.ticks(config?.ticks ?? DEFAULT_TICKS_NUM);
|
||||
const labelFormatParams = getYLabelFormatParams(ticks);
|
||||
|
||||
@@ -83,6 +83,7 @@ const NavLinkGroup = ({ item, isCollapsed }: Props) => {
|
||||
<Text
|
||||
{ ...styleProps.textProps }
|
||||
ml={ 3 }
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{ item.text }
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user