Commit f672536f authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Add backend integration for stats charts.

parent 346d4948
...@@ -43,6 +43,7 @@ NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__ ...@@ -43,6 +43,7 @@ NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__
# api config # api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__ NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com/
# external services config # external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__ NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
......
...@@ -94,6 +94,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -94,6 +94,7 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- | | --- | --- | --- | --- |
| NEXT_PUBLIC_API_HOST | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API host in this variable | `my-host.com` | | NEXT_PUBLIC_API_HOST | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API host in this variable | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` | | NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
| NEXT_PUBLIC_STATS_API_HOST | `string` *(optional)* | Pass the Stats API host in this variable | `my-host.com` |
### Featured network configuration properties ### Featured network configuration properties
......
...@@ -94,6 +94,9 @@ const config = Object.freeze({ ...@@ -94,6 +94,9 @@ const config = Object.freeze({
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com', socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''), basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
}, },
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
},
homepage: { homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [], charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) || plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
......
...@@ -12,3 +12,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom ...@@ -12,3 +12,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com/
...@@ -20,3 +20,4 @@ NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-t ...@@ -20,3 +20,4 @@ NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-t
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com/
...@@ -10,3 +10,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom ...@@ -10,3 +10,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com/
...@@ -14,3 +14,4 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true ...@@ -14,3 +14,4 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com/
...@@ -518,6 +518,8 @@ frontend: ...@@ -518,6 +518,8 @@ frontend:
_default: unknown _default: unknown
NEXT_PUBLIC_API_HOST: NEXT_PUBLIC_API_HOST:
_default: blockscout.com/eth/goerli _default: blockscout.com/eth/goerli
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.aws-k8s.blockscout.com/
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com/eth/goerli _default: blockscout.com/eth/goerli
NEXT_PUBLIC_LOGOUT_URL: NEXT_PUBLIC_LOGOUT_URL:
......
...@@ -373,6 +373,8 @@ frontend: ...@@ -373,6 +373,8 @@ frontend:
NEXT_PUBLIC_API_HOST: NEXT_PUBLIC_API_HOST:
_default: blockscout.com _default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.aws-k8s.blockscout.com/
NEXT_PUBLIC_AUTH_URL: NEXT_PUBLIC_AUTH_URL:
_default: https://blockscout-main.test.aws-k8s.blockscout.com _default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
......
...@@ -8,14 +8,17 @@ import * as cookies from 'lib/cookies'; ...@@ -8,14 +8,17 @@ import * as cookies from 'lib/cookies';
// first arg can be only a string // first arg can be only a string
// FIXME migrate to RequestInfo later if needed // FIXME migrate to RequestInfo later if needed
export default function fetchFactory(_req: NextApiRequest) { export default function fetchFactory(
_req: NextApiRequest,
apiEndpoint: string = appConfig.api.endpoint,
) {
return function fetch(path: string, init?: RequestInit): Promise<Response> { return function fetch(path: string, init?: RequestInit): Promise<Response> {
const headers = { const headers = {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`, cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
}; };
const url = new URL(path, appConfig.api.endpoint); const url = new URL(path, apiEndpoint);
httpLogger.logger.info({ httpLogger.logger.info({
message: 'Trying to call API', message: 'Trying to call API',
......
...@@ -6,7 +6,7 @@ import { httpLogger } from 'lib/api/logger'; ...@@ -6,7 +6,7 @@ import { httpLogger } from 'lib/api/logger';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE'; type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>) { export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>, apiEndpoint?: string) {
const handler = async(_req: NextApiRequest, res: NextApiResponse) => { const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
httpLogger(_req, res); httpLogger(_req, res);
...@@ -18,8 +18,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string, ...@@ -18,8 +18,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD'; const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`); const url = apiEndpoint ? `/api${ getUrl(_req) }` : getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req); const fetch = fetchFactory(_req, apiEndpoint);
const response = await fetch(url, { const response = await fetch(url, {
method: _req.method, method: _req.method,
body: isBodyDisallowed ? undefined : _req.body, body: isBodyDisallowed ? undefined : _req.body,
......
import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const { precision, from, to } = req.query;
return `/v1/blocks/new?precision=${ precision }${ from ? `&from=${ from }&to=${ to }` : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler;
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler'; import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats'; const getUrl = () => '/v1/counters';
const requestHandler = handler(getUrl, [ 'GET' ]); const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler; export default requestHandler;
...@@ -20,5 +20,19 @@ export type GasPrices = { ...@@ -20,5 +20,19 @@ export type GasPrices = {
} }
export type Stats = { export type Stats = {
total_blocks: string; totalBlocksAllTime: string;
}
export type Charts = {
'chart': Array<{
date: string;
value: string;
}>;
} }
export enum ChartPrecision {
'DAY' = 'DAY',
'MONTH' = 'MONTH',
}
export type ChartPrecisionIds = keyof typeof ChartPrecision;
...@@ -5,6 +5,7 @@ export enum QueryKeys { ...@@ -5,6 +5,7 @@ export enum QueryKeys {
txsPending = 'txs-pending', txsPending = 'txs-pending',
homeStats='homeStats', homeStats='homeStats',
stats='stats', stats='stats',
charts='stats',
tx = 'tx', tx = 'tx',
txInternals = 'tx-internals', txInternals = 'tx-internals',
txLogs = 'tx-logs', txLogs = 'tx-logs',
......
...@@ -18,9 +18,6 @@ const Stats = () => { ...@@ -18,9 +18,6 @@ const Stats = () => {
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
showChartFullscreen,
clearFullscreenChart,
fullscreenChart,
} = useStats(); } = useStats();
return ( return (
...@@ -43,9 +40,7 @@ const Stats = () => { ...@@ -43,9 +40,7 @@ const Stats = () => {
<ChartsWidgetsList <ChartsWidgetsList
charts={ displayedCharts } charts={ displayedCharts }
onChartFullscreenClick={ showChartFullscreen } interval={ interval }
fullscreenChart={ fullscreenChart }
onModalClose={ clearFullscreenChart }
/> />
</Page> </Page>
); );
......
import { Box, Button, Grid, Heading, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, useColorModeValue, VisuallyHidden } from '@chakra-ui/react'; import { Box, Button, Grid, Heading, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, useColorModeValue, VisuallyHidden } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import { useQuery } from '@tanstack/react-query';
import { format, parse } from 'date-fns';
import React, { useCallback, useState } from 'react';
import type { ModalChart } from 'types/client/stats'; import type { Charts } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import type { StatsIntervalIds } from 'types/client/stats';
import dotsIcon from 'icons/vertical-dots.svg'; import dotsIcon from 'icons/vertical-dots.svg';
import useFetch from 'lib/hooks/useFetch';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
import { demoChartsData } from './constants/demo-charts-data'; import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import { STATS_INTERVALS } from './constants';
import FullscreenChartModal from './FullscreenChartModal';
type Props = { type Props = {
id: string; id: string;
onFullscreenClick: (chart: ModalChart) => void;
apiMethodURL: string; apiMethodURL: string;
title: string; title: string;
description: string; description: string;
interval: StatsIntervalIds;
} }
const ChartWidget = ({ id, title, description, onFullscreenClick }: Props) => { const dateFormat = 'dd-LL-yyyy';
const ChartWidget = ({ id, title, description, apiMethodURL, interval }: Props) => {
const fetch = useFetch();
const selectedInterval = STATS_INTERVALS[interval];
const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const endDate = selectedInterval.start ? format(new Date(), dateFormat) : undefined;
const startDate = selectedInterval.start ? format(selectedInterval.start, dateFormat) : undefined;
const menuButtonColor = useColorModeValue('black', 'white');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const url = `${ apiMethodURL }?precision=${ selectedInterval.precision }${ startDate ? `&from=${ startDate }&to=${ endDate }` : '' }`;
const { data, isLoading } = useQuery<unknown, unknown, Charts>(
[ QueryKeys.charts, id, selectedInterval.precision, startDate ],
async() => await fetch(url),
);
const handleZoom = useCallback(() => { const handleZoom = useCallback(() => {
setIsZoomResetInitial(false); setIsZoomResetInitial(false);
}, []); }, []);
...@@ -27,81 +54,114 @@ const ChartWidget = ({ id, title, description, onFullscreenClick }: Props) => { ...@@ -27,81 +54,114 @@ const ChartWidget = ({ id, title, description, onFullscreenClick }: Props) => {
setIsZoomResetInitial(true); setIsZoomResetInitial(true);
}, []); }, []);
const handleFullscreenClick = useCallback(() => { const showChartFullscreen = useCallback(() => {
onFullscreenClick({ id, title }); setIsFullscreen(true);
}, [ id, title, onFullscreenClick ]);
if (!document.fullscreenElement) {
return ( document.documentElement.requestFullscreen();
<Box }
padding={{ base: 3, md: 4 }} }, []);
borderRadius="md"
border="1px" const clearFullscreenChart = useCallback(() => {
borderColor={ useColorModeValue('gray.200', 'gray.600') } setIsFullscreen(false);
>
<Grid if (document.fullscreenElement) {
gridTemplateColumns="auto auto 36px" document.exitFullscreen();
gridColumnGap={ 4 } }
> }, []);
<Heading
mb={ 1 } if (isLoading) {
size={{ base: 'xs', md: 'sm' }} return <ChartWidgetSkeleton/>;
> }
{ title }
</Heading> if (data) {
const items = data.chart
<Text .map((item) => {
mb={ 1 } return { date: parse(item.date, dateFormat, new Date()), value: Number(item.value) };
gridColumn={ 1 } });
as="p"
variant="secondary" return (
fontSize="xs" <>
<Box
padding={{ base: 3, md: 4 }}
borderRadius="md"
border="1px"
borderColor={ borderColor }
> >
{ description } <Grid
</Text> gridTemplateColumns="auto auto 36px"
gridColumnGap={ 4 }
{ !isZoomResetInitial && (
<Button
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
>
Reset zoom
</Button>
) }
<Menu>
<MenuButton
gridColumn={ 3 }
gridRow="1/3"
justifySelf="end"
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 } color={ useColorModeValue('black', 'white') }/> }
colorScheme="transparent"
as={ IconButton }
> >
<VisuallyHidden> <Heading
Open chart options menu mb={ 1 }
</VisuallyHidden> size={{ base: 'xs', md: 'sm' }}
</MenuButton> >
<MenuList> { title }
<MenuItem onClick={ handleFullscreenClick }>View fullscreen</MenuItem> </Heading>
</MenuList>
</Menu> <Text
</Grid> mb={ 1 }
gridColumn={ 1 }
<ChartWidgetGraph as="p"
items={ demoChartsData } variant="secondary"
onZoom={ handleZoom } fontSize="xs"
isZoomResetInitial={ isZoomResetInitial } >
title={ title } { description }
/> </Text>
</Box>
); { !isZoomResetInitial && (
<Button
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
>
Reset zoom
</Button>
) }
<Menu>
<MenuButton
gridColumn={ 3 }
gridRow="1/3"
justifySelf="end"
w="36px"
h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 } color={ menuButtonColor }/> }
colorScheme="transparent"
as={ IconButton }
>
<VisuallyHidden>
Open chart options menu
</VisuallyHidden>
</MenuButton>
<MenuList>
<MenuItem onClick={ showChartFullscreen }>View fullscreen</MenuItem>
</MenuList>
</Menu>
</Grid>
<ChartWidgetGraph
items={ items }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
</Box>
<FullscreenChartModal
isOpen={ isFullscreen }
items={ items }
title={ title }
onClose={ clearFullscreenChart }
/>
</>
);
}
return null;
}; };
export default ChartWidget; export default ChartWidget;
import { Box, Skeleton } from '@chakra-ui/react';
import React from 'react';
const ChartWidgetSkeleton = () => {
return (
<Box
height="235px"
padding={{ base: 3, md: 4 }}
>
<Skeleton w="75%" h="24px" mb={ 1 }/>
<Skeleton w="50%" h="18px" mb={ 5 }/>
<Skeleton w="100%" h="150px"/>
</Box>
);
};
export default ChartWidgetSkeleton;
import { Box, Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react'; import { Box, Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ModalChart, StatsSection } from 'types/client/stats'; import type { StatsIntervalIds, StatsSection } from 'types/client/stats';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from '../apps/EmptySearchResult'; import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidget from './ChartWidget'; import ChartWidget from './ChartWidget';
import FullscreenChartModal from './FullscreenChartModal';
type Props = { type Props = {
charts: Array<StatsSection>; charts: Array<StatsSection>;
onChartFullscreenClick: (chart: ModalChart) => void; interval: StatsIntervalIds;
fullscreenChart: ModalChart | null;
onModalClose: () => void;
} }
const ChartsWidgetsList = ({ charts, onChartFullscreenClick, fullscreenChart, onModalClose }: Props) => { const ChartsWidgetsList = ({ charts, interval }: Props) => {
const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible)); const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible));
return ( return (
...@@ -53,10 +50,10 @@ const ChartsWidgetsList = ({ charts, onChartFullscreenClick, fullscreenChart, on ...@@ -53,10 +50,10 @@ const ChartsWidgetsList = ({ charts, onChartFullscreenClick, fullscreenChart, on
> >
<ChartWidget <ChartWidget
id={ chart.id } id={ chart.id }
onFullscreenClick={ onChartFullscreenClick }
apiMethodURL={ chart.apiMethodURL } apiMethodURL={ chart.apiMethodURL }
title={ chart.title } title={ chart.title }
description={ chart.description } description={ chart.description }
interval={ interval }
/> />
</GridItem> </GridItem>
)) } )) }
...@@ -68,14 +65,6 @@ const ChartsWidgetsList = ({ charts, onChartFullscreenClick, fullscreenChart, on ...@@ -68,14 +65,6 @@ const ChartsWidgetsList = ({ charts, onChartFullscreenClick, fullscreenChart, on
) : ( ) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/> <EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
) } ) }
{ fullscreenChart && (
<FullscreenChartModal
id={ fullscreenChart.id }
title={ fullscreenChart.title }
onClose={ onModalClose }
/>
) }
</Box> </Box>
); );
}; };
......
import { Button, Flex, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react'; import { Button, Flex, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TimeChartItem } from '../shared/chart/types';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
import { demoChartsData } from './constants/demo-charts-data';
type Props = { type Props = {
id: string; isOpen: boolean;
title: string; title: string;
items: Array<TimeChartItem>;
onClose: () => void; onClose: () => void;
} }
const FullscreenChartModal = ({ const FullscreenChartModal = ({
id, isOpen,
title, title,
items,
onClose, onClose,
}: Props) => { }: Props) => {
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -27,7 +30,7 @@ const FullscreenChartModal = ({ ...@@ -27,7 +30,7 @@ const FullscreenChartModal = ({
return ( return (
<Modal <Modal
isOpen={ Boolean(id) } isOpen={ isOpen }
onClose={ onClose } onClose={ onClose }
size="full" size="full"
isCentered isCentered
...@@ -74,10 +77,10 @@ const FullscreenChartModal = ({ ...@@ -74,10 +77,10 @@ const FullscreenChartModal = ({
h="100%" h="100%"
> >
<ChartWidgetGraph <ChartWidgetGraph
items={ demoChartsData } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
title="test" title={ title }
/> />
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
......
...@@ -17,7 +17,6 @@ const NumberWidgetsList = () => { ...@@ -17,7 +17,6 @@ const NumberWidgetsList = () => {
const { data, isLoading } = useQuery<unknown, unknown, Stats>( const { data, isLoading } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ], [ QueryKeys.stats ],
// TODO: Just temporary. Remove this when the API is ready.
async() => await fetch(`/node-api/stats`), async() => await fetch(`/node-api/stats`),
); );
...@@ -30,8 +29,8 @@ const NumberWidgetsList = () => { ...@@ -30,8 +29,8 @@ const NumberWidgetsList = () => {
.map((e, i) => <NumberWidgetSkeleton key={ i }/>) : .map((e, i) => <NumberWidgetSkeleton key={ i }/>) :
( (
<NumberWidget <NumberWidget
label="Total blocks" label="Total blocks all time"
value={ Number(data?.total_blocks).toLocaleString() } value={ Number(data?.totalBlocksAllTime).toLocaleString() }
/> />
) } ) }
</Grid> </Grid>
......
...@@ -21,7 +21,8 @@ const sectionsList = Object.keys(STATS_SECTIONS) ...@@ -21,7 +21,8 @@ const sectionsList = Object.keys(STATS_SECTIONS)
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({ const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id, id: id,
title: STATS_INTERVALS[id as StatsIntervalIds], title: STATS_INTERVALS[id as StatsIntervalIds].title,
precision: STATS_INTERVALS[id as StatsIntervalIds].precision,
})) as Array<StatsInterval>; })) as Array<StatsInterval>;
type Props = { type Props = {
......
...@@ -9,31 +9,8 @@ export const statsChartsScheme: Array<StatsSection> = [ ...@@ -9,31 +9,8 @@ export const statsChartsScheme: Array<StatsSection> = [
id: 'new-blocks', id: 'new-blocks',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number per day', description: 'New blocks number per day',
apiMethodURL: '/node-api/stats/charts/transactions', apiMethodURL: '/node-api/stats/charts/blocks/new',
}, visible: true,
{
id: 'average-block-size',
title: 'Average block size',
description: 'Average size of blocks in bytes per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
id: 'transaction-fees',
title: 'Transaction fees',
description: 'Amount of tokens paid as fees per day',
apiMethodURL: '/node-api/stats/charts/transactions',
},
{
id: 'native-coin-holders-growth',
title: 'Native coin holders growth',
description: 'Total token holders number per day',
apiMethodURL: '/node-api/stats/charts/transactions',
}, },
], ],
}, },
......
import type { TimeChartItem } from 'ui/shared/chart/types';
export const demoChartsData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
date: new Date('2022-10-18T00:00:00.000Z'),
value: 370100,
}, { date: new Date('2022-10-19T00:00:00.000Z'), value: 283234 }, { date: new Date('2022-10-20T00:00:00.000Z'), value: 420910 }, {
date: new Date('2022-10-21T00:00:00.000Z'),
value: 411988,
}, { date: new Date('2022-10-22T00:00:00.000Z'), value: 356269 }, { date: new Date('2022-10-23T00:00:00.000Z'), value: 389747 }, {
date: new Date('2022-10-24T00:00:00.000Z'),
value: 387130,
}, { date: new Date('2022-10-25T00:00:00.000Z'), value: 428785 }, { date: new Date('2022-10-26T00:00:00.000Z'), value: 63809 }, {
date: new Date('2022-10-27T00:00:00.000Z'),
value: 50518,
}, { date: new Date('2022-10-28T00:00:00.000Z'), value: 39087 }, { date: new Date('2022-10-29T00:00:00.000Z'), value: 36789 }, {
date: new Date('2022-10-30T00:00:00.000Z'),
value: 48569,
}, { date: new Date('2022-10-31T00:00:00.000Z'), value: 62519 }, { date: new Date('2022-11-01T00:00:00.000Z'), value: 152059 }, {
date: new Date('2022-11-02T00:00:00.000Z'),
value: 63743,
}, { date: new Date('2022-11-03T00:00:00.000Z'), value: 83667 }, { date: new Date('2022-11-04T00:00:00.000Z'), value: 91725 }, {
date: new Date('2022-11-05T00:00:00.000Z'),
value: 82897,
}, { date: new Date('2022-11-06T00:00:00.000Z'), value: 62477 }, { date: new Date('2022-11-07T00:00:00.000Z'), value: 58131 }, {
date: new Date('2022-11-08T00:00:00.000Z'),
value: 74197,
}, { date: new Date('2022-11-09T00:00:00.000Z'), value: 43691 }, { date: new Date('2022-11-10T00:00:00.000Z'), value: 92887 }, {
date: new Date('2022-11-11T00:00:00.000Z'),
value: 79493,
}, { date: new Date('2022-11-12T00:00:00.000Z'), value: 86764 }, { date: new Date('2022-11-13T00:00:00.000Z'), value: 22338 }, {
date: new Date('2022-11-14T00:00:00.000Z'),
value: 62266,
}, { date: new Date('2022-11-15T00:00:00.000Z'), value: 84084 }, { date: new Date('2022-11-16T00:00:00.000Z'), value: 75898 } ];
import { sub } from 'date-fns';
import type { ChartPrecisionIds } from 'types/api/stats';
import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats'; import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats';
export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = { export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = {
...@@ -8,10 +11,33 @@ export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = { ...@@ -8,10 +11,33 @@ export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = {
gas: 'Gas', gas: 'Gas',
}; };
export const STATS_INTERVALS: { [key in StatsIntervalIds]: string } = { export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; precision: ChartPrecisionIds; start?: Date } } = {
all: 'All time', all: {
oneMonth: '1 month', title: 'All time',
threeMonths: '3 months', precision: 'MONTH',
sixMonths: '6 months', },
oneYear: '1 year', oneMonth: {
title: '1 month',
precision: 'DAY',
start: getStartDateInPast(1),
},
threeMonths: {
title: '3 months',
precision: 'DAY',
start: getStartDateInPast(3),
},
sixMonths: {
title: '6 months',
precision: 'MONTH',
start: getStartDateInPast(6),
},
oneYear: {
title: '1 year',
precision: 'MONTH',
start: getStartDateInPast(12),
},
}; };
function getStartDateInPast(months: number): Date {
return sub(new Date(), { months });
}
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { ModalChart, StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats'; import type { StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import { statsChartsScheme } from './constants/charts-scheme'; import { statsChartsScheme } from './constants/charts-scheme';
...@@ -16,8 +16,7 @@ function isChartNameMatches(q: string, chart: StatsChart) { ...@@ -16,8 +16,7 @@ function isChartNameMatches(q: string, chart: StatsChart) {
export default function useStats() { export default function useStats() {
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>(statsChartsScheme); const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>(statsChartsScheme);
const [ section, setSection ] = useState<StatsSectionIds>('all'); const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('all'); const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const [ fullscreenChart, setFullscreenChart ] = useState<ModalChart | null>(null);
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
...@@ -48,22 +47,6 @@ export default function useStats() { ...@@ -48,22 +47,6 @@ export default function useStats() {
setInterval(newInterval); setInterval(newInterval);
}, []); }, []);
const showChartFullscreen = useCallback((chart: ModalChart) => {
setFullscreenChart(chart);
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
}
}, []);
const clearFullscreenChart = useCallback(() => {
setFullscreenChart(null);
if (document.fullscreenElement) {
document.exitFullscreen();
}
}, []);
useEffect(() => { useEffect(() => {
filterCharts(filterQuery, section); filterCharts(filterQuery, section);
}, [ filterQuery, section, filterCharts ]); }, [ filterQuery, section, filterCharts ]);
...@@ -75,9 +58,6 @@ export default function useStats() { ...@@ -75,9 +58,6 @@ export default function useStats() {
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
showChartFullscreen,
clearFullscreenChart,
fullscreenChart,
}), [ }), [
section, section,
handleSectionChange, handleSectionChange,
...@@ -85,8 +65,5 @@ export default function useStats() { ...@@ -85,8 +65,5 @@ export default function useStats() {
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
showChartFullscreen,
clearFullscreenChart,
fullscreenChart,
]); ]);
} }
...@@ -4735,6 +4735,11 @@ data-urls@^3.0.2: ...@@ -4735,6 +4735,11 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0" whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0" whatwg-url "^11.0.0"
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
dateformat@^4.6.3: dateformat@^4.6.3:
version "4.6.3" version "4.6.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-4.6.3.tgz#556fa6497e5217fedb78821424f8a1c22fa3f4b5"
......
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