Commit 71fd48cd authored by tom goriunov's avatar tom goriunov Committed by GitHub

Home page stats config (#2221)

Fixes #2143
parent a3cdf628
import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation';
import type { ChainIndicatorId, HeroBannerConfig } from 'types/homepage';
import { HOME_STATS_WIDGET_IDS, type ChainIndicatorId, type HeroBannerConfig, type HomeStatsWidgetId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings';
import type { FontFamily } from 'types/ui';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
import * as features from './features';
import * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
......@@ -25,6 +26,22 @@ const hiddenLinks = (() => {
return result;
})();
const homePageStats: Array<HomeStatsWidgetId> = (() => {
const parsedValue = parseEnvJson<Array<HomeStatsWidgetId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_STATS'));
if (!Array.isArray(parsedValue)) {
const rollupFeature = features.rollup;
if (rollupFeature.isEnabled && [ 'zkEvm', 'zkSync', 'arbitrum' ].includes(rollupFeature.type)) {
return [ 'latest_batch', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ];
}
return [ 'total_blocks', 'average_block_time', 'total_txs', 'wallet_addresses', 'gas_tracker' ];
}
return parsedValue.filter((item) => HOME_STATS_WIDGET_IDS.includes(item));
})();
const highlightedRoutes = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
return Array.isArray(parsedValue) ? parsedValue : [];
......@@ -58,12 +75,13 @@ const UI = Object.freeze({
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
stats: homePageStats,
heroBanner: parseEnvJson<HeroBannerConfig>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')),
// !!! DEPRECATED !!!
plate: {
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND'),
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR'),
},
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
},
views,
indexingAlert: {
......
......@@ -37,3 +37,4 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
\ No newline at end of file
......@@ -24,7 +24,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
## sidebar
NEXT_PUBLIC_NETWORK_LOGO=
......
......@@ -25,7 +25,6 @@ NEXT_PUBLIC_API_BASE_PATH=/
# ui config
## homepage
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
## sidebar
## footer
NEXT_PUBLIC_GIT_TAG=v1.0.11
......
......@@ -43,3 +43,4 @@ NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward','nonce']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','btc_locked']
\ No newline at end of file
......@@ -29,8 +29,8 @@ import type { ValidatorsChainType } from '../../../types/client/validators';
import type { WalletType } from '../../../types/client/wallets';
import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig } from '../../../types/homepage';
import { CHAIN_INDICATOR_IDS, HOME_STATS_WIDGET_IDS } from '../../../types/homepage';
import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeStatsWidgetId } from '../../../types/homepage';
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { FontFamily } from '../../../types/ui';
......@@ -567,6 +567,11 @@ const schema = yup
.transform(replaceQuotes)
.json()
.of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS)),
NEXT_PUBLIC_HOMEPAGE_STATS: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string<HomeStatsWidgetId>().oneOf(HOME_STATS_WIDGET_IDS)),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup
......@@ -586,7 +591,6 @@ const schema = yup
const isUndefined = data === undefined;
return isUndefined || heroBannerSchema.isValidSync(data);
}),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
// b. sidebar
NEXT_PUBLIC_FEATURED_NETWORKS: yup
......
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[]
\ No newline at end of file
......@@ -35,10 +35,10 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c18099
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']}
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
......
......@@ -10,3 +10,4 @@
| 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 |
| NEXT_PUBLIC_NETWORK_GOVERNANCE_TOKEN_SYMBOL | `string` | Network governance token symbol | - | - | `GNO` | v1.12.0 | v1.29.0 | Replaced by NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL |
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaces by NEXT_PUBLIC_HOMEPAGE_STATS
\ No newline at end of file
......@@ -118,9 +118,9 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.x+ |
| 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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `white` | `\#DCFE76` | v1.0.x+ |
| 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). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` 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)` | v1.1.0+ |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ |
#### Hero banner configuration properties
......
export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const;
export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number];
export const HOME_STATS_WIDGET_IDS = [
'latest_batch',
'total_blocks',
'average_block_time',
'total_txs',
'latest_l1_state_batch',
'wallet_addresses',
'gas_tracker',
'btc_locked',
'current_epoch',
] as const;
export type HomeStatsWidgetId = typeof HOME_STATS_WIDGET_IDS[number];
export interface HeroBannerButtonState {
background?: Array<string | undefined>;
text_color?: Array<string | undefined>;
......
......@@ -9,7 +9,10 @@ import Stats from './Stats';
test.describe('all items', () => {
let component: Locator;
test.beforeEach(async({ render, mockApiResponse }) => {
test.beforeEach(async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_blocks","average_block_time","total_txs","wallet_addresses","gas_tracker","btc_locked"]' ],
]);
await mockApiResponse('stats', statsMock.withBtcLocked);
component = await render(<Stats/>);
});
......@@ -28,7 +31,7 @@ test('no gas info', async({ render, mockApiResponse }) => {
test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', 'false' ],
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","gas_tracker","wallet_addresses","total_blocks"]' ],
]);
await mockApiResponse('stats', statsMock.base);
const component = await render(<Stats/>);
......@@ -37,8 +40,7 @@ test('4 items default view +@mobile -@default', async({ render, mockApiResponse,
test('3 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME', 'false' ],
[ 'NEXT_PUBLIC_GAS_TRACKER_ENABLED', 'false' ],
[ 'NEXT_PUBLIC_HOMEPAGE_STATS', '["total_txs","wallet_addresses","total_blocks"]' ],
]);
await mockApiResponse('stats', statsMock.base);
const component = await render(<Stats/>);
......
......@@ -2,6 +2,8 @@ import { Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { HomeStatsWidgetId } from 'types/homepage';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
......@@ -12,7 +14,6 @@ import IconSvg from 'ui/shared/IconSvg';
import type { Props as StatsWidgetProps } from 'ui/shared/stats/StatsWidget';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
const Stats = () => {
......@@ -35,37 +36,54 @@ const Stats = () => {
const zkEvmLatestBatchQuery = useApiQuery('homepage_zkevm_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && config.UI.homepage.stats.includes('latest_batch'),
},
});
const zkSyncLatestBatchQuery = useApiQuery('homepage_zksync_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkSync',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && config.UI.homepage.stats.includes('latest_batch'),
},
});
const arbitrumLatestBatchQuery = useApiQuery('homepage_arbitrum_latest_batch', {
queryOptions: {
placeholderData: 12345,
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum',
enabled: rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && config.UI.homepage.stats.includes('latest_batch'),
},
});
if (isError || zkEvmLatestBatchQuery.isError || zkSyncLatestBatchQuery.isError || arbitrumLatestBatchQuery.isError) {
const latestBatchQuery = (() => {
if (!rollupFeature.isEnabled || !config.UI.homepage.stats.includes('latest_batch')) {
return;
}
switch (rollupFeature.type) {
case 'zkEvm':
return zkEvmLatestBatchQuery;
case 'zkSync':
return zkSyncLatestBatchQuery;
case 'arbitrum':
return arbitrumLatestBatchQuery;
}
})();
if (isError || latestBatchQuery?.isError) {
return null;
}
const isLoading = isPlaceholderData ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && zkEvmLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && zkSyncLatestBatchQuery.isPlaceholderData) ||
(rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && arbitrumLatestBatchQuery.isPlaceholderData);
const isLoading = isPlaceholderData || latestBatchQuery?.isPlaceholderData;
interface Item extends StatsWidgetProps {
id: HomeStatsWidgetId;
}
const content = (() => {
const items: Array<Item> = (() => {
if (!data) {
return null;
return [];
}
const gasInfoTooltip = hasGasTracker && data.gas_prices && data.gas_prices.average ? (
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt }>
<IconSvg
......@@ -80,41 +98,40 @@ const Stats = () => {
</GasInfoTooltip>
) : null;
const hasBatches = rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync' || rollupFeature.type === 'arbitrum');
const latestBatch =
(hasBatches && rollupFeature.type === 'zkEvm' ? zkEvmLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'zkSync' ? zkSyncLatestBatchQuery.data : null) ||
(hasBatches && rollupFeature.type === 'arbitrum' ? arbitrumLatestBatchQuery.data : null) || 0;
const items: Array<StatsWidgetProps> = [
hasBatches && {
return [
latestBatchQuery?.data !== undefined && {
id: 'latest_batch' as const,
icon: 'txn_batches_slim' as const,
label: 'Latest batch',
value: latestBatch.toLocaleString(),
value: latestBatchQuery.data.toLocaleString(),
href: { pathname: '/batches' as const },
isLoading,
},
!hasBatches && {
{
id: 'total_blocks' as const,
icon: 'block_slim' as const,
label: 'Total blocks',
value: Number(data.total_blocks).toLocaleString(),
href: { pathname: '/blocks' as const },
isLoading,
},
hasAvgBlockTime && {
{
id: 'average_block_time' as const,
icon: 'clock-light' as const,
label: 'Average block time',
value: `${ (data.average_block_time / 1000).toFixed(1) }s`,
isLoading,
},
{
id: 'total_txs' as const,
icon: 'transactions_slim' as const,
label: 'Total transactions',
value: Number(data.total_transactions).toLocaleString(),
href: { pathname: '/txs' as const },
isLoading,
},
rollupFeature.isEnabled && data.last_output_root_size && {
data.last_output_root_size && {
id: 'latest_l1_state_batch' as const,
icon: 'txn_batches_slim' as const,
label: 'Latest L1 state batch',
value: data.last_output_root_size,
......@@ -122,12 +139,14 @@ const Stats = () => {
isLoading,
},
{
id: 'wallet_addresses' as const,
icon: 'wallet' as const,
label: 'Wallet addresses',
value: Number(data.total_addresses).toLocaleString(),
isLoading,
},
hasGasTracker && data.gas_prices && {
id: 'gas_tracker' as const,
icon: 'gas' as const,
label: 'Gas tracker',
value: data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A',
......@@ -135,33 +154,39 @@ const Stats = () => {
isLoading,
},
data.rootstock_locked_btc && {
id: 'btc_locked' as const,
icon: 'coins/bitcoin' as const,
label: 'BTC Locked in 2WP',
value: `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC`,
isLoading,
},
data.celo && {
id: 'current_epoch' as const,
icon: 'hourglass' as const,
label: 'Current epoch',
value: `#${ data.celo.epoch_number }`,
isLoading,
},
].filter(Boolean);
return (
<>
{ items.map((item, index) => (
<StatsWidget
key={ item.icon }
{ ...item }
isLoading={ isLoading }
_last={ items.length % 2 === 1 && index === items.length - 1 ? { gridColumn: 'span 2' } : undefined }/>
),
) }
</>
);
]
.filter(Boolean)
.filter(({ id }) => config.UI.homepage.stats.includes(id))
.sort((a, b) => {
const indexA = config.UI.homepage.stats.indexOf(a.id);
const indexB = config.UI.homepage.stats.indexOf(b.id);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
});
})();
if (items.length === 0) {
return null;
}
return (
<Grid
gridTemplateColumns="1fr 1fr"
......@@ -169,7 +194,14 @@ const Stats = () => {
flexBasis="50%"
flexGrow={ 1 }
>
{ content }
{ items.map((item, index) => (
<StatsWidget
key={ item.id }
{ ...item }
isLoading={ isLoading }
_last={ items.length % 2 === 1 && index === items.length - 1 ? { gridColumn: 'span 2' } : undefined }/>
),
) }
</Grid>
);
......
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