Commit 9869cdb4 authored by isstuev's avatar isstuev

stats updates

parent 53697bb8
......@@ -14,7 +14,7 @@ NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
......@@ -59,7 +59,7 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_SENTRY_ENABLE_TRACING=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s.blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=noves
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
......@@ -20,7 +20,7 @@ export default function generate<Pathname extends Route['pathname']>(route: Rout
};
const title = compileValue(templates.title.make(route.pathname, Boolean(apiData)), params);
const description = compileValue(templates.description.make(route.pathname), params);
const description = compileValue(templates.description.make(route.pathname, Boolean(apiData)), params);
const pageOgType = getPageOgType(route.pathname);
......
......@@ -23,6 +23,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/apps': 'Root page',
'/apps/[id]': 'Regular page',
'/stats': 'Root page',
'/stats/[id]': 'Regular page',
'/api-docs': 'Regular page',
'/graphiql': 'Regular page',
'/search-results': 'Regular page',
......
......@@ -27,6 +27,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/apps': DEFAULT_TEMPLATE,
'/apps/[id]': DEFAULT_TEMPLATE,
'/stats': DEFAULT_TEMPLATE,
'/stats/[id]': DEFAULT_TEMPLATE,
'/api-docs': DEFAULT_TEMPLATE,
'/graphiql': DEFAULT_TEMPLATE,
'/search-results': DEFAULT_TEMPLATE,
......@@ -71,8 +72,10 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/auth/unverified-email': DEFAULT_TEMPLATE,
};
export function make(pathname: Route['pathname']) {
const template = TEMPLATE_MAP[pathname];
const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
'/stats/[id]': '%description%',
};
return template ?? '';
export function make(pathname: Route['pathname'], isEnriched = false) {
return (isEnriched ? TEMPLATE_MAP_ENHANCED[pathname] : undefined) ?? TEMPLATE_MAP[pathname] ?? '';
}
......@@ -23,6 +23,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/apps': '%network_name% DApps - Explore top apps',
'/apps/[id]': '%network_name% marketplace app',
'/stats': '%network_name% stats - %network_name% network insights',
'/stats/[id]': '%network_name% stats - %id% chart',
'/api-docs': '%network_name% API docs - %network_name% developer tools',
'/graphiql': 'GraphQL for %network_name% - %network_name% data query',
'/search-results': '%network_name% search result for %q%',
......@@ -72,6 +73,7 @@ const TEMPLATE_MAP_ENHANCED: Partial<Record<Route['pathname'], string>> = {
'/token/[hash]/instance/[id]': '%network_name% token instance for %symbol%',
'/apps/[id]': '%network_name% - %app_name%',
'/address/[hash]': '%network_name% address details for %domain_name%',
'/stats/[id]': '%title% chart on %network_name%',
};
export function make(pathname: Route['pathname'], isEnriched = false) {
......
import type { LineChart } from '@blockscout/stats-types';
import type { TokenInfo } from 'types/api/token';
import type { Route } from 'nextjs-routes';
......@@ -9,6 +10,7 @@ export type ApiData<Pathname extends Route['pathname']> =
Pathname extends '/token/[hash]' ? TokenInfo :
Pathname extends '/token/[hash]/instance/[id]' ? { symbol: string } :
Pathname extends '/apps/[id]' ? { app_name: string } :
Pathname extends '/stats/[id]' ? LineChart['info'] :
never
) | null;
......
......@@ -21,6 +21,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/apps': 'DApps',
'/apps/[id]': 'DApp',
'/stats': 'Stats',
'/stats/[id]': 'Stats chart',
'/api-docs': 'REST API',
'/graphiql': 'GraphQL',
'/search-results': 'Search results',
......
......@@ -116,6 +116,9 @@ Type extends EventTypes.PAGE_WIDGET ? (
'Type': 'Address tag';
'Info': string;
'URL': string;
} | {
'Type': 'Share chart';
'Info': string;
}
) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
......
......@@ -4,158 +4,195 @@ export const averageGasPrice: stats.LineChart = {
chart: [
{
date: '2023-12-22',
date_to: '2023-12-22',
value: '37.7804422597599',
is_approximate: false,
},
{
date: '2023-12-23',
date_to: '2023-12-23',
value: '25.84889883009387',
is_approximate: false,
},
{
date: '2023-12-24',
date_to: '2023-12-24',
value: '25.818463227198574',
is_approximate: false,
},
{
date: '2023-12-25',
date_to: '2023-12-25',
value: '26.045513050051298',
is_approximate: false,
},
{
date: '2023-12-26',
date_to: '2023-12-26',
value: '21.42600692652399',
is_approximate: false,
},
{
date: '2023-12-27',
date_to: '2023-12-27',
value: '31.066730409846656',
is_approximate: false,
},
{
date: '2023-12-28',
date_to: '2023-12-28',
value: '33.63955781902089',
is_approximate: false,
},
{
date: '2023-12-29',
date_to: '2023-12-29',
value: '28.064736756058384',
is_approximate: false,
},
{
date: '2023-12-30',
date_to: '2023-12-30',
value: '23.074500869678175',
is_approximate: false,
},
{
date: '2023-12-31',
date_to: '2023-12-31',
value: '17.651005734615133',
is_approximate: false,
},
{
date: '2024-01-01',
date_to: '2023-01-01',
value: '14.906085174476441',
is_approximate: false,
},
{
date: '2024-01-02',
date_to: '2023-01-02',
value: '22.28459059038656',
is_approximate: false,
},
{
date: '2024-01-03',
date_to: '2023-01-03',
value: '39.8311646806592',
is_approximate: false,
},
{
date: '2024-01-04',
date_to: '2023-01-04',
value: '26.09989322256083',
is_approximate: false,
},
{
date: '2024-01-05',
date_to: '2023-01-05',
value: '22.821996688111998',
is_approximate: false,
},
{
date: '2024-01-06',
date_to: '2023-01-06',
value: '20.32680041262083',
is_approximate: false,
},
{
date: '2024-01-07',
date_to: '2023-01-07',
value: '32.535045831809704',
is_approximate: false,
},
{
date: '2024-01-08',
date_to: '2023-01-08',
value: '27.443477102139482',
is_approximate: false,
},
{
date: '2024-01-09',
date_to: '2023-01-09',
value: '20.7911332558055',
is_approximate: false,
},
{
date: '2024-01-10',
date_to: '2023-01-10',
value: '42.10740192523919',
is_approximate: false,
},
{
date: '2024-01-11',
date_to: '2023-01-11',
value: '35.75215680343582',
is_approximate: false,
},
{
date: '2024-01-12',
date_to: '2023-01-12',
value: '27.430414798093253',
is_approximate: false,
},
{
date: '2024-01-13',
date_to: '2023-01-13',
value: '20.170934096589875',
is_approximate: false,
},
{
date: '2024-01-14',
date_to: '2023-01-14',
value: '38.79660984371034',
is_approximate: false,
},
{
date: '2024-01-15',
date_to: '2023-01-15',
value: '26.140740484554204',
is_approximate: false,
},
{
date: '2024-01-16',
date_to: '2023-01-16',
value: '36.708543184194156',
is_approximate: false,
},
{
date: '2024-01-17',
date_to: '2023-01-17',
value: '40.325438794298876',
is_approximate: false,
},
{
date: '2024-01-18',
date_to: '2023-01-18',
value: '37.55145309930694',
is_approximate: false,
},
{
date: '2024-01-19',
date_to: '2023-01-19',
value: '33.271450114434664',
is_approximate: false,
},
{
date: '2024-01-20',
date_to: '2023-01-20',
value: '19.303304377685638',
is_approximate: false,
},
{
date: '2024-01-21',
date_to: '2023-01-21',
value: '14.375908594704976',
is_approximate: false,
},
],
info: {
title: 'Chart title',
description: 'Chert description',
id: 'chart',
resolutions: [ 'DAY', 'MONTH' ],
},
};
......@@ -11,18 +11,21 @@ export const base: stats.LineCharts = {
title: 'Accounts growth',
description: 'Cumulative accounts number per period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'activeAccounts',
title: 'Active accounts',
description: 'Active accounts number per period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'newAccounts',
title: 'New accounts',
description: 'New accounts number per day',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......@@ -35,30 +38,35 @@ export const base: stats.LineCharts = {
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'newTxns',
title: 'New transactions',
description: 'New transactions number',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'txnsFee',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'txnsGrowth',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'txnsSuccessRate',
title: 'Transactions success rate',
description: 'Successful transactions rate per day',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......@@ -71,18 +79,21 @@ export const base: stats.LineCharts = {
title: 'Average block rewards',
description: 'Average amount of distributed reward in tokens per day',
units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'averageBlockSize',
title: 'Average block size',
description: 'Average size of blocks in bytes',
units: 'Bytes',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'newBlocks',
title: 'New blocks',
description: 'New blocks number',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......@@ -95,6 +106,7 @@ export const base: stats.LineCharts = {
title: 'New ETH transfers',
description: 'New token transfers number for the period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......@@ -107,18 +119,21 @@ export const base: stats.LineCharts = {
title: 'Average gas limit',
description: 'Average gas limit per block for the period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'averageGasPrice',
title: 'Average gas price',
description: 'Average gas price for the period (Gwei)',
units: 'Gwei',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'gasUsedGrowth',
title: 'Gas used growth',
description: 'Cumulative gas used for the period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......@@ -131,12 +146,14 @@ export const base: stats.LineCharts = {
title: 'New verified contracts',
description: 'New verified contracts number for the period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'verifiedContractsGrowth',
title: 'Verified contracts growth',
description: 'Cumulative number verified contracts for the period',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
},
......
......@@ -55,6 +55,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/public-tags/submit">
| StaticRoute<"/search-results">
| StaticRoute<"/sprite">
| DynamicRoute<"/stats/[id]", { "id": string }>
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
......
......@@ -12,6 +12,7 @@ type Params<R extends ResourceName> = (
{
resource: R;
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | number | undefined>;
} | {
url: string;
route: string;
......@@ -22,12 +23,11 @@ type Params<R extends ResourceName> = (
export default async function fetchApi<R extends ResourceName = never, S = ResourcePayload<R>>(params: Params<R>): Promise<S | undefined> {
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, params.timeout || SECOND);
const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams);
const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams, params.queryParams);
const route = 'route' in params ? params.route : RESOURCES[params.resource]['path'];
const end = metrics?.apiRequestDuration.startTimer();
......
import type { GetServerSideProps, NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Route } from 'nextjs-routes';
import * as gSSP from 'nextjs/getServerSideProps';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
import detectBotRequest from 'nextjs/utils/detectBotRequest';
import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import getQueryParamString from 'lib/router/getQueryParamString';
const Chart = dynamic(() => import('ui/pages/Chart'), { ssr: false });
const pathname: Route['pathname'] = '/stats/[id]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return (
<PageNextJs pathname="/stats/[id]" query={ props.query } apiData={ props.apiData }>
<Chart/>
</PageNextJs>
);
};
export default Page;
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
const baseResponse = await gSSP.base<typeof pathname>(ctx);
if ('props' in baseResponse) {
if (
config.meta.seo.enhancedDataEnabled ||
(config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const chartData = await fetchApi({
resource: 'stats_line',
pathParams: { id: getQueryParamString(ctx.query.id) },
queryParams: { from: dayjs().format('YYYY-MM-DD'), to: dayjs().format('YYYY-MM-DD') },
timeout: 1000,
});
(await baseResponse.props).apiData = chartData?.info ?? null;
}
}
return baseResponse;
};
<svg viewBox="0 0 114 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.71 1a1 1 0 0 0-1-1H4.52a1 1 0 0 0-1 1v2.167a1 1 0 0 1-1 1H1a1 1 0 0 0-1 1V19a1 1 0 0 0 1 1h2.188a1 1 0 0 0 1-1V5.167a1 1 0 0 1 1-1H6.71a1 1 0 0 0 1-1V1ZM16.114 1a1 1 0 0 0-1-1h-2.188a1 1 0 0 0-1 1v2.167a1 1 0 0 0 1 1h1.376a1 1 0 0 1 1 1V19a1 1 0 0 0 1 1h2.188a1 1 0 0 0 1-1V5.167a1 1 0 0 0-1-1h-1.376a1 1 0 0 1-1-1V1ZM11.868 8.976a1 1 0 0 0-1-1H8.68a1 1 0 0 0-1 1v6.095a1 1 0 0 0 1 1h2.188a1 1 0 0 0 1-1V8.976ZM27.702 4.533h4.948c2.096 0 3.23 1.103 3.23 2.69a2.468 2.468 0 0 1-1.495 2.344v.051a2.81 2.81 0 0 1 1.464 1.05c.36.501.546 1.107.529 1.725 0 1.774-1.227 3.17-3.453 3.17h-5.223V4.533Zm4.982 4.482c.79 0 1.357-.585 1.357-1.476 0-.892-.567-1.477-1.357-1.477h-3.139v2.953h3.14Zm.224 5.032c.944 0 1.631-.724 1.631-1.775s-.687-1.758-1.632-1.758h-3.362v3.533h3.362ZM38.68 5.845h-1.036V4.181h2.852v11.375h-1.821l.005-9.711ZM42.182 11.48c0-2.499 1.84-4.343 4.383-4.343 2.542 0 4.398 1.827 4.398 4.342 0 2.516-1.839 4.325-4.398 4.325-2.56 0-4.383-1.826-4.383-4.325Zm3.902 2.687h.981c1.117 0 2.096-1.189 2.096-2.688s-.982-2.707-2.096-2.707h-.981c-1.117 0-2.096 1.23-2.096 2.707 0 1.477.98 2.688 2.096 2.688ZM52.145 11.462c0-2.55 1.755-4.325 4.28-4.325 2.028 0 3.625 1.051 3.883 3.2h-1.77c-.171-1.085-.944-1.568-1.735-1.568h-.859c-1.1 0-1.992 1.206-1.992 2.708 0 1.501.893 2.722 1.992 2.722h.86a1.75 1.75 0 0 0 1.752-1.602h1.769c-.245 2.12-1.855 3.222-3.927 3.222-2.518-.015-4.253-1.807-4.253-4.357ZM61.955 4.181h1.803v6.274h1.308l2.44-3.084h2.061l-3.006 3.808 3.144 4.377H67.54l-2.628-3.619h-1.154v3.619h-1.803V4.18ZM70.53 13.1h1.734c.086.723.533 1.188 1.473 1.188h1.136c.807 0 1.185-.414 1.185-.965 0-.551-.309-.879-1.048-.985l-1.909-.246c-1.615-.19-2.336-1.154-2.336-2.412 0-1.654 1.227-2.55 3.418-2.55 2.099 0 3.37.844 3.436 2.722h-1.735c-.086-.706-.378-1.206-1.271-1.206H73.58c-.773 0-1.134.414-1.134.948s.343.896 1.082 1l1.927.245c1.563.19 2.284 1.034 2.284 2.344 0 1.688-1.116 2.619-3.59 2.619-2.393.002-3.554-.911-3.62-2.703ZM78.873 11.462c0-2.55 1.752-4.325 4.28-4.325 2.027 0 3.625 1.051 3.883 3.2h-1.77c-.172-1.085-.945-1.568-1.735-1.568h-.859c-1.102 0-1.995 1.206-1.995 2.708 0 1.501.893 2.722 1.995 2.722h.86a1.75 1.75 0 0 0 1.751-1.602h1.77c-.246 2.12-1.856 3.222-3.927 3.222-2.515-.015-4.253-1.807-4.253-4.357ZM88.199 11.48c0-2.499 1.838-4.343 4.38-4.343 2.543 0 4.4 1.827 4.4 4.342 0 2.516-1.838 4.325-4.4 4.325-2.562 0-4.38-1.826-4.38-4.325Zm3.9 2.687h.981c1.117 0 2.098-1.189 2.098-2.688s-.981-2.707-2.098-2.707h-.982c-1.116 0-2.095 1.23-2.095 2.707 0 1.477.984 2.688 2.1 2.688h-.005ZM98.477 12.53V7.381h1.803v4.842c0 1.189.602 1.895 1.392 1.895h.982c.841 0 1.597-.775 1.597-1.895V7.38h1.806v8.184h-1.754v-1.12c-.491.845-1.357 1.362-2.68 1.362-1.978-.003-3.146-1.157-3.146-3.277ZM109.059 13.65V8.974h-1.581V7.38h1.581V5.087h1.821V7.38h2.13v1.585h-2.13v4.17c0 .534.172.793.756.793h1.443v1.636h-1.964c-1.32-.002-2.056-.691-2.056-1.915Z" fill="#2B6CB0"/>
</svg>
......@@ -51,24 +51,28 @@ export const STATS_CHARTS_SECTION: stats.LineChartSection = {
title: 'Average transaction fee',
description: 'The average amount in ETH spent per transaction',
units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'chart_1',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
units: 'ETH',
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'chart_2',
title: 'New transactions',
description: 'New transactions number',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
{
id: 'chart_3',
title: 'Transactions growth',
description: 'Cumulative transactions number',
units: undefined,
resolutions: [ 'DAY', 'MONTH' ],
},
],
};
......
import { Button, Flex, IconButton, Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Resolution } from '@blockscout/stats-types';
import type { StatsIntervalIds } from 'types/client/stats';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser';
import * as metadata from 'lib/metadata';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import ChartMenu from 'ui/shared/chart/ChartMenu';
import ChartResolutionSelect from 'ui/shared/chart/ChartResolutionSelect';
import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent';
import useChartQuery from 'ui/shared/chart/useChartQuery';
import useZoomReset from 'ui/shared/chart/useZoomReset';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle';
const DEFAULT_RESOLUTION = Resolution.DAY;
const getIntervalByResolution = (resolution: Resolution): StatsIntervalIds => {
switch (resolution) {
case 'DAY':
return 'oneMonth';
case 'WEEK':
return 'oneMonth';
case 'MONTH':
return 'oneYear';
case 'YEAR':
return 'all';
default:
return 'oneMonth';
}
};
const Chart = () => {
const router = useRouter();
const id = getQueryParamString(router.query.id);
const [ intervalState, setIntervalState ] = React.useState<StatsIntervalIds | undefined>();
const [ resolution, setResolution ] = React.useState<Resolution>(DEFAULT_RESOLUTION);
const { isZoomResetInitial, handleZoom, handleZoomReset } = useZoomReset();
const interval = intervalState || getIntervalByResolution(resolution);
const ref = React.useRef(null);
const isMobile = useIsMobile();
const isInBrowser = isBrowser();
const appProps = useAppContext();
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/stats');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to charts list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
const handleReset = React.useCallback(() => {
handleZoomReset();
setResolution(DEFAULT_RESOLUTION);
}, [ handleZoomReset ]);
const { items, info, lineQuery } = useChartQuery(id, resolution, interval);
React.useEffect(() => {
if (info && !config.meta.seo.enhancedDataEnabled) {
metadata.update({ pathname: '/stats/[id]', query: { id } }, info);
}
}, [ info, id ]);
const onShare = React.useCallback(async() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Share chart', Info: id });
try {
await window.navigator.share({
title: info?.title,
text: info?.description,
url: window.location.href,
});
} catch (error) {}
}, [ info, id ]);
if (lineQuery.isError) {
if (isCustomAppError(lineQuery.error)) {
throwOnResourceLoadError({ resource: 'stats_line', error: lineQuery.error, isError: true });
}
}
const hasItems = (items && items.length > 2) || lineQuery.isPending;
const isInfoLoading = !info && lineQuery.isPlaceholderData;
const shareButton = isMobile ? (
<IconButton
aria-label="share"
variant="outline"
boxSize={ 8 }
size="sm"
icon={ <IconSvg name="share" boxSize={ 5 }/> }
onClick={ onShare }
/>
) : (
<Button
leftIcon={ <IconSvg name="share" w={ 4 } h={ 4 }/> }
colorScheme="blue"
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ onShare }
ml={ 6 }
>
Share
</Button>
);
const shareAndMenu = (
<Flex alignItems="center" ml="auto" gap={ 3 }>
{ /* TS thinks window.navigator.share can't be undefined, but it can */ }
{ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ }
{ (isInBrowser && ((window.navigator.share as any) ?
shareButton :
(
<CopyToClipboard
text={ config.app.baseUrl + router.asPath }
size={ 5 }
type="link"
variant="outline"
colorScheme="blue"
display="flex"
borderRadius="8px"
width={ 8 }
height={ 8 }
/>
)
)) }
{ (hasItems || lineQuery.isPlaceholderData) && (
<ChartMenu
items={ items }
title={ info?.title || '' }
isLoading={ lineQuery.isPlaceholderData }
chartRef={ ref }
/>
) }
</Flex>
);
return (
<>
<PageTitle
title={ info?.title || lineQuery.data?.info?.title || '' }
mb={ 3 }
isLoading={ isInfoLoading }
backLink={ backLink }
contentAfter={ isMobile ? shareAndMenu : undefined }
secondRow={ info?.description || lineQuery.data?.info?.description }
// withTextAd
/>
<Flex alignItems="center" justifyContent="space-between">
<Flex alignItems="center" gap={ 3 } maxW="100%" overflow="hidden">
<Text>Period</Text>
<ChartIntervalSelect interval={ interval } onIntervalChange={ setIntervalState }/>
{ lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 2 && (
<>
<Text>{ isMobile ? 'Res.' : 'Resolution' }</Text>
<ChartResolutionSelect
resolution={ resolution }
onResolutionChange={ setResolution }
resolutions={ lineQuery.data?.info?.resolutions || [] }
/>
</>
) }
{ (!isZoomResetInitial || resolution !== 'DAY') && (
isMobile ? (
<IconButton
aria-label="Reset"
variant="ghost"
size="sm"
icon={ <IconSvg name="repeat" boxSize={ 5 }/> }
onClick={ handleReset }
/>
) : (
<Button
leftIcon={ <IconSvg name="repeat" w={ 4 } h={ 4 }/> }
colorScheme="blue"
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleReset }
ml={ 6 }
>
Reset
</Button>
)
) }
</Flex>
{ !isMobile && shareAndMenu }
</Flex>
<Flex
ref={ ref }
flexGrow={ 1 }
h="50vh"
mt={ 3 }
position="relative"
>
<ChartWidgetContent
isError={ lineQuery.isError }
items={ items }
title={ info?.title || '' }
units={ info?.units || undefined }
isEnlarged
isLoading={ lineQuery.isPlaceholderData }
isZoomResetInitial={ isZoomResetInitial }
handleZoom={ handleZoom }
emptyText="No data for the selected resolution & interval."
/>
</Flex>
</>
);
};
export default Chart;
......@@ -22,6 +22,7 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type,
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('gray.400', 'gray.500');
const colorProps = colorScheme ? {} : { color: iconColor };
const iconName = icon || (type === 'link' ? 'link' : 'copy');
useEffect(() => {
......@@ -44,10 +45,10 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type,
return (
<Tooltip label={ copied ? 'Copied' : `Copy${ type === 'link' ? ' link ' : ' ' }to clipboard` } isOpen={ isOpen || copied }>
<IconButton
{ ...colorProps }
aria-label="copy"
icon={ <IconSvg name={ iconName } boxSize={ size }/> }
boxSize={ size }
color={ iconColor }
variant={ variant }
colorScheme={ colorScheme }
display="inline-block"
......
......@@ -154,9 +154,9 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
{ withTextAd && <TextAd order={{ base: -1, lg: 100 }} mb={{ base: 6, lg: 0 }} ml="auto" w={{ base: '100%', lg: 'auto' }}/> }
</Flex>
{ secondRow && (
<Flex alignItems="center" minH={ 10 } overflow="hidden" _empty={{ display: 'none' }}>
<Skeleton isLoaded={ !isLoading } alignItems="center" minH={ 10 } overflow="hidden" display="flex" _empty={{ display: 'none' }}>
{ secondRow }
</Flex>
</Skeleton>
) }
</Flex>
);
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect';
import { STATS_INTERVALS } from 'ui/stats/constants';
import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu';
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
title: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<StatsInterval>;
const intervalListShort = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
title: STATS_INTERVALS[id as StatsIntervalIds].shortTitle,
})) as Array<StatsInterval>;
type Props = {
interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void;
isLoading?: boolean;
}
const ChartIntervalSelect = ({ interval, onIntervalChange, isLoading }: Props) => {
return (
<>
<Skeleton display={{ base: 'none', lg: 'flex' }} borderRadius="base" isLoaded={ !isLoading }>
<TagGroupSelect<StatsIntervalIds> items={ intervalListShort } onChange={ onIntervalChange } value={ interval }/>
</Skeleton>
<Skeleton display={{ base: 'block', lg: 'none' }} borderRadius="base" isLoaded={ !isLoading }>
<StatsDropdownMenu
items={ intervalList }
selectedId={ interval }
onSelect={ onIntervalChange }
/>
</Skeleton>
</>
);
};
export default React.memo(ChartIntervalSelect);
import {
IconButton,
MenuButton,
MenuItem,
MenuList,
Skeleton,
useClipboard,
useColorModeValue,
VisuallyHidden,
} from '@chakra-ui/react';
import domToImage from 'dom-to-image';
import React from 'react';
import type { TimeChartItem } from './types';
import type { Route } from 'nextjs-routes';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import dayjs from 'lib/date/dayjs';
import isBrowser from 'lib/isBrowser';
import saveAsCSV from 'lib/saveAsCSV';
import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
import FullscreenChartModal from './FullscreenChartModal';
export type Props = {
items?: Array<TimeChartItem>;
title: string;
description?: string;
units?: string;
isLoading: boolean;
chartRef: React.RefObject<HTMLDivElement>;
href?: Route;
}
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartMenu = ({ items, title, description, units, isLoading, chartRef, href }: Props) => {
const pngBackgroundColor = useColorModeValue('white', 'black');
const [ isFullscreen, setIsFullscreen ] = React.useState(false);
const chartUrl = href ? config.app.baseUrl + route(href) : '';
const { onCopy } = useClipboard(chartUrl);
const isInBrowser = isBrowser();
const showChartFullscreen = React.useCallback(() => {
setIsFullscreen(true);
}, []);
const clearFullscreenChart = React.useCallback(() => {
setIsFullscreen(false);
}, []);
const handleFileSaveClick = React.useCallback(() => {
// wait for context menu to close
setTimeout(() => {
if (chartRef.current) {
domToImage.toPng(chartRef.current,
{
quality: 100,
bgcolor: pngBackgroundColor,
width: chartRef.current.offsetWidth * DOWNLOAD_IMAGE_SCALE,
height: chartRef.current.offsetHeight * DOWNLOAD_IMAGE_SCALE,
filter: (node) => node.nodeName !== 'BUTTON',
style: {
borderColor: 'transparent',
transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`,
'transform-origin': 'top left',
},
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = `${ title } (Blockscout chart).png`;
link.href = dataUrl;
link.click();
link.remove();
});
}
}, 100);
}, [ pngBackgroundColor, title, chartRef ]);
const handleSVGSavingClick = React.useCallback(() => {
if (items) {
const headerRows = [
'Date', 'Value',
];
const dataRows = items.map((item) => [
dayjs(item.date).format('YYYY-MM-DD'), String(item.value),
]);
saveAsCSV(headerRows, dataRows, `${ title } (Blockscout stats)`);
}
}, [ items, title ]);
// TS thinks window.navigator.share can't be undefined, but it can
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hasShare = isInBrowser && (window.navigator.share as any);
const handleShare = React.useCallback(async() => {
try {
await window.navigator.share({
title: title,
text: description,
url: chartUrl,
});
} catch (error) {}
}, [ title, description, chartUrl ]);
return (
<>
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<MenuButton
w="36px"
h="32px"
icon={ <IconSvg name="dots" boxSize={ 4 } transform="rotate(-90deg)"/> }
colorScheme="gray"
variant="ghost"
as={ IconButton }
>
<VisuallyHidden>
Open chart options menu
</VisuallyHidden>
</MenuButton>
</Skeleton>
<MenuList>
{ href && (
<MenuItem
display="flex"
alignItems="center"
onClick={ hasShare ? handleShare : onCopy }
>
<IconSvg name={ hasShare ? 'share' : 'copy' } boxSize={ 5 } mr={ 3 }/>
{ hasShare ? 'Share' : 'Copy link' }
</MenuItem>
) }
<MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<IconSvg name="scope" boxSize={ 5 } mr={ 3 }/>
View fullscreen
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleFileSaveClick }
>
<IconSvg name="files/image" boxSize={ 5 } mr={ 3 }/>
Save as PNG
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleSVGSavingClick }
>
<IconSvg name="files/csv" boxSize={ 5 } mr={ 3 }/>
Save as CSV
</MenuItem>
</MenuList>
</Menu>
{ items && (
<FullscreenChartModal
isOpen={ isFullscreen }
items={ items }
title={ title }
description={ description }
onClose={ clearFullscreenChart }
units={ units }
/>
) }
</>
);
};
export default ChartMenu;
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { Resolution } from '@blockscout/stats-types';
import { STATS_RESOLUTIONS } from 'ui/stats/constants';
import StatsDropdownMenu from 'ui/stats/StatsDropdownMenu';
type Props = {
resolution: Resolution;
resolutions: Array<string>;
onResolutionChange: (resolution: Resolution) => void;
isLoading?: boolean;
}
const ChartResolutionSelect = ({ resolution, resolutions, onResolutionChange, isLoading }: Props) => {
return (
<Skeleton borderRadius="base" isLoaded={ !isLoading } w={{ base: 'auto', lg: '160px' }}>
<StatsDropdownMenu
items={ STATS_RESOLUTIONS.filter(r => resolutions.includes(r.id)) }
selectedId={ resolution }
onSelect={ onResolutionChange }
/>
</Skeleton>
);
};
export default React.memo(ChartResolutionSelect);
This diff is collapsed.
import { Box, Center, Flex, Link, Skeleton, Text, Image } from '@chakra-ui/react';
import React from 'react';
import type { TimeChartItem } from './types';
import { apos } from 'lib/html-entities';
import ChartWidgetGraph from './ChartWidgetGraph';
export type Props = {
items?: Array<TimeChartItem>;
title: string;
units?: string;
isLoading?: boolean;
isError?: boolean;
emptyText?: string;
handleZoom: () => void;
isZoomResetInitial: boolean;
isEnlarged?: boolean;
noAnimation?: boolean;
}
const ChartWidgetContent = ({
items,
title,
isLoading,
isError,
units,
emptyText,
handleZoom,
isZoomResetInitial,
isEnlarged,
noAnimation,
}: Props) => {
const hasItems = items && items.length > 2;
if (isError) {
return (
<Flex
alignItems="center"
justifyContent="center"
flexGrow={ 1 }
py={ 4 }
>
<Text
variant="secondary"
fontSize="sm"
textAlign="center"
>
{ `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload the page.</Link>
</Text>
</Flex>
);
}
if (isLoading) {
return <Skeleton flexGrow={ 1 } w="100%"/>;
}
if (!hasItems) {
return (
<Center flexGrow={ 1 }>
<Text variant="secondary" fontSize="sm">{ emptyText || 'No data' }</Text>
</Center>
);
}
return (
<Box flexGrow={ 1 } maxW="100%" position="relative" h="100%">
<ChartWidgetGraph
items={ items }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
units={ units }
isEnlarged={ isEnlarged }
noAnimation={ noAnimation }
/>
<Image src="/static/logo.svg"
position="absolute"
alt="blockscout logo"
height="30px"
opacity={ 0.1 }
top="50%"
left="50%"
transform="translate(-50%, -50%)"
color="link"
pointerEvents="none"
/>
</Box>
);
};
export default React.memo(ChartWidgetContent);
......@@ -5,7 +5,7 @@ import type { TimeChartItem } from './types';
import IconSvg from 'ui/shared/IconSvg';
import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetContent from './ChartWidgetContent';
type Props = {
isOpen: boolean;
......@@ -30,7 +30,7 @@ const FullscreenChartModal = ({
setIsZoomResetInitial(false);
}, []);
const handleZoomResetClick = useCallback(() => {
const handleZoomReset = useCallback(() => {
setIsZoomResetInitial(true);
}, []);
......@@ -79,7 +79,7 @@ const FullscreenChartModal = ({
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
onClick={ handleZoomReset }
>
Reset zoom
</Button>
......@@ -91,13 +91,13 @@ const FullscreenChartModal = ({
<ModalBody
h="100%"
margin={{ bottom: 60 }}
>
<ChartWidgetGraph
margin={{ bottom: 60 }}
<ChartWidgetContent
isEnlarged
items={ items }
units={ units }
onZoom={ handleZoom }
handleZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
......
import React from 'react';
import type { LineChart, Resolution } from '@blockscout/stats-types';
import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import { STATS_INTERVALS } from 'ui/stats/constants';
import formatDate from './utils/formatIntervalDate';
export default function useChartQuery(id: string, resolution: Resolution, interval: StatsIntervalIds, enabled = true) {
const { apiData } = useAppContext<'/stats/[id]'>();
const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const [ info, setInfo ] = React.useState<LineChart['info']>(apiData || undefined);
const lineQuery = useApiQuery('stats_line', {
pathParams: { id },
queryParams: {
from: startDate,
to: endDate,
resolution,
},
queryOptions: {
enabled: enabled,
refetchOnMount: false,
placeholderData: {
info: {
title: 'Chart title placeholder',
description: 'Chart placeholder description chart placeholder description',
resolutions: [ 'DAY', 'WEEK', 'MONTH', 'YEAR' ],
id: 'placeholder',
units: undefined,
},
chart: [],
},
},
});
React.useEffect(() => {
if (!info && lineQuery.data?.info && !lineQuery.isPlaceholderData) {
// save info to keep title and description when change query params
setInfo(lineQuery.data?.info);
}
}, [ info, lineQuery.data?.info, lineQuery.isPlaceholderData ]);
const items = React.useMemo(() => lineQuery.data?.chart?.map((item) => {
return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate };
}), [ lineQuery ]);
return {
items,
info,
lineQuery,
};
}
import React from 'react';
export default function useZoomReset() {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const handleZoom = React.useCallback(() => {
setIsZoomResetInitial(false);
}, []);
const handleZoomReset = React.useCallback(() => {
setIsZoomResetInitial(true);
}, []);
return {
isZoomResetInitial,
handleZoom,
handleZoomReset,
};
}
export default function formatDate(date: Date) {
return date.toISOString().substring(0, 10);
}
import { chakra } from '@chakra-ui/react';
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { Resolution } from '@blockscout/stats-types';
import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery';
import type { Route } from 'nextjs-routes';
import useChartQuery from 'ui/shared/chart/useChartQuery';
import ChartWidget from '../shared/chart/ChartWidget';
import { STATS_INTERVALS } from './constants';
type Props = {
id: string;
......@@ -17,50 +19,39 @@ type Props = {
onLoadingError: () => void;
isPlaceholderData: boolean;
className?: string;
href?: Route;
}
function formatDate(date: Date) {
return date.toISOString().substring(0, 10);
}
const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError, units, isPlaceholderData, className }: Props) => {
const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const { data, isPending, isError } = useApiQuery('stats_line', {
pathParams: { id },
queryParams: {
from: startDate,
to: endDate,
},
queryOptions: {
enabled: !isPlaceholderData,
refetchOnMount: false,
},
});
const items = useMemo(() => data?.chart?.map((item) => {
return { date: new Date(item.date), value: Number(item.value), isApproximate: item.is_approximate };
}), [ data ]);
const ChartWidgetContainer = ({
id,
title,
description,
interval,
onLoadingError,
units,
isPlaceholderData,
className,
href,
}: Props) => {
const { items, lineQuery } = useChartQuery(id, Resolution.DAY, interval, !isPlaceholderData);
useEffect(() => {
if (isError) {
if (lineQuery.isError) {
onLoadingError();
}
}, [ isError, onLoadingError ]);
}, [ lineQuery.isError, onLoadingError ]);
return (
<ChartWidget
isError={ isError }
isError={ lineQuery.isError }
items={ items }
title={ title }
units={ units }
description={ description }
isLoading={ isPending }
isLoading={ lineQuery.isPlaceholderData }
minH="230px"
className={ className }
href={ href }
/>
);
};
......
......@@ -95,6 +95,7 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
units={ chart.units || undefined }
isPlaceholderData={ isPlaceholderData }
onLoadingError={ handleChartLoadingError }
href={{ pathname: '/stats/[id]', query: { id: chart.id } }}
/>
)) }
</Grid>
......
......@@ -5,7 +5,7 @@ import Menu from 'ui/shared/chakra/Menu';
import IconSvg from 'ui/shared/IconSvg';
type Props<T extends string> = {
items: Array<{id: T; title: string}>;
items: ReadonlyArray<{id: T; title: string}>;
selectedId: T;
onSelect: (id: T) => void;
}
......@@ -23,7 +23,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec
>
<MenuButton
as={ Button }
size="md"
size="sm"
variant="outline"
colorScheme="gray"
w="100%"
......
......@@ -2,18 +2,13 @@ import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type * as stats from '@blockscout/stats-types';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import type { StatsIntervalIds } from 'types/client/stats';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import FilterInput from 'ui/shared/filters/FilterInput';
import { STATS_INTERVALS } from './constants';
import StatsDropdownMenu from './StatsDropdownMenu';
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
title: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<StatsInterval>;
type Props = {
sections?: Array<stats.LineChartSection>;
currentSection: string;
......@@ -37,7 +32,7 @@ const StatsFilters = ({
}: Props) => {
const sectionsList = [ {
id: 'all',
title: 'All',
title: 'All stats',
}, ... (sections || []) ];
return (
......@@ -49,12 +44,13 @@ const StatsFilters = ({
lg: `"section interval input"`,
}}
gridTemplateColumns={{ base: 'repeat(2, minmax(0, 1fr))', lg: 'auto auto 1fr' }}
alignItems="center"
>
<GridItem
w={{ base: '100%', lg: 'auto' }}
area="section"
>
{ isLoading ? <Skeleton w={{ base: '100%', lg: '76px' }} h="40px" borderRadius="base"/> : (
{ isLoading ? <Skeleton w={{ base: '100%', lg: '103px' }} h="32px" borderRadius="base"/> : (
<StatsDropdownMenu
items={ sectionsList }
selectedId={ currentSection }
......@@ -67,13 +63,7 @@ const StatsFilters = ({
w={{ base: '100%', lg: 'auto' }}
area="interval"
>
{ isLoading ? <Skeleton w={{ base: '100%', lg: '118px' }} h="40px" borderRadius="base"/> : (
<StatsDropdownMenu
items={ intervalList }
selectedId={ interval }
onSelect={ onIntervalChange }
/>
) }
<ChartIntervalSelect interval={ interval } onIntervalChange={ onIntervalChange } isLoading={ isLoading }/>
</GridItem>
<GridItem
......@@ -86,6 +76,7 @@ const StatsFilters = ({
onChange={ onFilterInputChange }
placeholder="Find chart, metric..."
initialValue={ initialFilterValue }
size="xs"
/>
</GridItem>
</Grid>
......
import { Resolution } from '@blockscout/stats-types';
import type { StatsIntervalIds } from 'types/client/stats';
export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; start?: Date } } = {
export const STATS_RESOLUTIONS: Array<{id: Resolution; title: string }> = [
{
id: Resolution.DAY,
title: 'Day',
},
{
id: Resolution.WEEK,
title: 'Week',
},
{
id: Resolution.MONTH,
title: 'Month',
},
{
id: Resolution.YEAR,
title: 'Year',
},
];
export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; shortTitle: string; start?: Date } } = {
all: {
title: 'All time',
shortTitle: 'All time',
},
oneMonth: {
title: '1 month',
shortTitle: '1M',
start: getStartDateInPast(1),
},
threeMonths: {
title: '3 months',
shortTitle: '3M',
start: getStartDateInPast(3),
},
sixMonths: {
title: '6 months',
shortTitle: '6M',
start: getStartDateInPast(6),
},
oneYear: {
title: '1 year',
shortTitle: '1Y',
start: getStartDateInPast(12),
},
};
......
......@@ -1327,10 +1327,10 @@
resolved "https://registry.yarnpkg.com/@blockscout/bens-types/-/bens-types-1.4.1.tgz#9182a79d9015b7fa2339edf0bfa3cd0c32045e66"
integrity sha512-TlZ1HVdZ2Cswm/CcvNoxS+Ydiht/YGaLo//PJR/UmkmihlEFoY4HfVJvVcUnOQXi+Si7FwJ486DPii889nTJsQ==
"@blockscout/stats-types@1.6.0":
version "1.6.0"
resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-1.6.0.tgz#cdb27ab3d3cb1eef7b8b069c39d4e09afda1aec9"
integrity sha512-MzItYOsLa3zgoFzRgFAgg7gynSXG0w/GqHzg5BGHcBPbPSp/g7A6mMtyIchI6TnZxxnCwziHHvzmJFXz11emUg==
"@blockscout/stats-types@^1.6.2-alpha":
version "1.6.2-alpha"
resolved "https://registry.yarnpkg.com/@blockscout/stats-types/-/stats-types-1.6.2-alpha.tgz#e0ec8d12921255943a3b7fc860e1b97e73171a69"
integrity sha512-3rFDgCt0sP2pbPcZ6s3m/zdZxH6hs8PlEchDyqYvKIqVBiBmRwFnXWY22W/Y71r5DJkCjWYbLzxij0WXQxwlnA==
"@blockscout/visualizer-types@0.2.0":
version "0.2.0"
......
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