Commit 7c693149 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Gas tracker page (#1524)

* add custom headers to API resource

* refactoring

* page title

* chart

* gas prices snippet

* GasPrice component

* add ENV for gas units

* dark theme and other tweaks

* tests

* add base and priority fee

* [skip ci] small fiat values

* change behavior of the variable

* update tooltip layout

* refactor gas info tooltip

* refactor GasPrice component

* fix logic and deploy demo for eth mainnet

* tests

* remove link from anchor

* [skip ci] rollback ENVs for demo

* review fixes

* fix tests
parent 5c2d06fc
import type { Feature } from './types';
import { GAS_UNITS } from 'types/client/gasTracker';
import type { GasUnit } from 'types/client/gasTracker';
import { getEnvValue, parseEnvJson } from '../utils';
const isDisabled = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_ENABLED') === 'false';
const units = ((): Array<GasUnit> => {
const envValue = getEnvValue('NEXT_PUBLIC_GAS_TRACKER_UNITS');
if (!envValue) {
return [ 'usd', 'gwei' ];
}
const units = parseEnvJson<Array<GasUnit>>(envValue)?.filter((type) => GAS_UNITS.includes(type)) || [];
return units;
})();
const title = 'Gas tracker';
const config: Feature<{ units: Array<GasUnit> }> = (() => {
if (!isDisabled && units.length > 0) {
return Object.freeze({
title,
isEnabled: true,
units,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -6,6 +6,7 @@ export { default as beaconChain } from './beaconChain'; ...@@ -6,6 +6,7 @@ export { default as beaconChain } from './beaconChain';
export { default as bridgedTokens } from './bridgedTokens'; export { default as bridgedTokens } from './bridgedTokens';
export { default as blockchainInteraction } from './blockchainInteraction'; export { default as blockchainInteraction } from './blockchainInteraction';
export { default as csvExport } from './csvExport'; export { default as csvExport } from './csvExport';
export { default as gasTracker } from './gasTracker';
export { default as googleAnalytics } from './googleAnalytics'; export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs'; export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook'; export { default as growthBook } from './growthBook';
......
...@@ -49,7 +49,6 @@ const UI = Object.freeze({ ...@@ -49,7 +49,6 @@ const UI = Object.freeze({
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT, background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white', textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
}, },
showGasTracker: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER') === 'false' ? false : true,
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true, showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
}, },
views, views,
......
...@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/ ...@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
## homepage ## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND= NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
## sidebar ## sidebar
NEXT_PUBLIC_NETWORK_LOGO= NEXT_PUBLIC_NETWORK_LOGO=
......
...@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/ ...@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
## homepage ## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
## sidebar ## sidebar
## footer ## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11 NEXT_PUBLIC_GIT_TAG=v1.0.11
......
...@@ -12,6 +12,8 @@ import type { AdButlerConfig } from '../../../types/client/adButlerConfig'; ...@@ -12,6 +12,8 @@ import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders'; import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS } from '../../../types/client/adProviders';
import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders'; import type { AdTextProviders, AdBannerProviders } from '../../../types/client/adProviders';
import type { ContractCodeIde } from '../../../types/client/contract'; import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview } from '../../../types/client/marketplace'; import type { MarketplaceAppOverview } from '../../../types/client/marketplace';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items'; import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items'; import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
...@@ -390,7 +392,6 @@ const schema = yup ...@@ -390,7 +392,6 @@ const schema = yup
.of(yup.string<ChainIndicatorId>().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])), .of(yup.string<ChainIndicatorId>().oneOf([ 'daily_txs', 'coin_price', 'market_cap', 'tvl' ])),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(), NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER: yup.boolean(),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(), NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
// b. sidebar // b. sidebar
...@@ -493,6 +494,8 @@ const schema = yup ...@@ -493,6 +494,8 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(), NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(), NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE), NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -19,8 +19,9 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false ...@@ -19,8 +19,9 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff' NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)' NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>' NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>'
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
...@@ -6,3 +6,4 @@ ...@@ -6,3 +6,4 @@
| NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE | | NEXT_PUBLIC_IS_ZKEVM_L2_NETWORK | `boolean` | Set to true for zkevm L2 solutions | Required | - | `true` | v1.24.0 | Replaced by NEXT_PUBLIC_ROLLUP_TYPE |
| NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | | NEXT_PUBLIC_OPTIMISTIC_L2_WITHDRAWAL_URL | `string` | URL for optimistic L2 -> L1 withdrawals | Required | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL |
| NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL | | NEXT_PUBLIC_L1_BASE_URL | `string` | Blockscout base URL for L1 network | Required | - | `'http://eth-goerli.blockscout.com'` | v1.24.0 | Renamed to NEXT_PUBLIC_ROLLUP_L1_BASE_URL |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` | v1.25.0 | Replaced by NEXT_PUBLIC_GAS_TRACKER_ENABLED |
\ No newline at end of file
...@@ -28,6 +28,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -28,6 +28,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Misc](ENVS.md#misc) - [Misc](ENVS.md#misc)
- [App features](ENVS.md#app-features) - [App features](ENVS.md#app-features)
- [My account](ENVS.md#my-account) - [My account](ENVS.md#my-account)
- [Gas tracker](ENVS.md#gas-tracker)
- [Address verification](ENVS.md#address-verification-in-my-account) in "My account" - [Address verification](ENVS.md#address-verification-in-my-account) in "My account"
- [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.) - [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) (writing to contract, etc.)
- [Banner ads](ENVS.md#banner-ads) - [Banner ads](ENVS.md#banner-ads)
...@@ -108,7 +109,6 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -108,7 +109,6 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | | NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | | NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` |
&nbsp; &nbsp;
...@@ -302,6 +302,17 @@ Settings for meta tags and OG tags ...@@ -302,6 +302,17 @@ Settings for meta tags and OG tags
&nbsp; &nbsp;
### Gas tracker
This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_TRACKER_ENABLED=false`.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GAS_TRACKER_ENABLED | `boolean` | Set to true to enable "Gas tracker" in the app | Required | `true` | `false` |
| NEXT_PUBLIC_GAS_TRACKER_UNITS | Array<`usd` \| `gwei`> | Array of units for displaying gas prices on the Gas Tracker page, in the stats snippet on the Home page, and in the top bar. The first value in the array will take priority over the second one in all mentioned views. If only one value is provided, gas prices will be displayed only in that unit. | - | `[ 'usd', 'gwei' ]` | `[ 'gwei' ]` |
&nbsp;
### Address verification in "My account" ### Address verification in "My account"
*Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones: *Note* all ENV variables required for [My account](ENVS.md#my-account) feature should be passed alongside the following ones:
......
<svg viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.121 1C3.131 1 .667 3.464.667 6.455V41H26.12V28.273h3.637v7.272c0 2.99 2.464 5.455 5.454 5.455 2.99 0 5.455-2.465 5.455-5.455V17.023a5.33 5.33 0 0 0-1.591-3.807l-8.58-8.58-2.557 2.557 5.17 5.17c-1.952.832-3.352 2.756-3.352 5 0 2.99 2.465 5.455 5.455 5.455.64 0 1.243-.135 1.818-.34v13.067c0 1.03-.788 1.819-1.818 1.819a1.786 1.786 0 0 1-1.818-1.819v-7.272c0-1.989-1.648-3.637-3.636-3.637H26.12V6.455c0-2.99-2.464-5.455-5.454-5.455H6.12Zm0 3.636h14.546c1.03 0 1.818.789 1.818 1.819v7.272H4.303V6.455c0-1.03.788-1.819 1.818-1.819Zm29.091 10.91c1.023 0 1.818.795 1.818 1.818a1.795 1.795 0 0 1-1.818 1.818 1.795 1.795 0 0 1-1.818-1.818c0-1.023.795-1.819 1.818-1.819ZM4.303 17.364h18.182v20H4.303v-20Z" fill="currentColor" stroke="currentColor"/>
</svg>
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.05 8.552c3.799-3.798 7.474-4.099 9.352-3.954.145 1.878-.156 5.553-3.954 9.352l-11.32 11.32-5.398-5.398 9.163-9.162.008-.01 2.15-2.148ZM22.817 5.32l-1.484 1.483H9.94a3.81 3.81 0 0 0-2.656 1.132l-6.166 6.147-.003.003a3.81 3.81 0 0 0 1.95 6.43h.002l7.302 1.464 7.653 7.653 1.463 7.301v.003a3.812 3.812 0 0 0 6.43 1.95l6.151-6.169a3.81 3.81 0 0 0 1.132-2.656V18.667l1.483-1.484C40.216 11.65 40.24 5.961 39.863 3.36A3.773 3.773 0 0 0 36.641.137c-2.602-.376-8.29-.353-13.824 5.182ZM5.358 16.31l5.388 1.08 6.015-6.015h-6.452l-4.95 4.935ZM22.61 29.255l1.08 5.387 4.935-4.951v-6.452l-6.015 6.015Zm-13.608-.278a2.286 2.286 0 1 0-2.29-3.958c-3.224 1.866-4.709 5.062-5.404 7.487a19.116 19.116 0 0 0-.697 4.184 11.72 11.72 0 0 0-.012.38v.044l2.286.001H.598a2.286 2.286 0 0 0 2.287 2.287v-2.287l.001 2.287h.045a9.419 9.419 0 0 0 .38-.013 19.11 19.11 0 0 0 4.184-.698c2.424-.694 5.62-2.179 7.486-5.404a2.286 2.286 0 0 0-3.958-2.29c-1.012 1.749-2.874 2.75-4.788 3.299-.244.07-.483.13-.714.183.053-.23.113-.47.183-.714.549-1.914 1.55-3.776 3.298-4.788Z" fill="currentColor"/>
</svg>
...@@ -108,6 +108,7 @@ export interface ApiResource { ...@@ -108,6 +108,7 @@ export interface ApiResource {
basePath?: string; basePath?: string;
pathParams?: Array<string>; pathParams?: Array<string>;
needAuth?: boolean; // for external APIs which require authentication needAuth?: boolean; // for external APIs which require authentication
headers?: RequestInit['headers'];
} }
export const SORTING_FIELDS = [ 'sort', 'order' ]; export const SORTING_FIELDS = [ 'sort', 'order' ];
...@@ -184,7 +185,7 @@ export const RESOURCES = { ...@@ -184,7 +185,7 @@ export const RESOURCES = {
needAuth: true, needAuth: true,
}, },
// STATS // STATS MICROSERVICE API
stats_counters: { stats_counters: {
path: '/api/v1/counters', path: '/api/v1/counters',
endpoint: getFeaturePayload(config.features.stats)?.api.endpoint, endpoint: getFeaturePayload(config.features.stats)?.api.endpoint,
...@@ -513,16 +514,21 @@ export const RESOURCES = { ...@@ -513,16 +514,21 @@ export const RESOURCES = {
filterFields: [], filterFields: [],
}, },
// HOMEPAGE // APP STATS
homepage_stats: { stats: {
path: '/api/v2/stats', path: '/api/v2/stats',
headers: {
'updated-gas-oracle': 'true',
},
}, },
homepage_chart_txs: { stats_charts_txs: {
path: '/api/v2/stats/charts/transactions', path: '/api/v2/stats/charts/transactions',
}, },
homepage_chart_market: { stats_charts_market: {
path: '/api/v2/stats/charts/market', path: '/api/v2/stats/charts/market',
}, },
// HOMEPAGE
homepage_blocks: { homepage_blocks: {
path: '/api/v2/main-page/blocks', path: '/api/v2/main-page/blocks',
}, },
...@@ -750,9 +756,9 @@ Q extends 'watchlist' ? WatchlistResponse : ...@@ -750,9 +756,9 @@ Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse : Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig : Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications : Q extends 'token_info_applications' ? TokenInfoApplications :
Q extends 'homepage_stats' ? HomeStats : Q extends 'stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse : Q extends 'stats_charts_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse : Q extends 'stats_charts_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_txs_watchlist' ? Array<Transaction> : Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
......
...@@ -41,6 +41,7 @@ export default function useApiFetch() { ...@@ -41,6 +41,7 @@ export default function useApiFetch() {
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...resource.headers,
...fetchParams?.headers, ...fetchParams?.headers,
}, Boolean) as HeadersInit; }, Boolean) as HeadersInit;
......
...@@ -219,8 +219,13 @@ export default function useNavItems(): ReturnType { ...@@ -219,8 +219,13 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/contract-verification' as const }, nextRoute: { pathname: '/contract-verification' as const },
isActive: pathname.startsWith('/contract-verification'), isActive: pathname.startsWith('/contract-verification'),
}, },
config.features.gasTracker.isEnabled && {
text: 'Gas tracker',
nextRoute: { pathname: '/gas-tracker' as const },
isActive: pathname.startsWith('/gas-tracker'),
},
...config.UI.sidebar.otherLinks, ...config.UI.sidebar.otherLinks,
], ].filter(Boolean),
}, },
].filter(Boolean); ].filter(Boolean);
......
...@@ -43,6 +43,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -43,6 +43,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/name-domains': 'Root page', '/name-domains': 'Root page',
'/name-domains/[name]': 'Regular page', '/name-domains/[name]': 'Regular page',
'/validators': 'Root page', '/validators': 'Root page',
'/gas-tracker': 'Root page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
......
...@@ -46,6 +46,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -46,6 +46,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': DEFAULT_TEMPLATE, '/name-domains': DEFAULT_TEMPLATE,
'/name-domains/[name]': DEFAULT_TEMPLATE, '/name-domains/[name]': DEFAULT_TEMPLATE,
'/validators': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE,
'/gas-tracker': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
......
...@@ -41,6 +41,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -41,6 +41,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/name-domains': 'domains search and resolve', '/name-domains': 'domains search and resolve',
'/name-domains/[name]': '%name% domain details', '/name-domains/[name]': '%name% domain details',
'/validators': 'validators list', '/validators': 'validators list',
'/gas-tracker': 'gas tracker',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'login', '/login': 'login',
......
...@@ -41,6 +41,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -41,6 +41,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/name-domains': 'Domains search and resolve', '/name-domains': 'Domains search and resolve',
'/name-domains/[name]': 'Domain details', '/name-domains/[name]': 'Domain details',
'/validators': 'Validators list', '/validators': 'Validators list',
'/gas-tracker': 'Gas tracker',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
......
import type { HomeStats } from 'types/api/stats'; import _mapValues from 'lodash/mapValues';
export const base: HomeStats = { export const base = {
average_block_time: 6212.0, average_block_time: 6212.0,
coin_price: '0.00199678', coin_price: '0.00199678',
coin_price_change_percentage: -7.42, coin_price_change_percentage: -7.42,
gas_prices: { gas_prices: {
average: { average: {
fiat_price: '1.01', fiat_price: '1.39',
price: 20.41, price: 23.75,
time: 12283, time: 12030.25,
base_fee: 2.22222,
priority_fee: 12.424242,
}, },
fast: { fast: {
fiat_price: '1.26', fiat_price: '1.74',
price: 25.47, price: 29.72,
time: 9321, time: 8763.25,
base_fee: 4.44444,
priority_fee: 22.242424,
}, },
slow: { slow: {
fiat_price: '0.97', fiat_price: '1.35',
price: 19.55, price: 23.04,
time: 24543, time: 20100.25,
base_fee: 1.11111,
priority_fee: 7.8909,
}, },
}, },
gas_price_updated_at: '2022-11-11T11:09:49.051171Z', gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
...@@ -35,7 +41,22 @@ export const base: HomeStats = { ...@@ -35,7 +41,22 @@ export const base: HomeStats = {
tvl: '1767425.102766552', tvl: '1767425.102766552',
}; };
export const withBtcLocked: HomeStats = { export const withBtcLocked = {
...base, ...base,
rootstock_locked_btc: '3337493406696977561374', rootstock_locked_btc: '3337493406696977561374',
}; };
export const withoutFiatPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null),
};
export const withoutGweiPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null),
};
export const withoutBothPrices = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
};
export const averageGasPrice = {
chart: [
{
date: '2023-12-22',
value: '37.7804422597599',
},
{
date: '2023-12-23',
value: '25.84889883009387',
},
{
date: '2023-12-24',
value: '25.818463227198574',
},
{
date: '2023-12-25',
value: '26.045513050051298',
},
{
date: '2023-12-26',
value: '21.42600692652399',
},
{
date: '2023-12-27',
value: '31.066730409846656',
},
{
date: '2023-12-28',
value: '33.63955781902089',
},
{
date: '2023-12-29',
value: '28.064736756058384',
},
{
date: '2023-12-30',
value: '23.074500869678175',
},
{
date: '2023-12-31',
value: '17.651005734615133',
},
{
date: '2024-01-01',
value: '14.906085174476441',
},
{
date: '2024-01-02',
value: '22.28459059038656',
},
{
date: '2024-01-03',
value: '39.8311646806592',
},
{
date: '2024-01-04',
value: '26.09989322256083',
},
{
date: '2024-01-05',
value: '22.821996688111998',
},
{
date: '2024-01-06',
value: '20.32680041262083',
},
{
date: '2024-01-07',
value: '32.535045831809704',
},
{
date: '2024-01-08',
value: '27.443477102139482',
},
{
date: '2024-01-09',
value: '20.7911332558055',
},
{
date: '2024-01-10',
value: '42.10740192523919',
},
{
date: '2024-01-11',
value: '35.75215680343582',
},
{
date: '2024-01-12',
value: '27.430414798093253',
},
{
date: '2024-01-13',
value: '20.170934096589875',
},
{
date: '2024-01-14',
value: '38.79660984371034',
},
{
date: '2024-01-15',
value: '26.140740484554204',
},
{
date: '2024-01-16',
value: '36.708543184194156',
},
{
date: '2024-01-17',
value: '40.325438794298876',
},
{
date: '2024-01-18',
value: '37.55145309930694',
},
{
date: '2024-01-19',
value: '33.271450114434664',
},
{
date: '2024-01-20',
value: '19.303304377685638',
},
{
date: '2024-01-21',
value: '14.375908594704976',
},
],
};
export const base = {
sections: [
{
id: 'accounts',
title: 'Accounts',
charts: [
{
id: 'accountsGrowth',
title: 'Accounts growth',
description: 'Cumulative accounts number per period',
units: null,
},
{
id: 'activeAccounts',
title: 'Active accounts',
description: 'Active accounts number per period',
units: null,
},
{
id: 'newAccounts',
title: 'New accounts',
description: 'New accounts number per day',
units: null,
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'averageTxnFee',
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
},
{
id: 'newTxns',
title: 'New transactions',
description: 'New transactions number',
units: null,
},
{
id: 'txnsFee',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
},
{
id: 'txnsGrowth',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: null,
},
{
id: 'txnsSuccessRate',
title: 'Transactions success rate',
description: 'Successful transactions rate per day',
units: null,
},
],
},
{
id: 'blocks',
title: 'Blocks',
charts: [
{
id: 'averageBlockRewards',
title: 'Average block rewards',
description: 'Average amount of distributed reward in tokens per day',
units: 'ETH',
},
{
id: 'averageBlockSize',
title: 'Average block size',
description: 'Average size of blocks in bytes',
units: 'Bytes',
},
{
id: 'newBlocks',
title: 'New blocks',
description: 'New blocks number',
units: null,
},
],
},
{
id: 'tokens',
title: 'Tokens',
charts: [
{
id: 'newNativeCoinTransfers',
title: 'New ETH transfers',
description: 'New token transfers number for the period',
units: null,
},
],
},
{
id: 'gas',
title: 'Gas',
charts: [
{
id: 'averageGasLimit',
title: 'Average gas limit',
description: 'Average gas limit per block for the period',
units: null,
},
{
id: 'averageGasPrice',
title: 'Average gas price',
description: 'Average gas price for the period (Gwei)',
units: 'Gwei',
},
{
id: 'gasUsedGrowth',
title: 'Gas used growth',
description: 'Cumulative gas used for the period',
units: null,
},
],
},
{
id: 'contracts',
title: 'Contracts',
charts: [
{
id: 'newVerifiedContracts',
title: 'New verified contracts',
description: 'New verified contracts number for the period',
units: null,
},
{
id: 'verifiedContractsGrowth',
title: 'Verified contracts growth',
description: 'Cumulative number verified contracts for the period',
units: null,
},
],
},
],
};
...@@ -191,3 +191,13 @@ export const validators: GetServerSideProps<Props> = async(context) => { ...@@ -191,3 +191,13 @@ export const validators: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const gasTracker: GetServerSideProps<Props> = async(context) => {
if (!config.features.gasTracker.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -33,6 +33,7 @@ declare module "nextjs-routes" { ...@@ -33,6 +33,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/contract-verification"> | StaticRoute<"/contract-verification">
| StaticRoute<"/csv-export"> | StaticRoute<"/csv-export">
| StaticRoute<"/deposits"> | StaticRoute<"/deposits">
| StaticRoute<"/gas-tracker">
| StaticRoute<"/graphiql"> | StaticRoute<"/graphiql">
| StaticRoute<"/"> | StaticRoute<"/">
| StaticRoute<"/login"> | StaticRoute<"/login">
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const GasTracker = dynamic(() => import('ui/pages/GasTracker'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/gas-tracker">
<GasTracker/>
</PageNextJs>
);
};
export default Page;
export { gasTracker as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -3,6 +3,7 @@ import { devices } from '@playwright/test'; ...@@ -3,6 +3,7 @@ import { devices } from '@playwright/test';
export const viewport = { export const viewport = {
mobile: devices['iPhone 13 Pro'].viewport, mobile: devices['iPhone 13 Pro'].viewport,
md: { width: 1001, height: 800 },
xl: { width: 1600, height: 1000 }, xl: { width: 1600, height: 1000 },
}; };
......
...@@ -49,6 +49,7 @@ ...@@ -49,6 +49,7 @@
| "filter" | "filter"
| "finalized" | "finalized"
| "flame" | "flame"
| "gas_xl"
| "gas" | "gas"
| "gear" | "gear"
| "globe-b" | "globe-b"
...@@ -82,6 +83,7 @@ ...@@ -82,6 +83,7 @@
| "qr_code" | "qr_code"
| "repeat_arrow" | "repeat_arrow"
| "restAPI" | "restAPI"
| "rocket_xl"
| "rocket" | "rocket"
| "RPC" | "RPC"
| "scope" | "scope"
......
...@@ -9,16 +9,22 @@ export const HOMEPAGE_STATS: HomeStats = { ...@@ -9,16 +9,22 @@ export const HOMEPAGE_STATS: HomeStats = {
fiat_price: '1.01', fiat_price: '1.01',
price: 20.41, price: 20.41,
time: 12283, time: 12283,
base_fee: 2.22222,
priority_fee: 12.424242,
}, },
fast: { fast: {
fiat_price: '1.26', fiat_price: '1.26',
price: 25.47, price: 25.47,
time: 9321, time: 9321,
base_fee: 4.44444,
priority_fee: 22.242424,
}, },
slow: { slow: {
fiat_price: '0.97', fiat_price: '0.97',
price: 19.55, price: 19.55,
time: 24543, time: 24543,
base_fee: 1.11111,
priority_fee: 7.8909,
}, },
}, },
gas_price_updated_at: '2022-11-11T11:09:49.051171Z', gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
......
...@@ -28,6 +28,8 @@ export interface GasPriceInfo { ...@@ -28,6 +28,8 @@ export interface GasPriceInfo {
fiat_price: string | null; fiat_price: string | null;
price: number | null; price: number | null;
time: number | null; time: number | null;
base_fee: number | null;
priority_fee: number | null;
} }
export type Counters = { export type Counters = {
......
export const GAS_UNITS = [
'usd',
'gwei',
] as const;
export type GasUnit = typeof GAS_UNITS[number];
...@@ -13,12 +13,7 @@ interface Props { ...@@ -13,12 +13,7 @@ interface Props {
} }
const BlocksTabSlot = ({ pagination }: Props) => { const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useApiQuery('homepage_stats', { const statsQuery = useApiQuery('stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
}, },
......
import { Box, Flex, chakra, useBoolean } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import { STATS_CHARTS } from 'stubs/stats';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LinkInternal from 'ui/shared/LinkInternal';
import ChartWidgetContainer from 'ui/stats/ChartWidgetContainer';
const GAS_PRICE_CHART_ID = 'averageGasPrice';
const GasTrackerChart = () => {
const [ isChartLoadingError, setChartLoadingError ] = useBoolean(false);
const { data, isPlaceholderData, isError } = useApiQuery('stats_lines', {
queryOptions: {
placeholderData: STATS_CHARTS,
},
});
const content = (() => {
if (isPlaceholderData) {
return <ContentLoader/>;
}
if (isChartLoadingError || isError) {
return <DataFetchAlert/>;
}
const chart = data?.sections.map((section) => section.charts.find((chart) => chart.id === GAS_PRICE_CHART_ID)).filter(Boolean)?.[0];
if (!chart) {
return <DataFetchAlert/>;
}
return (
<ChartWidgetContainer
id={ GAS_PRICE_CHART_ID }
title={ chart.title }
description={ chart.description }
interval="oneMonth"
units={ chart.units || undefined }
isPlaceholderData={ isPlaceholderData }
onLoadingError={ setChartLoadingError.on }
h="320px"
/>
);
})();
return (
<Box>
<Flex justifyContent="space-between" alignItems="center" mb={ 6 }>
<chakra.h3 textStyle="h3">Gas price history</chakra.h3>
<LinkInternal href={ route({ pathname: '/stats', hash: 'gas' }) }>Charts & stats</LinkInternal>
</Flex>
{ content }
</Box>
);
};
export default React.memo(GasTrackerChart);
import { Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import { mdash } from 'lib/html-entities';
interface Props {
percentage: number;
isLoading: boolean;
}
const GasTrackerNetworkUtilization = ({ percentage, isLoading }: Props) => {
const load = (() => {
if (percentage > 80) {
return 'high';
}
if (percentage > 50) {
return 'medium';
}
return 'low';
})();
const colors = {
high: 'red.600',
medium: 'orange.600',
low: 'green.600',
};
const color = colors[load];
return (
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">
<span>Network utilization </span>
<chakra.span color={ color }>{ percentage.toFixed(2) }% { mdash } { load } load</chakra.span>
</Skeleton>
);
};
export default React.memo(GasTrackerNetworkUtilization);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import * as configs from 'playwright/utils/configs';
import GasTrackerPriceSnippet from './GasTrackerPriceSnippet';
test.use({ viewport: configs.viewport.md });
const data = statsMock.base.gas_prices.fast;
const clip = { x: 0, y: 0, width: 334, height: 204 };
test('with usd as primary unit +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="fast"
isLoading={ false }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
test('loading state', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="fast"
isLoading={ true }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
const gweiUnitsTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_GAS_TRACKER_UNITS', value: '["gwei","usd"]' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
gweiUnitsTest('with gwei as primary unit +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<GasTrackerPriceSnippet
data={ data }
type="slow"
isLoading={ false }
/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip });
});
import { Box, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo, GasPrices } from 'types/api/stats';
import { SECOND } from 'lib/consts';
import { asymp } from 'lib/html-entities';
import GasPrice from 'ui/shared/gas/GasPrice';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
type: keyof GasPrices;
data: GasPriceInfo;
isLoading: boolean;
}
const TITLES: Record<keyof GasPrices, string> = {
fast: 'Fast',
average: 'Normal',
slow: 'Slow',
};
const ICONS: Record<keyof GasPrices, IconName> = {
fast: 'rocket_xl',
average: 'gas_xl',
slow: 'gas_xl',
};
const GasTrackerPriceSnippet = ({ data, type, isLoading }: Props) => {
const bgColors = {
fast: 'transparent',
average: useColorModeValue('gray.50', 'whiteAlpha.200'),
slow: useColorModeValue('gray.50', 'whiteAlpha.200'),
};
return (
<Box
as="li"
listStyleType="none"
px={ 9 }
py={ 6 }
w={{ lg: 'calc(100% / 3)' }}
bgColor={ bgColors[type] }
>
<Skeleton textStyle="h3" display="inline-block" isLoaded={ !isLoading }>{ TITLES[type] }</Skeleton>
<Flex columnGap={ 3 } alignItems="center" mt={ 3 }>
<IconSvg name={ ICONS[type] } boxSize={{ base: '30px', xl: 10 }} isLoading={ isLoading } flexShrink={ 0 }/>
<Skeleton isLoaded={ !isLoading }>
<GasPrice data={ data } fontSize={{ base: '36px', xl: '48px' }} lineHeight="48px" fontWeight={ 600 } letterSpacing="-1px" fontFamily="heading"/>
</Skeleton>
</Flex>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" mt={ 3 } display="inline-block">
{ data.price && data.fiat_price && <GasPrice data={ data } prefix={ `${ asymp } ` } unitMode="secondary"/> }
<span> per transaction</span>
{ data.time && <span> / { (data.time / SECOND).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s</span> }
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontSize="sm" color="text_secondary" mt={ 2 } display="inline-block" whiteSpace="pre">
{ data.base_fee && <span>Base { data.base_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }</span> }
{ data.base_fee && data.priority_fee && <span> / </span> }
{ data.priority_fee && <span>Priority { data.priority_fee.toLocaleString(undefined, { maximumFractionDigits: 0 }) }</span> }
</Skeleton>
</Box>
);
};
export default React.memo(GasTrackerPriceSnippet);
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { GasPrices } from 'types/api/stats';
import GasTrackerPriceSnippet from './GasTrackerPriceSnippet';
interface Props {
prices: GasPrices;
isLoading: boolean;
}
const GasTrackerPrices = ({ prices, isLoading }: Props) => {
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.300');
return (
<Flex
as="ul"
flexDir={{ base: 'column', lg: 'row' }}
borderColor={ borderColor }
borderWidth="2px"
borderRadius="xl"
overflow="hidden"
sx={{
'li:not(:last-child)': {
borderColor: borderColor,
borderRightWidth: { lg: '2px' },
borderBottomWidth: { base: '2px', lg: '0' },
},
}}
>
{ prices.fast && <GasTrackerPriceSnippet type="fast" data={ prices.fast } isLoading={ isLoading }/> }
{ prices.average && <GasTrackerPriceSnippet type="average" data={ prices.average } isLoading={ isLoading }/> }
{ prices.slow && <GasTrackerPriceSnippet type="slow" data={ prices.slow } isLoading={ isLoading }/> }
</Flex>
);
};
export default React.memo(GasTrackerPrices);
...@@ -11,7 +11,7 @@ import * as configs from 'playwright/utils/configs'; ...@@ -11,7 +11,7 @@ import * as configs from 'playwright/utils/configs';
import LatestBlocks from './LatestBlocks'; import LatestBlocks from './LatestBlocks';
const STATS_API_URL = buildApiUrl('homepage_stats'); const STATS_API_URL = buildApiUrl('stats');
const BLOCKS_API_URL = buildApiUrl('homepage_blocks'); const BLOCKS_API_URL = buildApiUrl('homepage_blocks');
export const test = base.extend<socketServer.SocketServerFixture>({ export const test = base.extend<socketServer.SocketServerFixture>({
......
...@@ -36,12 +36,7 @@ const LatestBlocks = () => { ...@@ -36,12 +36,7 @@ const LatestBlocks = () => {
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', { const statsQueryResult = useApiQuery('stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
......
...@@ -10,7 +10,7 @@ import * as configs from 'playwright/utils/configs'; ...@@ -10,7 +10,7 @@ import * as configs from 'playwright/utils/configs';
import Stats from './Stats'; import Stats from './Stats';
const API_URL = buildApiUrl('homepage_stats'); const API_URL = buildApiUrl('stats');
test.describe('all items', () => { test.describe('all items', () => {
let component: Locator; let component: Locator;
...@@ -69,7 +69,7 @@ test.describe('3 items', () => { ...@@ -69,7 +69,7 @@ test.describe('3 items', () => {
const extendedTest = test.extend({ const extendedTest = test.extend({
context: contextWithEnvs([ context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', value: 'false' }, { name: 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', value: 'false' },
{ name: 'NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER', value: 'false' }, { name: 'NEXT_PUBLIC_GAS_TRACKER_ENABLED', value: 'false' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any, ]) as any,
}); });
......
...@@ -7,23 +7,19 @@ import { route } from 'nextjs-routes'; ...@@ -7,23 +7,19 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import { currencyUnits } from 'lib/units';
import { HOMEPAGE_STATS } from 'stubs/stats'; import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import StatsItem from './StatsItem'; import StatsItem from './StatsItem';
const hasGasTracker = config.UI.homepage.showGasTracker; const hasGasTracker = config.features.gasTracker.isEnabled;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime; const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const Stats = () => { const Stats = () => {
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', { const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
...@@ -53,19 +49,21 @@ const Stats = () => { ...@@ -53,19 +49,21 @@ const Stats = () => {
!data.gas_prices && itemsCount--; !data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++; data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2); const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> : null; const gasInfoTooltip = hasGasTracker && data.gas_prices ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
const gasPriceText = (() => { <IconSvg
if (data.gas_prices?.average?.fiat_price) { isLoading={ isPlaceholderData }
return `$${ data.gas_prices.average.fiat_price }`; name="info"
} boxSize={ 5 }
display="block"
if (data.gas_prices?.average?.price) { cursor="pointer"
return `${ data.gas_prices.average.price.toLocaleString() } ${ currencyUnits.gwei }`; _hover={{ color: 'link_hovered' }}
} position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
return 'N/A'; right="10px"
})(); />
</GasInfoTooltip>
) : null;
content = ( content = (
<> <>
...@@ -112,9 +110,9 @@ const Stats = () => { ...@@ -112,9 +110,9 @@ const Stats = () => {
<StatsItem <StatsItem
icon="gas" icon="gas"
title="Gas tracker" title="Gas tracker"
value={ gasPriceText } value={ <GasPrice data={ data.gas_prices.average }/> }
_last={ isOdd ? lastItemTouchStyle : undefined } _last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel } tooltip={ gasInfoTooltip }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
) } ) }
......
import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react'; import type { SystemStyleObject } from '@chakra-ui/react';
import { Skeleton, Flex, useColorModeValue, chakra, LightMode } from '@chakra-ui/react'; import { Skeleton, Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import breakpoints from 'theme/foundations/breakpoints'; import breakpoints from 'theme/foundations/breakpoints';
import Hint from 'ui/shared/Hint';
import type { IconName } from 'ui/shared/IconSvg'; import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
type Props = { type Props = {
icon: IconName; icon: IconName;
title: string; title: string;
value: string; value: string | React.ReactNode;
className?: string; className?: string;
tooltipLabel?: React.ReactNode; tooltip?: React.ReactNode;
url?: string; url?: string;
isLoading?: boolean; isLoading?: boolean;
} }
const LARGEST_BREAKPOINT = '1240px'; const LARGEST_BREAKPOINT = '1240px';
const TOOLTIP_PROPS: Partial<TooltipProps> = { const StatsItem = ({ icon, title, value, className, tooltip, url, isLoading }: Props) => {
hasArrow: false,
borderRadius: 'md',
placement: 'bottom-end',
offset: [ 0, 0 ],
bgColor: 'blackAlpha.900',
};
const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = { const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' }, [`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
}; };
...@@ -38,7 +29,6 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading ...@@ -38,7 +29,6 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading
const bgColor = useColorModeValue('blue.50', 'blue.800'); const bgColor = useColorModeValue('blue.50', 'blue.800');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const infoColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Flex <Flex
...@@ -68,22 +58,10 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading ...@@ -68,22 +58,10 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel, url, isLoading
<span>{ title }</span> <span>{ title }</span>
</Skeleton> </Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base"> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
<span>{ value }</span> { typeof value === 'string' ? <span>{ value }</span> : value }
</Skeleton> </Skeleton>
</Flex> </Flex>
{ tooltipLabel && !isLoading && ( { tooltip }
<LightMode>
<Hint
label={ tooltipLabel }
tooltipProps={ TOOLTIP_PROPS }
boxSize={ 6 }
color={ infoColor }
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</LightMode>
) }
</Flex> </Flex>
); );
}; };
......
...@@ -10,8 +10,8 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -10,8 +10,8 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ChainIndicators from './ChainIndicators'; import ChainIndicators from './ChainIndicators';
const STATS_API_URL = buildApiUrl('homepage_stats'); const STATS_API_URL = buildApiUrl('stats');
const TX_CHART_API_URL = buildApiUrl('homepage_chart_txs'); const TX_CHART_API_URL = buildApiUrl('stats_charts_txs');
const test = base.extend({ const test = base.extend({
context: contextWithEnvs([ context: contextWithEnvs([
......
...@@ -30,12 +30,7 @@ const ChainIndicators = () => { ...@@ -30,12 +30,7 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator); const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator); const queryResult = useFetchChartData(indicator);
const statsQueryResult = useApiQuery('homepage_stats', { const statsQueryResult = useApiQuery('stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
......
...@@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types'; ...@@ -4,7 +4,7 @@ import type { TimeChartData } from 'ui/shared/chart/types';
import type { ResourcePayload } from 'lib/api/resources'; import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market'; export type ChartsResources = 'stats_charts_txs' | 'stats_charts_market';
export interface TChainIndicator<R extends ChartsResources> { export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId; id: ChainIndicatorId;
......
...@@ -8,7 +8,7 @@ import type { ResourcePayload } from 'lib/api/resources'; ...@@ -8,7 +8,7 @@ import type { ResourcePayload } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
export default function useFetchChartData<R extends ChartsResources>(indicator: TChainIndicator<R> | undefined): UseQueryResult<TimeChartData> { export default function useFetchChartData<R extends ChartsResources>(indicator: TChainIndicator<R> | undefined): UseQueryResult<TimeChartData> {
const queryResult = useApiQuery(indicator?.api.resourceName || 'homepage_chart_txs', { const queryResult = useApiQuery(indicator?.api.resourceName || 'stats_charts_txs', {
queryOptions: { enabled: Boolean(indicator) }, queryOptions: { enabled: Boolean(indicator) },
}); });
......
...@@ -7,14 +7,14 @@ import { sortByDateDesc } from 'ui/shared/chart/utils/sorts'; ...@@ -7,14 +7,14 @@ import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = { const dailyTxsIndicator: TChainIndicator<'stats_charts_txs'> = {
id: 'daily_txs', id: 'daily_txs',
title: 'Daily transactions', title: 'Daily transactions',
value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), value: (stats) => Number(stats.transactions_today).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
icon: <IconSvg name="transactions" boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>, icon: <IconSvg name="transactions" boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`, hint: `Number of transactions yesterday (0:00 - 23:59 UTC). The chart displays daily transactions for the past 30 days.`,
api: { api: {
resourceName: 'homepage_chart_txs', resourceName: 'stats_charts_txs',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count })) .map((item) => ({ date: new Date(item.date), value: item.tx_count }))
...@@ -33,14 +33,14 @@ const nativeTokenData = { ...@@ -33,14 +33,14 @@ const nativeTokenData = {
type: 'ERC-20' as const, type: 'ERC-20' as const,
}; };
const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = { const coinPriceIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'coin_price', id: 'coin_price',
title: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`, title: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>, icon: <TokenEntity.Icon token={ nativeTokenData } boxSize={ 6 } marginRight={ 0 }/>,
hint: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } token daily price in USD.`, hint: `${ config.chain.governanceToken.symbol || config.chain.currency.symbol } token daily price in USD.`,
api: { api: {
resourceName: 'homepage_chart_market', resourceName: 'stats_charts_market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) })) .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
...@@ -51,7 +51,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = { ...@@ -51,7 +51,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
}, },
}; };
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = { const marketPriceIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'market_cap', id: 'market_cap',
title: 'Market cap', title: 'Market cap',
value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }), value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }),
...@@ -59,7 +59,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = { ...@@ -59,7 +59,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.', hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.',
api: { api: {
resourceName: 'homepage_chart_market', resourceName: 'stats_charts_market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ( .map((item) => (
...@@ -74,7 +74,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = { ...@@ -74,7 +74,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
}, },
}; };
const tvlIndicator: TChainIndicator<'homepage_chart_market'> = { const tvlIndicator: TChainIndicator<'stats_charts_market'> = {
id: 'tvl', id: 'tvl',
title: 'Total value locked', title: 'Total value locked',
value: (stats) => '$' + Number(stats.tvl).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }), value: (stats) => '$' + Number(stats.tvl).toLocaleString(undefined, { maximumFractionDigits: 2, notation: 'compact' }),
...@@ -82,7 +82,7 @@ const tvlIndicator: TChainIndicator<'homepage_chart_market'> = { ...@@ -82,7 +82,7 @@ const tvlIndicator: TChainIndicator<'homepage_chart_market'> = {
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
hint: 'Total value of digital assets locked or staked in a chain', hint: 'Total value of digital assets locked or staked in a chain',
api: { api: {
resourceName: 'homepage_chart_market', resourceName: 'stats_charts_market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ( .map((item) => (
......
...@@ -13,7 +13,7 @@ import * as configs from 'playwright/utils/configs'; ...@@ -13,7 +13,7 @@ import * as configs from 'playwright/utils/configs';
import Blocks from './Blocks'; import Blocks from './Blocks';
const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block'; const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block';
const STATS_API_URL = buildApiUrl('homepage_stats'); const STATS_API_URL = buildApiUrl('stats');
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { tab: 'blocks' }, query: { tab: 'blocks' },
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import * as statsLineMock from 'mocks/stats/line';
import * as statsLinesMock from 'mocks/stats/lines';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import GasTracker from './GasTracker';
const STATS_LINES_API_URL = buildApiUrl('stats_lines');
const GAS_PRICE_CHART_API_URL = buildApiUrl('stats_line', { id: 'averageGasPrice' }) + '?**';
const STATS_API_URL = buildApiUrl('stats');
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ ...statsMock.base, coin_price: '2442.789' }),
}));
await page.route(STATS_LINES_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsLinesMock.base),
}));
await page.route(GAS_PRICE_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsLineMock.averageGasPrice),
}));
const component = await mount(
<TestApp>
<GasTracker/>
</TestApp>,
);
await page.waitForResponse(GAS_PRICE_CHART_API_URL);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Averagegasprice-small"]')?.getAttribute('opacity') === '1';
});
await expect(component).toHaveScreenshot();
});
import { Box, Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import { HOMEPAGE_STATS } from 'stubs/stats';
import GasTrackerChart from 'ui/gasTracker/GasTrackerChart';
import GasTrackerNetworkUtilization from 'ui/gasTracker/GasTrackerNetworkUtilization';
import GasTrackerPrices from 'ui/gasTracker/GasTrackerPrices';
import GasInfoUpdateTimer from 'ui/shared/gas/GasInfoUpdateTimer';
import PageTitle from 'ui/shared/Page/PageTitle';
const GasTracker = () => {
const { data, isPlaceholderData, isError, error, dataUpdatedAt } = useApiQuery('stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
refetchOnMount: false,
},
});
if (isError) {
throw new Error(undefined, { cause: error });
}
const isLoading = isPlaceholderData;
const titleSecondRow = (
<Flex
alignItems={{ base: 'flex-start', lg: 'center' }}
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
w="100%"
columnGap={ 3 }
rowGap={ 1 }
flexDir={{ base: 'column', lg: 'row' }}
>
{ data?.network_utilization_percentage && <GasTrackerNetworkUtilization percentage={ data.network_utilization_percentage } isLoading={ isLoading }/> }
{ data?.gas_price_updated_at && (
<Skeleton isLoaded={ !isLoading } whiteSpace="pre" display="flex" alignItems="center">
<span>Last updated </span>
<chakra.span color="text_secondary">{ dayjs(data.gas_price_updated_at).format('DD MMM, HH:mm:ss') }</chakra.span>
{ data.gas_prices_update_in !== 0 && (
<GasInfoUpdateTimer
key={ dataUpdatedAt }
startTime={ dataUpdatedAt }
duration={ data.gas_prices_update_in }
size={ 5 }
ml={ 2 }
/>
) }
</Skeleton>
) }
{ data?.coin_price && (
<Skeleton isLoaded={ !isLoading } ml={{ base: 0, lg: 'auto' }} whiteSpace="pre">
<chakra.span color="text_secondary">{ config.chain.currency.symbol }</chakra.span>
<span> ${ Number(data.coin_price).toLocaleString(undefined, { maximumFractionDigits: 2 }) }</span>
</Skeleton>
) }
</Flex>
);
return (
<>
<PageTitle
title="Gas tracker"
secondRow={ titleSecondRow }
withTextAd
/>
{ data?.gas_prices && <GasTrackerPrices prices={ data.gas_prices } isLoading={ isLoading }/> }
<Box mt={ 12 }>
<GasTrackerChart/>
</Box>
</>
);
};
export default GasTracker;
...@@ -17,7 +17,7 @@ test.describe('default view', () => { ...@@ -17,7 +17,7 @@ test.describe('default view', () => {
let component: Locator; let component: Locator;
test.beforeEach(async({ page, mount }) => { test.beforeEach(async({ page, mount }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({ await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(statsMock.base), body: JSON.stringify(statsMock.base),
})); }));
...@@ -36,7 +36,7 @@ test.describe('default view', () => { ...@@ -36,7 +36,7 @@ test.describe('default view', () => {
txMock.withTokenTransfer, txMock.withTokenTransfer,
]), ]),
})); }));
await page.route(buildApiUrl('homepage_chart_txs'), (route) => route.fulfill({ await page.route(buildApiUrl('stats_charts_txs'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(dailyTxsMock.base), body: JSON.stringify(dailyTxsMock.base),
})); }));
...@@ -104,7 +104,7 @@ test.describe('mobile', () => { ...@@ -104,7 +104,7 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount, page }) => { test('base view', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({ await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(statsMock.base), body: JSON.stringify(statsMock.base),
})); }));
...@@ -123,7 +123,7 @@ test.describe('mobile', () => { ...@@ -123,7 +123,7 @@ test.describe('mobile', () => {
txMock.withTokenTransfer, txMock.withTokenTransfer,
]), ]),
})); }));
await page.route(buildApiUrl('homepage_chart_txs'), (route) => route.fulfill({ await page.route(buildApiUrl('stats_charts_txs'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(dailyTxsMock.base), body: JSON.stringify(dailyTxsMock.base),
})); }));
......
import { GridItem, chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import { asymp, space } from 'lib/html-entities';
import { currencyUnits } from 'lib/units';
interface Props {
name: string;
info: GasPriceInfo | null;
}
const GasInfoRow = ({ name, info }: Props) => {
const content = (() => {
if (!info || info.price === null) {
return 'N/A';
}
return (
<>
<span>{ info.fiat_price ? `$${ info.fiat_price }` : `${ info.price } ${ currencyUnits.gwei }` }</span>
{ info.time && (
<chakra.span color="text_secondary">
{ space }per tx { asymp } { (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s
</chakra.span>
) }
</>
);
})();
return (
<>
<GridItem color="blue.100">{ name }</GridItem>
<GridItem color="text" textAlign="right">{ content }</GridItem>
</>
);
};
export default React.memo(GasInfoRow);
import { DarkMode, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import dayjs from 'lib/date/dayjs';
import GasInfoRow from './GasInfoRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';
interface Props {
data: HomeStats;
dataUpdatedAt: number;
}
const GasInfoTooltipContent = ({ data, dataUpdatedAt }: Props) => {
if (!data.gas_prices) {
return null;
}
return (
<DarkMode>
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs" lineHeight={ 4 }>
{ data.gas_price_updated_at && (
<>
<GridItem color="text_secondary">Last update</GridItem>
<GridItem color="text_secondary" display="flex" justifyContent="flex-end" columnGap={ 2 }>
{ dayjs(data.gas_price_updated_at).format('MMM DD, HH:mm:ss') }
{ data.gas_prices_update_in !== 0 &&
<GasInfoUpdateTimer key={ dataUpdatedAt } startTime={ dataUpdatedAt } duration={ data.gas_prices_update_in }/> }
</GridItem>
</>
) }
<GasInfoRow name="Slow" info={ data.gas_prices.slow }/>
<GasInfoRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoRow name="Fast" info={ data.gas_prices.fast }/>
</Grid>
</DarkMode>
);
};
export default React.memo(GasInfoTooltipContent);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { SECOND } from 'lib/consts';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import GasInfoTooltip from './GasInfoTooltip';
import GasPrice from './GasPrice';
const dataUpdatedAt = Date.now() - 30 * SECOND;
test.use({ viewport: { width: 300, height: 300 } });
test('all data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.base } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas <GasPrice data={ statsMock.base.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('without primary unit price', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutFiatPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutFiatPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('without secondary unit price', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutGweiPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutGweiPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
test('no data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutBothPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutBothPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
const oneUnitTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_GAS_TRACKER_UNITS', value: '["gwei"]' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
oneUnitTest.describe('one unit', () => {
oneUnitTest('with data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutFiatPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutFiatPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
oneUnitTest('without data', async({ mount, page }) => {
await mount(
<TestApp>
<GasInfoTooltip data={ statsMock.withoutGweiPrices } dataUpdatedAt={ dataUpdatedAt } isOpen>
<span>Gas: <GasPrice data={ statsMock.withoutGweiPrices.gas_prices.average }/></span>
</GasInfoTooltip>
</TestApp>,
);
// await page.getByText(/gas/i).hover();
await page.getByText(/last update/i).isVisible();
await expect(page).toHaveScreenshot();
});
});
import { Box, DarkMode, Flex, Grid, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import LinkInternal from 'ui/shared/LinkInternal';
import GasInfoTooltipRow from './GasInfoTooltipRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';
interface Props {
children: React.ReactNode;
data: HomeStats;
dataUpdatedAt: number;
isOpen?: boolean; // for testing purposes only; the tests were flaky, i couldn't find a better way
}
const POPOVER_OFFSET: [ number, number ] = [ 0, 10 ];
const feature = config.features.gasTracker;
const GasInfoTooltip = ({ children, data, dataUpdatedAt, isOpen }: Props) => {
const tooltipBg = useColorModeValue('gray.700', 'gray.900');
if (!data.gas_prices) {
return null;
}
const columnNum =
Object.values(data.gas_prices).some((price) => price?.fiat_price) &&
Object.values(data.gas_prices).some((price) => price?.price) &&
feature.isEnabled && feature.units.length === 2 ?
3 : 2;
return (
<Popover trigger="hover" isLazy offset={ POPOVER_OFFSET } isOpen={ isOpen }>
<PopoverTrigger>
{ children }
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ tooltipBg } w="auto">
<PopoverBody color="white">
<DarkMode>
<Flex flexDir="column" fontSize="xs" lineHeight={ 4 } rowGap={ 3 }>
{ data.gas_price_updated_at && (
<Flex justifyContent="space-between">
<Box color="text_secondary">Last update</Box>
<Flex color="text_secondary" justifyContent="flex-end" columnGap={ 2 } ml={ 3 }>
{ dayjs(data.gas_price_updated_at).format('MMM DD, HH:mm:ss') }
{ data.gas_prices_update_in !== 0 &&
<GasInfoUpdateTimer key={ dataUpdatedAt } startTime={ dataUpdatedAt } duration={ data.gas_prices_update_in }/> }
</Flex>
</Flex>
) }
<Grid rowGap={ 2 } columnGap="10px" gridTemplateColumns={ `repeat(${ columnNum }, minmax(min-content, auto))` }>
<GasInfoTooltipRow name="Fast" info={ data.gas_prices.fast }/>
<GasInfoTooltipRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoTooltipRow name="Slow" info={ data.gas_prices.slow }/>
</Grid>
<LinkInternal href={ route({ pathname: '/gas-tracker' }) }>
Gas tracker overview
</LinkInternal>
</Flex>
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
export default React.memo(GasInfoTooltip);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import { space } from 'lib/html-entities';
import GasPrice from 'ui/shared/gas/GasPrice';
interface Props {
name: string;
info: GasPriceInfo | null;
}
const GasInfoTooltipRow = ({ name, info }: Props) => {
return (
<>
<Box>
<chakra.span>{ name }</chakra.span>
{ info && info.time && (
<chakra.span color="text_secondary">
{ space }{ (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s
</chakra.span>
) }
</Box>
<GasPrice data={ info } textAlign="right"/>
<GasPrice data={ info } unitMode="secondary" color="text_secondary" textAlign="right"/>
</>
);
};
export default React.memo(GasInfoTooltipRow);
import { CircularProgress } from '@chakra-ui/react'; import { CircularProgress, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
...@@ -6,6 +6,8 @@ import dayjs from 'lib/date/dayjs'; ...@@ -6,6 +6,8 @@ import dayjs from 'lib/date/dayjs';
interface Props { interface Props {
startTime: number; startTime: number;
duration: number; duration: number;
className?: string;
size?: number;
} }
const getValue = (startDate: dayjs.Dayjs, duration: number) => { const getValue = (startDate: dayjs.Dayjs, duration: number) => {
...@@ -20,8 +22,9 @@ const getValue = (startDate: dayjs.Dayjs, duration: number) => { ...@@ -20,8 +22,9 @@ const getValue = (startDate: dayjs.Dayjs, duration: number) => {
return value; return value;
}; };
const GasInfoUpdateTimer = ({ startTime, duration }: Props) => { const GasInfoUpdateTimer = ({ startTime, duration, className, size = 4 }: Props) => {
const [ value, setValue ] = React.useState(getValue(dayjs(startTime), duration)); const [ value, setValue ] = React.useState(getValue(dayjs(startTime), duration));
const trackColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
React.useEffect(() => { React.useEffect(() => {
const startDate = dayjs(startTime); const startDate = dayjs(startTime);
...@@ -39,7 +42,7 @@ const GasInfoUpdateTimer = ({ startTime, duration }: Props) => { ...@@ -39,7 +42,7 @@ const GasInfoUpdateTimer = ({ startTime, duration }: Props) => {
}; };
}, [ startTime, duration ]); }, [ startTime, duration ]);
return <CircularProgress value={ value } trackColor="whiteAlpha.100" size={ 4 }/>; return <CircularProgress className={ className } value={ value } trackColor={ trackColor } size={ size }/>;
}; };
export default React.memo(GasInfoUpdateTimer); export default React.memo(chakra(GasInfoUpdateTimer));
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { GasPriceInfo } from 'types/api/stats';
import type { GasUnit } from 'types/client/gasTracker';
import config from 'configs/app';
import formatGasValue from './formatGasValue';
const feature = config.features.gasTracker;
const UNITS_TO_API_FIELD_MAP: Record<GasUnit, 'price' | 'fiat_price'> = {
gwei: 'price',
usd: 'fiat_price',
};
interface Props {
data: GasPriceInfo | null;
className?: string;
unitMode?: 'primary' | 'secondary';
prefix?: string;
}
const GasPrice = ({ data, prefix, className, unitMode = 'primary' }: Props) => {
if (!data || !feature.isEnabled) {
return null;
}
switch (unitMode) {
case 'secondary': {
const primaryUnits = feature.units[0];
const secondaryUnits = feature.units[1];
if (!secondaryUnits) {
return null;
}
const primaryUnitsValue = data[UNITS_TO_API_FIELD_MAP[primaryUnits]];
if (!primaryUnitsValue) {
// in this case we display values in secondary untis in primary mode as fallback
return null;
}
const secondaryUnitsValue = data[UNITS_TO_API_FIELD_MAP[secondaryUnits]];
if (!secondaryUnitsValue) {
return null;
}
const formattedValue = formatGasValue(data, secondaryUnits);
return <span className={ className }>{ prefix }{ formattedValue }</span>;
}
case 'primary': {
const primaryUnits = feature.units[0];
const secondaryUnits = feature.units[1];
if (!primaryUnits) {
// this should never happen since feature will be disabled if there are no units at all
return null;
}
const value = data[UNITS_TO_API_FIELD_MAP[primaryUnits]];
if (!value) {
// in primary mode we want to fallback to secondary units if value in primary units are not available
// unless there are no secondary units
const valueInSecondaryUnits = data[UNITS_TO_API_FIELD_MAP[secondaryUnits]];
if (!secondaryUnits || !valueInSecondaryUnits) {
// in primary mode we always want to show something
// this will return "N/A <units>"
return <span className={ className }>{ formatGasValue(data, primaryUnits) }</span>;
} else {
return <span className={ className }>{ prefix }{ formatGasValue(data, secondaryUnits) }</span>;
}
}
return <span className={ className }>{ prefix }{ formatGasValue(data, primaryUnits) }</span>;
}
}
};
export default chakra(GasPrice);
import type { GasPriceInfo } from 'types/api/stats';
import type { GasUnit } from 'types/client/gasTracker';
import { currencyUnits } from 'lib/units';
export default function formatGasValue(data: GasPriceInfo, unit: GasUnit) {
switch (unit) {
case 'gwei': {
if (!data.price) {
return `N/A ${ currencyUnits.gwei }`;
}
return `${ Number(data.price).toLocaleString(undefined, { maximumFractionDigits: 1 }) } ${ currencyUnits.gwei }`;
}
case 'usd': {
if (!data.fiat_price) {
return `$N/A`;
}
if (Number(data.fiat_price) < 0.01) {
return `< $0.01`;
}
return `$${ Number(data.fiat_price).toLocaleString(undefined, { maximumFractionDigits: 2 }) }`;
}
}
}
...@@ -16,7 +16,7 @@ const test = base.extend({ ...@@ -16,7 +16,7 @@ const test = base.extend({
}); });
test('default view +@dark-mode +@mobile', async({ mount, page }) => { test('default view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({ await page.route(buildApiUrl('stats'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(statsMock.base), body: JSON.stringify(statsMock.base),
})); }));
...@@ -27,7 +27,7 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -27,7 +27,7 @@ test('default view +@dark-mode +@mobile', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await component.getByText(/\$1\.01/).hover(); await component.getByText(/\$1\.39/).click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
await component.getByLabel('color mode switch').click(); await component.getByLabel('color mode switch').click();
......
import { Flex, Link, Skeleton, Tooltip, chakra, useDisclosure } from '@chakra-ui/react'; import { Flex, Link, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { currencyUnits } from 'lib/units';
import { HOMEPAGE_STATS } from 'stubs/stats'; import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
const TopBarStats = () => { const TopBarStats = () => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107 const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('stats', {
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
onToggle();
}, [ onToggle ]);
const { data, isPlaceholderData, isError, refetch, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
placeholderData: HOMEPAGE_STATS, placeholderData: HOMEPAGE_STATS,
refetchOnMount: false, refetchOnMount: false,
...@@ -76,28 +63,15 @@ const TopBarStats = () => { ...@@ -76,28 +63,15 @@ const TopBarStats = () => {
) } ) }
</Flex> </Flex>
) } ) }
{ data?.coin_price && config.UI.homepage.showGasTracker && <TextSeparator color="divider"/> } { data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator color="divider"/> }
{ data?.gas_prices && data.gas_prices.average !== null && config.UI.homepage.showGasTracker && ( { data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
<chakra.span color="text_secondary">Gas </chakra.span> <chakra.span color="text_secondary">Gas </chakra.span>
<Tooltip <GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } >
label={ <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> } <Link>
hasArrow={ false } <GasPrice data={ data.gas_prices.average }/>
borderRadius="md"
offset={ [ 0, 16 ] }
bgColor="blackAlpha.900"
p={ 0 }
isOpen={ isOpen }
>
<Link
_hover={{ textDecoration: 'none', color: 'link_hovered' }}
onClick={ handleClick }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
>
{ data.gas_prices.average.fiat_price ? `$${ data.gas_prices.average.fiat_price }` : `${ data.gas_prices.average.price } ${ currencyUnits.gwei }` }
</Link> </Link>
</Tooltip> </GasInfoTooltip>
</Skeleton> </Skeleton>
) } ) }
</Flex> </Flex>
......
import { chakra } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import type { StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
...@@ -15,13 +16,14 @@ type Props = { ...@@ -15,13 +16,14 @@ type Props = {
interval: StatsIntervalIds; interval: StatsIntervalIds;
onLoadingError: () => void; onLoadingError: () => void;
isPlaceholderData: boolean; isPlaceholderData: boolean;
className?: string;
} }
function formatDate(date: Date) { function formatDate(date: Date) {
return date.toISOString().substring(0, 10); return date.toISOString().substring(0, 10);
} }
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData }: Props) => { const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData, className }: Props) => {
const selectedInterval = STATS_INTERVALS[interval]; const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
...@@ -58,8 +60,9 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -58,8 +60,9 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
description={ description } description={ description }
isLoading={ isPending } isLoading={ isPending }
minH="230px" minH="230px"
className={ className }
/> />
); );
}; };
export default ChartWidgetContainer; export default chakra(ChartWidgetContainer);
import type { TooltipProps } from '@chakra-ui/react';
import { Box, Grid, Heading, List, ListItem, Skeleton } from '@chakra-ui/react'; import { Box, Grid, Heading, List, ListItem, Skeleton } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -8,18 +7,12 @@ import type { StatsIntervalIds } from 'types/client/stats'; ...@@ -8,18 +7,12 @@ import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/shared/EmptySearchResult'; import EmptySearchResult from 'ui/shared/EmptySearchResult';
import GasInfoTooltipContent from 'ui/shared/GasInfoTooltipContent/GasInfoTooltipContent'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import Hint from 'ui/shared/Hint'; import IconSvg from 'ui/shared/IconSvg';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert'; import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
import ChartWidgetContainer from './ChartWidgetContainer'; import ChartWidgetContainer from './ChartWidgetContainer';
const GAS_TOOLTIP_PROPS: Partial<TooltipProps> = {
borderRadius: 'md',
hasArrow: false,
padding: 0,
};
type Props = { type Props = {
filterQuery: string; filterQuery: string;
isError: boolean; isError: boolean;
...@@ -33,12 +26,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in ...@@ -33,12 +26,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed; const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
const homeStatsQuery = useApiQuery('homepage_stats', { const homeStatsQuery = useApiQuery('stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: { queryOptions: {
refetchOnMount: false, refetchOnMount: false,
}, },
...@@ -72,15 +60,14 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in ...@@ -72,15 +60,14 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
marginBottom: 0, marginBottom: 0,
}} }}
> >
<Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 }> <Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }>
<Heading size="md" > <Heading size="md" >
{ section.title } { section.title }
</Heading> </Heading>
{ section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && ( { section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && (
<Hint <GasInfoTooltip data={ homeStatsQuery.data } dataUpdatedAt={ homeStatsQuery.dataUpdatedAt }>
label={ <GasInfoTooltipContent data={ homeStatsQuery.data } dataUpdatedAt={ homeStatsQuery.dataUpdatedAt }/> } <IconSvg name="info" boxSize={ 5 } display="block" cursor="pointer" _hover={{ color: 'link_hovered' }}/>
tooltipProps={ GAS_TOOLTIP_PROPS } </GasInfoTooltip>
/>
) } ) }
</Skeleton> </Skeleton>
......
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