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
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
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 { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
......@@ -70,8 +70,13 @@ export const RESOURCES = {
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_charts: {
path: '/api/v1/charts/line',
stats_lines: {
path: '/api/v1/lines',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_line: {
path: '/api/v1/lines/:id',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
......@@ -293,7 +298,8 @@ Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
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 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse :
......
......@@ -30,11 +30,25 @@ type Counter = {
units: string;
}
export type Charts = {
chart: Array<ChartsItem>;
export type StatsCharts = {
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;
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 StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId {
......@@ -18,9 +7,3 @@ export enum StatsIntervalId {
'sixMonths',
'oneYear',
}
export type StatsChart = {
apiId: string;
title: string;
description: string;
}
......@@ -12,12 +12,16 @@ import useStats from '../stats/useStats';
const Stats = () => {
const {
section,
isLoading,
isError,
sections,
currentSection,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
filterQuery,
} = useStats();
return (
......@@ -30,7 +34,8 @@ const Stats = () => {
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters
section={ section }
sections={ sections }
currentSection={ currentSection }
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
......@@ -39,6 +44,9 @@ const Stats = () => {
</Box>
<ChartsWidgetsList
filterQuery={ filterQuery }
isError={ isError }
isLoading={ isLoading }
charts={ displayedCharts }
interval={ interval }
/>
......
......@@ -236,9 +236,10 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight, isErro
<Text
variant="secondary"
fontSize="sm"
textAlign="center"
>
{ `Data didn${ apos }t load, please ` }
<Link href={ window.document.location.href }>try to reload page.</Link>
{ `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload the page.</Link>
</Text>
</Flex>
) }
......
......@@ -25,16 +25,15 @@ const ChartWidgetContainer = ({ id, title, description, interval, onLoadingError
const endDate = selectedInterval.start ? formatDate(new Date()) : 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: {
name: id,
from: startDate,
to: endDate,
},
});
const items = data?.chart
.map((item) => {
const items = data?.chart?.map((item) => {
return { date: new Date(item.date), value: Number(item.value) };
});
......
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 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 EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidgetSkeleton from '../shared/chart/ChartWidgetSkeleton';
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
import ChartWidgetContainer from './ChartWidgetContainer';
type Props = {
charts: Array<StatsSection>;
filterQuery: string;
isError: boolean;
isLoading: boolean;
charts?: Array<StatsChartsSection>;
interval: StatsIntervalIds;
}
const ChartsWidgetsList = ({ charts, interval }: Props) => {
const skeletonsCount = 4;
const ChartsWidgetsList = ({ filterQuery, isError, isLoading, charts, interval }: Props) => {
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(
() => setIsSomeChartLoadingError(true),
[ 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 (
<Box>
{ isSomeChartLoadingError && (
<ChartsLoadingErrorAlert/>
) }
{ isAnyChartDisplayed ? (
<List>
{
charts.map((section) => (
charts?.map((section) => (
<ListItem
key={ section.id }
mb={ 8 }
......@@ -54,10 +92,10 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
>
{ section.charts.map((chart) => (
<GridItem
key={ chart.apiId }
key={ chart.id }
>
<ChartWidgetContainer
id={ chart.apiId }
id={ chart.id }
title={ chart.title }
description={ chart.description }
interval={ interval }
......@@ -70,9 +108,6 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
))
}
</List>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
) }
</Box>
);
};
......
......@@ -4,17 +4,22 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 8;
const NumberWidgetsList = () => {
const { data, isLoading } = useApiQuery('stats_counters');
const { data, isLoading, isError } = useApiQuery('stats_counters');
const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => <NumberWidgetSkeleton key={ i }/>);
if (isError) {
return <DataFetchAlert/>;
}
return (
<Grid
gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
......@@ -27,7 +32,7 @@ const NumberWidgetsList = () => {
<NumberWidget
key={ id }
label={ title }
value={ `${ formatNumberToMetricPrefix(Number(value)) } ${ units }` }
value={ `${ formatNumberToMetricPrefix(Number(value)) } ${ units ? units : '' }` }
/>
);
}) }
......
import { Grid, GridItem } from '@chakra-ui/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 { STATS_INTERVALS, STATS_SECTIONS } from './constants';
import { statsChartsScheme } from './constants/charts-scheme';
import { STATS_INTERVALS } from './constants';
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) => ({
id: id,
title: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<StatsInterval>;
type Props = {
section: StatsSectionIds;
onSectionChange: (newSection: StatsSectionIds) => void;
sections?: Array<StatsChartsSection>;
currentSection: string;
onSectionChange: (newSection: string) => void;
interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void;
}
const StatsFilters = ({
section,
sections,
currentSection,
onSectionChange,
interval,
onIntervalChange,
onFilterInputChange,
}: Props) => {
const sectionsList = [ {
id: 'all',
title: 'All',
}, ... (sections || []) ];
return (
<Grid
gap={ 2 }
......@@ -56,7 +53,7 @@ const StatsFilters = ({
>
<StatsDropdownMenu
items={ sectionsList }
selectedId={ section }
selectedId={ currentSection }
onSelect={ onSectionChange }
/>
</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';
export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = {
all: 'All stats',
accounts: 'Accounts',
blocks: 'Blocks',
transactions: 'Transactions',
gas: 'Gas',
};
import type { StatsIntervalIds } from 'types/client/stats';
export const STATS_INTERVALS: { [key in StatsIntervalIds]: { title: string; start?: Date } } = {
all: {
......
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;
}
function isChartNameMatches(q: string, chart: StatsChart) {
function isChartNameMatches(q: string, chart: StatsChartInfo) {
return chart.title.toLowerCase().includes(q.toLowerCase());
}
export default function useStats() {
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>(statsChartsScheme);
const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('oneMonth');
const { data, isLoading, isError } = useApiQuery('stats_lines');
const [ currentSection, setCurrentSection ] = useState('all');
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
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => {
const charts = statsChartsScheme
?.map((section: StatsSection) => {
const charts = section.charts.filter((chart: StatsChart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart));
const filterCharts = useCallback((q: string, currentSection: string) => {
const charts = data?.sections
?.map((section) => {
const charts = section.charts.filter((chart) => isSectionMatches(section, currentSection) && isChartNameMatches(q, chart));
return {
...section,
charts,
};
}).filter((section: StatsSection) => section.charts.length > 0);
}).filter((section) => section.charts.length > 0);
setDisplayedCharts(charts || []);
}, []);
}, [ data ]);
const handleSectionChange = useCallback((newSection: StatsSectionIds) => {
setSection(newSection);
const handleSectionChange = useCallback((newSection: string) => {
setCurrentSection(newSection);
}, []);
const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => {
......@@ -45,18 +49,28 @@ export default function useStats() {
}, []);
useEffect(() => {
filterCharts(filterQuery, section);
}, [ filterQuery, section, filterCharts ]);
filterCharts(filterQuery, currentSection);
}, [ filterQuery, currentSection, filterCharts ]);
return React.useMemo(() => ({
section,
sections: data?.sections,
sectionIds,
isLoading,
isError,
filterQuery,
currentSection,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
}), [
section,
data,
sectionIds,
isLoading,
isError,
filterQuery,
currentSection,
handleSectionChange,
interval,
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