Skip to content

Commit

Permalink
stats updates
Browse files Browse the repository at this point in the history
  • Loading branch information
isstuev committed Sep 6, 2024
1 parent e2081d4 commit ad56b45
Show file tree
Hide file tree
Showing 27 changed files with 888 additions and 296 deletions.
4 changes: 2 additions & 2 deletions configs/envs/.env.eth_sepolia
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/metadata/templates/description.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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,
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata/templates/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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%',
Expand Down Expand Up @@ -71,6 +72,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) {
Expand Down
2 changes: 2 additions & 0 deletions lib/metadata/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { LineChart } from '@blockscout/stats-types';
import type { TokenInfo } from 'types/api/token';

import type { Route } from 'nextjs-routes';
Expand All @@ -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;

Expand Down
3 changes: 3 additions & 0 deletions lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? {
Expand Down
1 change: 1 addition & 0 deletions nextjs/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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 }>
Expand Down
4 changes: 2 additions & 2 deletions nextjs/utils/fetchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Params<R extends ResourceName> = (
{
resource: R;
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | number | undefined>;
} | {
url: string;
route: string;
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"dependencies": {
"@blockscout/bens-types": "1.4.1",
"@blockscout/stats-types": "1.6.0",
"@blockscout/stats-types": "^1.6.2-alpha",
"@blockscout/visualizer-types": "0.2.0",
"@chakra-ui/react": "2.7.1",
"@chakra-ui/theme-tools": "^2.0.18",
Expand Down
52 changes: 52 additions & 0 deletions pages/stats/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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;
};

// export { base as getServerSideProps } from 'nextjs/getServerSideProps';
3 changes: 3 additions & 0 deletions public/static/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
236 changes: 236 additions & 0 deletions ui/pages/Chart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
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="gray"
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 }/>
<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;
Loading

0 comments on commit ad56b45

Please sign in to comment.