Commit 67ab023c authored by Yuri Mikhin's avatar Yuri Mikhin Committed by Yuri Mikhin

Add charts filtering logic to the Stats page.

parent 98de5351
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId { export enum StatsSectionId {
'all', 'all',
'accounts', 'accounts',
...@@ -5,9 +7,9 @@ export enum StatsSectionId { ...@@ -5,9 +7,9 @@ export enum StatsSectionId {
'transactions', 'transactions',
'gas', 'gas',
} }
export type StatsSectionIds = keyof typeof StatsSectionId;
export type StatsSection = { id: StatsSectionIds; value: string }
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId { export enum StatsIntervalId {
'all', 'all',
'oneMonth', 'oneMonth',
...@@ -15,5 +17,11 @@ export enum StatsIntervalId { ...@@ -15,5 +17,11 @@ export enum StatsIntervalId {
'sixMonths', 'sixMonths',
'oneYear', 'oneYear',
} }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export type StatsInterval = { id: StatsIntervalIds; value: string } export type StatsChart = {
visible?: boolean;
id: string;
title: string;
description: string;
apiMethodURL: string;
}
...@@ -5,18 +5,36 @@ import Page from 'ui/shared/Page/Page'; ...@@ -5,18 +5,36 @@ import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import StatsFilters from '../stats/StatsFilters'; import StatsFilters from '../stats/StatsFilters';
import useStats from '../stats/useStats';
import WidgetsList from '../stats/WidgetsList'; import WidgetsList from '../stats/WidgetsList';
const Stats = () => { const Stats = () => {
const {
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
} = useStats();
return ( return (
<Page> <Page>
<PageTitle text="Ethereum Stats"/> <PageTitle text="Ethereum Stats"/>
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<StatsFilters/> <StatsFilters
section={ section }
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts }
/>
</Box> </Box>
<WidgetsList/> <WidgetsList
charts={ displayedCharts }
/>
</Page> </Page>
); );
}; };
......
...@@ -65,6 +65,7 @@ const ChartWidget = ({ title, description }: Props) => { ...@@ -65,6 +65,7 @@ const ChartWidget = ({ title, description }: Props) => {
items={ demoData } items={ demoData }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
title={ title }
/> />
</Box> </Box>
); );
......
...@@ -14,6 +14,7 @@ import useChartSize from 'ui/shared/chart/useChartSize'; ...@@ -14,6 +14,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props { interface Props {
title: string;
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onZoom: () => void; onZoom: () => void;
isZoomResetInitial: boolean; isZoomResetInitial: boolean;
...@@ -21,7 +22,7 @@ interface Props { ...@@ -21,7 +22,7 @@ interface Props {
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 }; const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => { const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]); const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN); const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
...@@ -30,7 +31,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => { ...@@ -30,7 +31,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => {
const displayedData = useMemo(() => items.slice(range[0], range[1]).map((d) => const displayedData = useMemo(() => items.slice(range[0], range[1]).map((d) =>
({ ...d, date: new Date(d.date) })), [ items, range ]); ({ ...d, date: new Date(d.date) })), [ items, range ]);
const chartData = [ { items: items, name: 'chart', color } ]; const chartData = [ { items: items, name: title, color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({ const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: 'chart', color } ], data: [ { items: displayedData, name: 'chart', color } ],
......
...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react'; ...@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg'; import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
type Props<T extends string> = { type Props<T extends string> = {
items: Array<{id: T; value: string}>; items: Array<{id: T; title: string}>;
selectedId: T; selectedId: T;
onSelect: (id: T) => void; onSelect: (id: T) => void;
} }
...@@ -32,7 +32,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec ...@@ -32,7 +32,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
{ selectedCategory?.value } { selectedCategory?.title }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/> <Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box> </Box>
</MenuButton> </MenuButton>
...@@ -48,7 +48,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec ...@@ -48,7 +48,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec
key={ item.id } key={ item.id }
value={ item.id } value={ item.id }
> >
{ item.value } { item.title }
</MenuItemOption> </MenuItemOption>
)) } )) }
</MenuOptionGroup> </MenuOptionGroup>
......
import { Grid, GridItem } from '@chakra-ui/react'; import { Grid, GridItem } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import React from 'react';
import React, { useCallback, useState } from 'react';
import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats'; import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
...@@ -11,21 +10,29 @@ import StatsDropdownMenu from './StatsDropdownMenu'; ...@@ -11,21 +10,29 @@ import StatsDropdownMenu from './StatsDropdownMenu';
const sectionsList = Object.keys(STATS_SECTIONS).map((id: string) => ({ const sectionsList = Object.keys(STATS_SECTIONS).map((id: string) => ({
id: id, id: id,
value: STATS_SECTIONS[id as StatsSectionIds], title: STATS_SECTIONS[id as StatsSectionIds],
})) as Array<StatsSection>; })) 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,
value: STATS_INTERVALS[id as StatsIntervalIds], title: STATS_INTERVALS[id as StatsIntervalIds],
})) as Array<StatsInterval>; })) as Array<StatsInterval>;
const StatsFilters = () => { type Props = {
const [ selectedSectionId, setSelectedSectionId ] = useState<StatsSectionIds>('all'); section: StatsSectionIds;
const [ selectedIntervalId, setSelectedIntervalId ] = useState<StatsIntervalIds>('all'); onSectionChange: (newSection: StatsSectionIds) => void;
const [ , setFilterQuery ] = useState(''); interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void;
}
// eslint-disable-next-line react-hooks/exhaustive-deps const StatsFilters = ({
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []); section,
onSectionChange,
interval,
onIntervalChange,
onFilterInputChange,
}: Props) => {
return ( return (
<Grid <Grid
...@@ -42,7 +49,7 @@ const StatsFilters = () => { ...@@ -42,7 +49,7 @@ const StatsFilters = () => {
area="input" area="input"
> >
<FilterInput <FilterInput
onChange={ debounceFilterCharts } onChange={ onFilterInputChange }
placeholder="Find chart, metric..."/> placeholder="Find chart, metric..."/>
</GridItem> </GridItem>
...@@ -52,8 +59,8 @@ const StatsFilters = () => { ...@@ -52,8 +59,8 @@ const StatsFilters = () => {
> >
<StatsDropdownMenu <StatsDropdownMenu
items={ sectionsList } items={ sectionsList }
selectedId={ selectedSectionId } selectedId={ section }
onSelect={ setSelectedSectionId } onSelect={ onSectionChange }
/> />
</GridItem> </GridItem>
...@@ -63,8 +70,8 @@ const StatsFilters = () => { ...@@ -63,8 +70,8 @@ const StatsFilters = () => {
> >
<StatsDropdownMenu <StatsDropdownMenu
items={ intervalList } items={ intervalList }
selectedId={ selectedIntervalId } selectedId={ interval }
onSelect={ setSelectedIntervalId } onSelect={ onIntervalChange }
/> />
</GridItem> </GridItem>
</Grid> </Grid>
......
import { Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react'; import { Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { StatsSection } from 'types/client/stats';
import { apos } from 'lib/html-entities';
import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidget from './ChartWidget'; import ChartWidget from './ChartWidget';
import { statisticsChartsScheme } from './constants/charts-scheme';
const WidgetsList = () => { type Props = {
return ( charts: Array<StatsSection>;
}
const WidgetsList = ({ charts }: Props) => {
const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible));
return isAnyChartDisplayed ? (
<List> <List>
{ {
statisticsChartsScheme.map((section) => ( charts.map((section) => (
<ListItem <ListItem
display={ section.charts.every((chart) => !chart.visible) ? 'none' : 'block' }
key={ section.id } key={ section.id }
mb={ 8 } mb={ 8 }
_last={{ _last={{
...@@ -30,7 +41,10 @@ const WidgetsList = () => { ...@@ -30,7 +41,10 @@ const WidgetsList = () => {
gap={ 4 } gap={ 4 }
> >
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem key={ chart.id }> <GridItem
key={ chart.id }
display={ chart.visible ? 'block' : 'none' }
>
<ChartWidget <ChartWidget
apiMethodURL={ chart.apiMethodURL } apiMethodURL={ chart.apiMethodURL }
title={ chart.title } title={ chart.title }
...@@ -43,6 +57,8 @@ const WidgetsList = () => { ...@@ -43,6 +57,8 @@ const WidgetsList = () => {
)) ))
} }
</List> </List>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
); );
}; };
......
export const statisticsChartsScheme = [ import type { StatsSection } from 'types/client/stats';
export const statsChartsScheme: Array<StatsSection> = [
{ {
id: 'blocks', id: 'blocks',
title: 'Blocks', title: 'Blocks',
......
import type { TimeChartItem } from '../../shared/chart/types'; import type { TimeChartItem } from 'ui/shared/chart/types';
export const demoData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, { export const demoData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
date: new Date('2022-10-18T00:00:00.000Z'), date: new Date('2022-10-18T00:00:00.000Z'),
......
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import { statsChartsScheme } from './constants/charts-scheme';
function isSectionMatches(section: StatsSection, currentSection: StatsSectionIds): boolean {
return currentSection === 'all' || section.id === currentSection;
}
function isChartNameMatches(q: string, chart: StatsChart) {
return chart.title.toLowerCase().includes(q.toLowerCase());
}
export default function useStats() {
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultCharts, setDefaultCharts ] = useState<Array<StatsSection>>();
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>([]);
const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
// 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 = defaultCharts
?.map((section: StatsSection) => {
const charts = section.charts.map((chart: StatsChart) => ({
...chart,
visible: isSectionMatches(section, currentSection) && isChartNameMatches(q, chart),
}));
return {
...section,
charts,
};
});
setDisplayedCharts(charts || []);
}, [ defaultCharts ]);
const handleSectionChange = useCallback((newSection: StatsSectionIds) => {
setSection(newSection);
}, []);
const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => {
setInterval(newInterval);
}, []);
useEffect(() => {
filterCharts(filterQuery, section);
}, [ filterQuery, section, filterCharts ]);
useEffect(() => {
setDefaultCharts(statsChartsScheme);
setDisplayedCharts(statsChartsScheme);
setIsLoading(false);
}, []);
return React.useMemo(() => ({
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
isLoading,
displayedCharts,
}), [
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
isLoading,
]);
}
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