Commit add0e9bb authored by tom's avatar tom

stats pages

parent 8fb28abe
......@@ -13,14 +13,14 @@ 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 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={ pathname } query={ props.query } apiData={ props.apiData }>
{ /* <Chart/> */ }
<Chart/>
</PageNextJs>
);
};
......
......@@ -3,12 +3,12 @@ import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
// import Stats from 'ui/pages/Stats';
import Stats from 'ui/pages/Stats';
const Page: NextPage = () => {
return (
<PageNextJs pathname="/stats">
{ /* <Stats/> */ }
<Stats/>
</PageNextJs>
);
};
......
......@@ -13,6 +13,7 @@ export interface TagProps extends ChakraTag.RootProps {
closable?: boolean;
truncated?: boolean;
loading?: boolean;
selected?: boolean;
}
export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
......@@ -26,6 +27,7 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
children,
truncated = false,
loading,
selected,
...rest
} = props;
......@@ -37,7 +39,11 @@ export const Tag = React.forwardRef<HTMLSpanElement, TagProps>(
return (
<Skeleton loading={ loading } asChild>
<ChakraTag.Root ref={ ref } { ...rest }>
<ChakraTag.Root
ref={ ref }
{ ...(selected && { 'data-selected': true }) }
{ ...rest }
>
{ startElement && (
<ChakraTag.StartElement>{ startElement }</ChakraTag.StartElement>
) }
......
......@@ -358,6 +358,13 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
bg: { value: { _light: '{colors.gray.100}', _dark: '{colors.gray.800}' } },
fg: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
},
select: {
bg: {
DEFAULT: { value: { _light: '{colors.gray.100}', _dark: '{colors.gray.800}' } },
selected: { value: { _light: '{colors.blue.500}', _dark: '{colors.blue.900}' } },
},
fg: { value: { _light: '{colors.gray.500}', _dark: '{colors.whiteAlpha.800}' } },
},
},
closeTrigger: {
color: { value: { _light: '{colors.gray.400}', _dark: '{colors.gray.500}' } },
......
......@@ -79,6 +79,20 @@ export const recipe = defineSlotRecipe({
textStyle: 'sm',
},
},
lg: {
root: {
px: '6px',
py: '6px',
minH: '8',
gap: '1',
'--tag-avatar-size': 'spacing.4',
'--tag-element-size': 'spacing.3',
'--tag-element-offset': '0px',
},
label: {
textStyle: 'sm',
},
},
},
variant: {
......@@ -104,6 +118,28 @@ export const recipe = defineSlotRecipe({
},
},
},
select: {
root: {
cursor: 'pointer',
bgColor: 'tag.root.select.bg',
color: 'tag.root.select.fg',
'&:not([data-loading], [aria-busy=true])': {
bgColor: 'tag.root.select.bg',
},
_hover: {
color: 'blue.400',
opacity: 0.76,
},
_selected: {
bgColor: 'tag.root.select.bg.selected',
color: 'whiteAlpha.800',
_hover: {
color: 'whiteAlpha.800',
opacity: 0.76,
},
},
},
},
},
},
......
import { Button, Flex, Link, Text } from '@chakra-ui/react';
import { createListCollection, Flex, Text } from '@chakra-ui/react';
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -15,8 +15,11 @@ 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 { Button } from 'toolkit/chakra/button';
import { IconButton } from 'toolkit/chakra/icon-button';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import { Skeleton } from 'toolkit/chakra/skeleton';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import Skeleton from 'ui/shared/chakra/Skeleton';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import ChartMenu from 'ui/shared/chart/ChartMenu';
import ChartWidgetContent from 'ui/shared/chart/ChartWidgetContent';
......@@ -25,7 +28,6 @@ import useZoom from 'ui/shared/chart/useZoom';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import IconSvg from 'ui/shared/IconSvg';
import PageTitle from 'ui/shared/Page/PageTitle';
import Select from 'ui/shared/select/Select';
import { STATS_RESOLUTIONS } from 'ui/stats/constants';
const DEFAULT_RESOLUTION = Resolution.DAY;
......@@ -108,11 +110,11 @@ const Chart = () => {
);
}, [ setIntervalState, router ]);
const onResolutionChange = React.useCallback((resolution: Resolution) => {
setResolution(resolution);
const onResolutionChange = React.useCallback(({ value }: { value: Array<string> }) => {
setResolution(value[0] as Resolution);
router.push({
pathname: router.pathname,
query: { ...router.query, resolution },
query: { ...router.query, resolution: value[0] },
},
undefined,
{ shallow: true },
......@@ -121,7 +123,7 @@ const Chart = () => {
const handleReset = React.useCallback(() => {
handleZoomReset();
onResolutionChange(DEFAULT_RESOLUTION);
onResolutionChange({ value: [ DEFAULT_RESOLUTION ] });
}, [ handleZoomReset, onResolutionChange ]);
const { items, info, lineQuery } = useChartQuery(id, resolution, interval);
......@@ -155,22 +157,24 @@ const Chart = () => {
const shareButton = (
<Button
leftIcon={ <IconSvg name="share" w={ 4 } h={ 4 }/> }
colorScheme="blue"
size="sm"
variant="outline"
onClick={ onShare }
ml={ 6 }
loadingSkeleton={ lineQuery.isPlaceholderData }
>
<IconSvg name="share" w={ 4 } h={ 4 }/>
Share
</Button>
);
const resolutionOptions = React.useMemo(() => {
const resolutionCollection = React.useMemo(() => {
const resolutions = lineQuery.data?.info?.resolutions || [];
return STATS_RESOLUTIONS
const items = STATS_RESOLUTIONS
.filter((resolution) => resolutions.includes(resolution.id))
.map((resolution) => ({ value: resolution.id, label: resolution.title }));
return createListCollection({ items });
}, [ lineQuery.data?.info?.resolutions ]);
return (
......@@ -194,21 +198,32 @@ const Chart = () => {
(!info && lineQuery.data?.info?.resolutions && lineQuery.data?.info?.resolutions.length > 1)
) && (
<Flex alignItems="center" gap={ 3 }>
<Skeleton isLoaded={ !isInfoLoading }>
<Skeleton loading={ isInfoLoading }>
{ isMobile ? 'Res.' : 'Resolution' }
</Skeleton>
<Select
options={ resolutionOptions }
defaultValue={ defaultResolution }
onChange={ onResolutionChange }
isLoading={ isInfoLoading }
<SelectRoot
collection={ resolutionCollection }
variant="outline"
defaultValue={ [ defaultResolution ] }
onValueChange={ onResolutionChange }
w={{ base: 'fit-content', lg: '160px' }}
fontWeight={ 600 }
/>
>
<SelectControl loading={ isInfoLoading }>
<SelectValueText placeholder="Select resolution"/>
</SelectControl>
<SelectContent>
{ resolutionCollection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
</Flex>
) }
{ (Boolean(zoomRange)) && (
<Link
<Button
variant="link"
onClick={ handleReset }
display="flex"
alignItems="center"
......@@ -216,7 +231,7 @@ const Chart = () => {
>
<IconSvg name="repeat" w={ 5 } h={ 5 }/>
{ !isMobile && 'Reset' }
</Link>
</Button>
) }
</Flex>
<Flex alignItems="center" gap={ 3 }>
......@@ -225,23 +240,23 @@ const Chart = () => {
{ !isMobile && (isInBrowser && ((window.navigator.share as any) ?
shareButton :
(
<CopyToClipboard
text={ config.app.baseUrl + router.asPath }
size={ 5 }
type="link"
variant="outline"
colorScheme="blue"
display="flex"
borderRadius="8px"
width={ 8 }
height={ 8 }
/>
<IconButton variant="outline" size="sm" asChild p={ 1 }>
<CopyToClipboard
text={ config.app.baseUrl + router.asPath }
type="link"
boxSize={ 8 }
color="button.outline.fg"
ml={ 0 }
borderRadius="base"
/>
</IconButton>
)
)) }
{ (hasItems || lineQuery.isPlaceholderData) && (
<ChartMenu
items={ items }
title={ info?.title || '' }
description={ info?.description || '' }
isLoading={ lineQuery.isPlaceholderData }
chartRef={ ref }
resolution={ resolution }
......
import { Box, Heading, Icon } from '@chakra-ui/react';
import { Box, Icon } from '@chakra-ui/react';
import React from 'react';
// This icon doesn't work properly when it is in the sprite
// Probably because of radial gradient
// eslint-disable-next-line no-restricted-imports
import emptySearchResultIcon from 'icons/empty_search_result.svg';
import { Heading } from 'toolkit/chakra/heading';
interface Props {
text: string | React.JSX.Element;
......@@ -26,7 +27,7 @@ const EmptySearchResult = ({ text }: Props) => {
mb={{ base: 4, sm: 6 }}
/>
<Heading as="h4" size="sm" mb={ 2 }>
<Heading level="3" mb={ 2 }>
No results
</Heading>
......
import type { TagProps } from '@chakra-ui/react';
import { createListCollection } from '@chakra-ui/react';
import React from 'react';
import type { StatsInterval, StatsIntervalIds } from 'types/client/stats';
import type { SelectOption } from 'toolkit/chakra/select';
import Skeleton from 'ui/shared/chakra/Skeleton';
import Select from 'ui/shared/select/Select';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { TagProps } from 'toolkit/chakra/tag';
import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect';
import { STATS_INTERVALS } from 'ui/stats/constants';
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
value: id,
label: STATS_INTERVALS[id as StatsIntervalIds].title,
})) as Array<SelectOption>;
const intervalCollection = createListCollection({
items: Object.keys(STATS_INTERVALS).map((id: string) => ({
value: id,
label: STATS_INTERVALS[id as StatsIntervalIds].shortTitle,
})),
});
const intervalListShort = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
......@@ -27,21 +29,35 @@ type Props = {
};
const ChartIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagSize }: Props) => {
const handleItemSelect = React.useCallback(({ value }: { value: Array<string> }) => {
onIntervalChange(value[0] as StatsIntervalIds);
}, [ onIntervalChange ]);
return (
<>
<Skeleton display={{ base: 'none', lg: 'flex' }} borderRadius="base" isLoaded={ !isLoading }>
<Skeleton hideBelow="lg" borderRadius="base" loading={ isLoading }>
<TagGroupSelect<StatsIntervalIds> items={ intervalListShort } onChange={ onIntervalChange } value={ interval } tagSize={ selectTagSize }/>
</Skeleton>
<Select
options={ intervalList }
defaultValue={ interval }
onChange={ onIntervalChange }
isLoading={ isLoading }
w={{ base: '100%', lg: '136px' }}
display={{ base: 'flex', lg: 'none' }}
flexShrink={ 0 }
fontWeight={ 600 }
/>
<SelectRoot
collection={ intervalCollection }
variant="outline"
defaultValue={ [ interval ] }
onValueChange={ handleItemSelect }
hideFrom="lg"
w="100%"
>
<SelectControl loading={ isLoading }>
<SelectValueText placeholder="Select interval"/>
</SelectControl>
<SelectContent>
{ intervalCollection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
</>
);
};
......
......@@ -130,7 +130,7 @@ const ChartMenu = ({
onClick={ hasShare ? handleShare : handleCopy }
closeOnSelect={ hasShare ? false : true }
>
<IconSvg name={ hasShare ? 'share' : 'copy' } boxSize={ 5 } mr={ 3 }/>
<IconSvg name={ hasShare ? 'share' : 'copy' } boxSize={ 5 }/>
{ hasShare ? 'Share' : 'Copy link' }
</MenuItem>
) }
......@@ -138,21 +138,21 @@ const ChartMenu = ({
value="fullscreen"
onClick={ showChartFullscreen }
>
<IconSvg name="scope" boxSize={ 5 } mr={ 3 }/>
<IconSvg name="scope" boxSize={ 5 }/>
View fullscreen
</MenuItem>
<MenuItem
value="save-png"
onClick={ handleFileSaveClick }
>
<IconSvg name="files/image" boxSize={ 5 } mr={ 3 }/>
<IconSvg name="files/image" boxSize={ 5 }/>
Save as PNG
</MenuItem>
<MenuItem
value="save-csv"
onClick={ handleSVGSavingClick }
>
<IconSvg name="files/csv" boxSize={ 5 } mr={ 3 }/>
<IconSvg name="files/csv" boxSize={ 5 }/>
Save as CSV
</MenuItem>
</MenuContent>
......
......@@ -40,12 +40,13 @@ const FullscreenChartModal = ({
<DialogRoot
open={ open }
onOpenChange={ onOpenChange }
size="full"
// FIXME: with size="full" the chart will not be expanded to the full height of the modal
size="cover"
>
<DialogContent>
<DialogHeader/>
<DialogBody pt={ 6 } display="flex" flexDir="column">
<Grid gridColumnGap={ 2 } >
<Grid gridColumnGap={ 2 } mb={ 4 }>
<Heading mb={ 1 } level="2">
{ title }
</Heading>
......@@ -54,7 +55,7 @@ const FullscreenChartModal = ({
<Text
gridColumn={ 1 }
color="text.secondary"
fontSize="xs"
textStyle="sm"
>
{ description }
</Text>
......
import type { TagProps } from '@chakra-ui/react';
import { HStack, Tag } from '@chakra-ui/react';
import { HStack } from '@chakra-ui/react';
import React from 'react';
import type { TagProps } from 'toolkit/chakra/tag';
import { Tag } from 'toolkit/chakra/tag';
type Props<T extends string> = {
items: Array<{ id: T; title: string }>;
tagSize?: TagProps['size'];
......@@ -42,7 +44,7 @@ const TagGroupSelect = <T extends string>({ items, value, isMulti, onChange, tag
variant="select"
key={ item.id }
data-id={ item.id }
data-selected={ isSelected }
selected={ isSelected }
fontWeight={ 500 }
onClick={ onItemClick }
size={ tagSize }
......
......@@ -19,6 +19,22 @@ const TagShowcase = () => {
<Sample label="variant: clickable">
<Tag variant="clickable">My tag</Tag>
</Sample>
<Sample label="variant: select">
<Tag variant="select">Default</Tag>
<Tag variant="select" selected>Selected</Tag>
</Sample>
</SamplesStack>
</Section>
<Section>
<SectionHeader>Size</SectionHeader>
<SamplesStack>
<Sample label="size: md">
<Tag size="md">My tag</Tag>
</Sample>
<Sample label="size: lg">
<Tag size="lg">My tag</Tag>
</Sample>
</SamplesStack>
</Section>
......
import { Box, Grid, Heading, List, ListItem } from '@chakra-ui/react';
import { Box, Grid } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import type * as stats from '@blockscout/stats-types';
......@@ -6,7 +6,8 @@ import type { StatsIntervalIds } from 'types/client/stats';
import useApiQuery from 'lib/api/useApiQuery';
import { apos } from 'lib/html-entities';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Heading } from 'toolkit/chakra/heading';
import { Skeleton } from 'toolkit/chakra/skeleton';
import EmptySearchResult from 'ui/shared/EmptySearchResult';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import IconSvg from 'ui/shared/IconSvg';
......@@ -61,18 +62,18 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
<ChartsLoadingErrorAlert/>
) }
<List ref={ sectionRef }>
<section ref={ sectionRef }>
{
charts?.map((section) => (
<ListItem
<Box
key={ section.id }
mb={ 8 }
_last={{
marginBottom: 0,
}}
>
<Skeleton isLoaded={ !isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }>
<Heading size="md" id={ section.id }>
<Skeleton loading={ isPlaceholderData } mb={ 4 } display="inline-flex" alignItems="center" columnGap={ 2 } id={ section.id }>
<Heading level="2" id={ section.id }>
{ section.title }
</Heading>
{ section.id === 'gas' && homeStatsQuery.data && homeStatsQuery.data.gas_prices && (
......@@ -100,10 +101,10 @@ const ChartsWidgetsList = ({ filterQuery, isError, isPlaceholderData, charts, in
/>
)) }
</Grid>
</ListItem>
</Box>
))
}
</List>
</section>
</Box>
);
};
......
import { Grid, GridItem } from '@chakra-ui/react';
import { createListCollection, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import type * as stats from '@blockscout/stats-types';
import type { StatsIntervalIds } from 'types/client/stats';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import ChartIntervalSelect from 'ui/shared/chart/ChartIntervalSelect';
import FilterInput from 'ui/shared/filters/FilterInput';
import Select from 'ui/shared/select/Select';
type Props = {
sections?: Array<stats.LineChartSection>;
......@@ -30,13 +30,19 @@ const StatsFilters = ({
initialFilterValue,
}: Props) => {
const options = React.useMemo(() => {
return [
{ value: 'all', label: 'All stats' },
...(sections || []).map((section) => ({ value: section.id, label: section.title })),
];
const collection = React.useMemo(() => {
return createListCollection({
items: [
{ value: 'all', label: 'All stats' },
...(sections || []).map((section) => ({ value: section.id, label: section.title })),
],
});
}, [ sections ]);
const handleItemSelect = React.useCallback(({ value }: { value: Array<string> }) => {
onSectionChange(value[0]);
}, [ onSectionChange ]);
return (
<Grid
gap={{ base: 2, lg: 6 }}
......@@ -52,14 +58,24 @@ const StatsFilters = ({
w={{ base: '100%', lg: 'auto' }}
area="section"
>
<Select
options={ options }
defaultValue={ currentSection }
onChange={ onSectionChange }
isLoading={ isLoading }
<SelectRoot
collection={ collection }
variant="outline"
defaultValue={ [ currentSection ] }
onValueChange={ handleItemSelect }
w={{ base: '100%', lg: '136px' }}
fontWeight={ 600 }
/>
>
<SelectControl loading={ isLoading }>
<SelectValueText placeholder="Select section"/>
</SelectControl>
<SelectContent>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
</GridItem>
<GridItem
......@@ -75,11 +91,11 @@ const StatsFilters = ({
>
<FilterInput
key={ initialFilterValue }
isLoading={ isLoading }
loading={ isLoading }
onChange={ onFilterInputChange }
placeholder="Find chart, metric..."
initialValue={ initialFilterValue }
size="xs"
size="sm"
/>
</GridItem>
</Grid>
......
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