Commit 12ba8d63 authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Remake charts API integration.

parent 1130e1c2
...@@ -22,7 +22,7 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction ...@@ -22,7 +22,7 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, Charts, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
...@@ -70,8 +70,13 @@ export const RESOURCES = { ...@@ -70,8 +70,13 @@ export const RESOURCES = {
endpoint: appConfig.statsApi.endpoint, endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath, basePath: appConfig.statsApi.basePath,
}, },
stats_charts: { stats_lines: {
path: '/api/v1/charts/line', path: '/api/v1/lines',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_line: {
path: '/api/v1/lines/:id',
endpoint: appConfig.statsApi.endpoint, endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath, basePath: appConfig.statsApi.basePath,
}, },
...@@ -293,7 +298,8 @@ Q extends 'homepage_blocks' ? Array<Block> : ...@@ -293,7 +298,8 @@ Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Counters : Q extends 'stats_counters' ? Counters :
Q extends 'stats_charts' ? Charts : Q extends 'stats_lines' ? StatsCharts :
Q extends 'stats_line' ? StatsChart :
Q extends 'blocks' ? BlocksResponse : Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block : Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_txs' ? BlockTransactionsResponse :
......
...@@ -30,11 +30,25 @@ type Counter = { ...@@ -30,11 +30,25 @@ type Counter = {
units: string; units: string;
} }
export type Charts = { export type StatsCharts = {
chart: Array<ChartsItem>; sections: Array<StatsChartsSection>;
} }
export type ChartsItem ={ export type StatsChartsSection = {
id: string;
title: string;
charts: Array<StatsChartInfo>;
}
export type StatsChartInfo = {
id: string;
title: string;
description: string;
}
export type StatsChart = { chart: Array<StatsChartItem> };
export type StatsChartItem = {
date: string; date: string;
value: string; value: string;
} }
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'tokens',
'transactions',
'gas',
}
export type StatsInterval = { id: StatsIntervalIds; title: string } export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId; export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId { export enum StatsIntervalId {
...@@ -18,9 +7,3 @@ export enum StatsIntervalId { ...@@ -18,9 +7,3 @@ export enum StatsIntervalId {
'sixMonths', 'sixMonths',
'oneYear', 'oneYear',
} }
export type StatsChart = {
apiId: string;
title: string;
description: string;
}
...@@ -12,12 +12,16 @@ import useStats from '../stats/useStats'; ...@@ -12,12 +12,16 @@ import useStats from '../stats/useStats';
const Stats = () => { const Stats = () => {
const { const {
section, isLoading,
isError,
sections,
currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
filterQuery,
} = useStats(); } = useStats();
return ( return (
...@@ -30,7 +34,8 @@ const Stats = () => { ...@@ -30,7 +34,8 @@ const Stats = () => {
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<StatsFilters <StatsFilters
section={ section } sections={ sections }
currentSection={ currentSection }
onSectionChange={ handleSectionChange } onSectionChange={ handleSectionChange }
interval={ interval } interval={ interval }
onIntervalChange={ handleIntervalChange } onIntervalChange={ handleIntervalChange }
...@@ -39,6 +44,9 @@ const Stats = () => { ...@@ -39,6 +44,9 @@ const Stats = () => {
</Box> </Box>
<ChartsWidgetsList <ChartsWidgetsList
filterQuery={ filterQuery }
isError={ isError }
isLoading={ isLoading }
charts={ displayedCharts } charts={ displayedCharts }
interval={ interval } interval={ interval }
/> />
......
...@@ -236,9 +236,10 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro ...@@ -236,9 +236,10 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
<Text <Text
variant="secondary" variant="secondary"
fontSize="sm" fontSize="sm"
textAlign="center"
> >
{ `Data didn${ apos }t load, please ` } { `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload page.</Link> <Link href={ window.document.location.href }>try to reload the page.</Link>
</Text> </Text>
</Flex> </Flex>
) } ) }
......
...@@ -25,18 +25,17 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError ...@@ -25,18 +25,17 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined; const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined; const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const { data, isLoading, isError } = useApiQuery('stats_charts', { const { data, isLoading, isError } = useApiQuery('stats_line', {
pathParams: { id },
queryParams: { queryParams: {
name: id,
from: startDate, from: startDate,
to: endDate, to: endDate,
}, },
}); });
const items = data?.chart const items = data?.chart?.map((item) => {
.map((item) => { return { date: new Date(item.date), value: Number(item.value) };
return { date: new Date(item.date), value: Number(item.value) }; });
});
useEffect(() => { useEffect(() => {
if (isError) { if (isError) {
......
import { Box, Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react'; import { Box, Grid, GridItem, Heading, List, ListItem, Skeleton } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { StatsIntervalIds, StatsSection } from 'types/client/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } 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 ChartWidgetSkeleton from '../shared/chart/ChartWidgetSkeleton';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert'; import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
import ChartWidgetContainer from './ChartWidgetContainer'; import ChartWidgetContainer from './ChartWidgetContainer';
type Props = { type Props = {
charts: Array<StatsSection>; filterQuery: string;
isError: boolean;
isLoading: boolean;
charts?: Array<StatsChartsSection>;
interval: StatsIntervalIds; interval: StatsIntervalIds;
} }
const ChartsWidgetsList = ({ charts, interval }: Props) => { const skeletonsCount = 4;
const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }: Props) => {
const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false); const [ isSomeChartLoadingError, setIsSomeChartLoadingError ] = useState(false);
const isAnyChartDisplayed = charts.some((section) => section.charts.length > 0); const isAnyChartDisplayed = charts?.some((section) => section.charts.length > 0);
const isEmptyChartList = Boolean(filterQuery) && !isAnyChartDisplayed;
const handleChartLoadingError = useCallback( const handleChartLoadingError = useCallback(
() => setIsSomeChartLoadingError(true), () => setIsSomeChartLoadingError(true),
[ setIsSomeChartLoadingError ]); [ setIsSomeChartLoadingError ]);
const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => (
<GridItem key={ i }>
<ChartWidgetSkeleton hasDescription={ true }/>
</GridItem>
));
if (isLoading) {
return (
<>
<Skeleton w="30%" h="32px" mb={ 4 }/>
<Grid
templateColumns={{
lg: 'repeat(2, minmax(0, 1fr))',
}}
gap={ 4 }
>
{ skeletonElement }
</Grid>
</>
);
}
if (isError) {
return <ChartsLoadingErrorAlert/>;
}
if (isEmptyChartList) {
return <EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>;
}
return ( return (
<Box> <Box>
{ isSomeChartLoadingError && ( { isSomeChartLoadingError && (
<ChartsLoadingErrorAlert/> <ChartsLoadingErrorAlert/>
) } ) }
{ isAnyChartDisplayed ? ( <List>
<List> {
{ charts?.map((section) => (
charts.map((section) => ( <ListItem
<ListItem key={ section.id }
key={ section.id } mb={ 8 }
mb={ 8 } _last={{
_last={{ marginBottom: 0,
marginBottom: 0, }}
}} >
<Heading
size="md"
mb={ 4 }
> >
<Heading { section.title }
size="md" </Heading>
mb={ 4 }
>
{ section.title }
</Heading>
<Grid <Grid
templateColumns={{ templateColumns={{
lg: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(2, minmax(0, 1fr))',
}} }}
gap={ 4 } gap={ 4 }
> >
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem <GridItem
key={ chart.apiId } key={ chart.id }
> >
<ChartWidgetContainer <ChartWidgetContainer
id={ chart.apiId } id={ chart.id }
title={ chart.title } title={ chart.title }
description={ chart.description } description={ chart.description }
interval={ interval } interval={ interval }
onLoadingError={ handleChartLoadingError } onLoadingError={ handleChartLoadingError }
/> />
</GridItem> </GridItem>
)) } )) }
</Grid> </Grid>
</ListItem> </ListItem>
)) ))
} }
</List> </List>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
) }
</Box> </Box>
); );
}; };
......
...@@ -4,17 +4,22 @@ import React from 'react'; ...@@ -4,17 +4,22 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix'; import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton'; import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 8; const skeletonsCount = 8;
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const { data, isLoading } = useApiQuery('stats_counters'); const { data, isLoading, isError } = useApiQuery('stats_counters');
const skeletonElement = [ ...Array(skeletonsCount) ] const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => <NumberWidgetSkeleton key={ i }/>); .map((e, i) => <NumberWidgetSkeleton key={ i }/>);
if (isError) {
return <DataFetchAlert/>;
}
return ( return (
<Grid <Grid
gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }} gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
...@@ -27,7 +32,7 @@ const NumberWidgetsList = () => { ...@@ -27,7 +32,7 @@ const NumberWidgetsList = () => {
<NumberWidget <NumberWidget
key={ id } key={ id }
label={ title } label={ title }
value={ `${ formatNumberToMetricPrefix(Number(value)) } ${ units }` } value={ `${ formatNumberToMetricPrefix(Number(value)) } ${ units ? units : '' }` }
/> />
); );
}) } }) }
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats'; import type { StatsChartsSection } from 'types/api/stats';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
import { STATS_INTERVALS, STATS_SECTIONS } from './constants'; import { STATS_INTERVALS } from './constants';
import { statsChartsScheme } from './constants/charts-scheme';
import StatsDropdownMenu from './StatsDropdownMenu'; import StatsDropdownMenu from './StatsDropdownMenu';
const listedSections = statsChartsScheme
.filter(section => section.charts.length > 0);
const sectionsList = Object.keys(STATS_SECTIONS)
.filter(key => key === 'all' || listedSections.some(section => section.id === key))
.map((id: string) => ({
id: id,
title: STATS_SECTIONS[id as StatsSectionIds],
})) as Array<StatsSection>;
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, title: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<StatsInterval>; })) as Array<StatsInterval>;
type Props = { type Props = {
section: StatsSectionIds; sections?: Array<StatsChartsSection>;
onSectionChange: (newSection: StatsSectionIds) => void; currentSection: string;
onSectionChange: (newSection: string) => void;
interval: StatsIntervalIds; interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void; onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void; onFilterInputChange: (q: string) => void;
} }
const StatsFilters = ({ const StatsFilters = ({
section, sections,
currentSection,
onSectionChange, onSectionChange,
interval, interval,
onIntervalChange, onIntervalChange,
onFilterInputChange, onFilterInputChange,
}: Props) => { }: Props) => {
const sectionsList = [ {
id: 'all',
title: 'All',
}, ... (sections || []) ];
return ( return (
<Grid <Grid
gap={ 2 } gap={ 2 }
...@@ -56,7 +53,7 @@ const StatsFilters = ({ ...@@ -56,7 +53,7 @@ const StatsFilters = ({
> >
<StatsDropdownMenu <StatsDropdownMenu
items={ sectionsList } items={ sectionsList }
selectedId={ section } selectedId={ currentSection }
onSelect={ onSectionChange } onSelect={ onSectionChange }
/> />
</GridItem> </GridItem>
......
import type { StatsSection } from 'types/client/stats';
export const statsChartsScheme: Array<StatsSection> = [
{
id: 'accounts',
title: 'Accounts',
charts: [
{
apiId: 'activeAccounts',
title: 'Active accounts',
description: 'Active accounts number per period',
},
{
apiId: 'accountsGrowth',
title: 'Accounts growth',
description: 'Cumulative accounts number per period',
},
],
},
{
id: 'transactions',
title: 'Transactions',
charts: [
{
apiId: 'averageTxnFee',
title: 'Average transaction fee',
description: 'The average amount in USD spent per transaction',
},
{
apiId: 'txnsFee',
title: 'Transactions fees',
description: 'Amount of tokens paid as fees',
},
{
apiId: 'newTxns',
title: 'New transactions',
description: 'New transactions number',
},
{
apiId: 'txnsGrowth',
title: 'Transactions growth',
description: 'Cumulative transactions number',
},
],
},
{
id: 'blocks',
title: 'Blocks',
charts: [
{
apiId: 'newBlocks',
title: 'New blocks',
description: 'New blocks number',
},
{
apiId: 'averageBlockSize',
title: 'Average block size',
description: 'Average size of blocks in bytes',
},
],
},
{
id: 'tokens',
title: 'Tokens',
charts: [
{
apiId: 'nativeCoinHoldersGrowth',
title: 'Native coin holders growth',
description: 'Cumulative token holders number for the period',
},
{
apiId: 'newNativeCoinTransfers',
title: 'New native coins transfers',
description: 'New token transfers number for the period',
},
{
apiId: 'nativeCoinSupply',
title: 'Native coin circulating supply',
description: 'Amount of token circulating supply for the period',
},
],
},
{
id: 'gas',
title: 'Gas',
charts: [
{
apiId: 'averageGasLimit',
title: 'Average gas limit',
description: 'Average gas limit per block for the period',
},
{
apiId: 'gasUsedGrowth',
title: 'Gas used growth',
description: 'Cumulative gas used for the period',
},
{
apiId: 'averageGasPrice',
title: 'Average gas price',
description: 'Average gas price for the period (Gwei)',
},
],
},
];
import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats'; import type { StatsIntervalIds } from 'types/client/stats';
export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = {
all: 'All stats',
accounts: 'Accounts',
blocks: 'Blocks',
transactions: 'Transactions',
gas: 'Gas',
};
export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; start?: Date } } = { export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; start?: Date } } = {
all: { all: {
......
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import type { StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats'; import type { StatsChartInfo, StatsChartsSection } from 'types/api/stats';
import type { StatsIntervalIds } from 'types/client/stats';
import { statsChartsScheme } from './constants/charts-scheme'; import useApiQuery from 'lib/api/useApiQuery';
function isSectionMatches(section: StatsSection, currentSection: StatsSectionIds): boolean { function isSectionMatches(section: StatsChartsSection, currentSection: string): boolean {
return currentSection === 'all' || section.id === currentSection; return currentSection === 'all' || section.id === currentSection;
} }
function isChartNameMatches(q: string, chart: StatsChart) { function isChartNameMatches(q: string, chart: StatsChartInfo) {
return chart.title.toLowerCase().includes(q.toLowerCase()); return chart.title.toLowerCase().includes(q.toLowerCase());
} }
export default function useStats() { export default function useStats() {
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>(statsChartsScheme); const { data, isLoading, isError } = useApiQuery('stats_lines');
const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth'); const [ currentSection, setCurrentSection ] = useState('all');
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const [ displayedCharts, setDisplayedCharts ] = useState(data?.sections);
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const sectionIds = useMemo(() => data?.sections?.map(({ id }) => id), [ data ]);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []); const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => { const filterCharts = useCallback((q: string, currentSection: string) => {
const charts = statsChartsScheme const charts = data?.sections
?.map((section: StatsSection) => { ?.map((section) => {
const charts = section.charts.filter((chart: StatsChart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart)); const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart));
return { return {
...section, ...section,
charts, charts,
}; };
}).filter((section: StatsSection) => section.charts.length > 0); }).filter((section) => section.charts.length > 0);
setDisplayedCharts(charts || []); setDisplayedCharts(charts || []);
}, []); }, [ data ]);
const handleSectionChange = useCallback((newSection: StatsSectionIds) => { const handleSectionChange = useCallback((newSection: string) => {
setSection(newSection); setCurrentSection(newSection);
}, []); }, []);
const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => { const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => {
...@@ -45,18 +49,28 @@ export default function useStats() { ...@@ -45,18 +49,28 @@ export default function useStats() {
}, []); }, []);
useEffect(() => { useEffect(() => {
filterCharts(filterQuery, section); filterCharts(filterQuery, currentSection);
}, [ filterQuery, section, filterCharts ]); }, [ filterQuery, currentSection, filterCharts ]);
return React.useMemo(() => ({ return React.useMemo(() => ({
section, sections: data?.sections,
sectionIds,
isLoading,
isError,
filterQuery,
currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
}), [ }), [
section, data,
sectionIds,
isLoading,
isError,
filterQuery,
currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
......
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