Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gas tooltip: periodical updates of data #1482

Merged
merged 10 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/api/useApiFetch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import _omit from 'lodash/omit';
import _pickBy from 'lodash/pickBy';
import React from 'react';

Expand All @@ -19,7 +20,7 @@ import type { ApiResource, ResourceName, ResourcePathParams } from './resources'
export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
}

export default function useApiFetch() {
Expand All @@ -40,6 +41,7 @@ export default function useApiFetch() {
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...fetchParams?.headers,
}, Boolean) as HeadersInit;

return fetch<SuccessType, ErrorType>(
Expand All @@ -51,7 +53,7 @@ export default function useApiFetch() {
// change condition here if something is changed
credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
headers,
...fetchParams,
..._omit(fetchParams, 'headers'),
},
{
resource: resource.path,
Expand Down
21 changes: 18 additions & 3 deletions mocks/stats/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@ import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
coin_price_change_percentage: -7.42,
gas_prices: {
average: 48.0,
fast: 67.5,
slow: 48.0,
average: {
fiat_price: '1.01',
price: 20.41,
time: 12283,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
gas_prices_update_in: 300000,
gas_used_today: '4108680603',
market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064,
Expand Down
11 changes: 7 additions & 4 deletions nextjs/utils/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IncomingMessage } from 'http';
import _pick from 'lodash/pick';
import type { NextApiRequest } from 'next';
import type { NextApiRequestCookies } from 'next/dist/server/api-utils';
import type { RequestInit, Response } from 'node-fetch';
Expand All @@ -14,16 +15,18 @@ export default function fetchFactory(
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
return function fetch(url: string, init?: RequestInit): Promise<Response> {
const csrfToken = _req.headers['x-csrf-token'];
const authToken = _req.headers['Authorization'];
const apiToken = _req.cookies[cookies.NAMES.API_TOKEN];

const headers = {
accept: _req.headers['accept'] || 'application/json',
'content-type': _req.headers['content-type'] || 'application/json',
cookie: apiToken ? `${ cookies.NAMES.API_TOKEN }=${ apiToken }` : '',
...(csrfToken ? { 'x-csrf-token': String(csrfToken) } : {}),
...(authToken ? { Authorization: String(authToken) } : {}),
..._pick(_req.headers, [
'x-csrf-token',
'Authorization',
// feature flags
'updated-gas-oracle',
]) as Record<string, string | undefined>,
};

httpLogger.logger.info({
Expand Down
21 changes: 18 additions & 3 deletions stubs/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,26 @@ import type { Counter, HomeStats, StatsChartsSection } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346,
coin_price: '1807.68',
coin_price_change_percentage: 42,
gas_prices: {
average: 0.1,
fast: 0.11,
slow: 0.1,
average: {
fiat_price: '1.01',
price: 20.41,
time: 12283,
},
fast: {
fiat_price: '1.26',
price: 25.47,
time: 9321,
},
slow: {
fiat_price: '0.97',
price: 19.55,
time: 24543,
},
},
gas_price_updated_at: '2022-11-11T11:09:49.051171Z',
gas_prices_update_in: 300000,
gas_used_today: '0',
market_cap: '0',
network_utilization_percentage: 22.56,
Expand Down
15 changes: 12 additions & 3 deletions types/api/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ export type HomeStats = {
total_transactions: string;
average_block_time: number;
coin_price: string | null;
coin_price_change_percentage: number | null; // e.g -6.22
total_gas_used: string;
transactions_today: string;
gas_used_today: string;
gas_prices: GasPrices | null;
gas_price_updated_at: string | null;
gas_prices_update_in: number;
static_gas_price: string | null;
market_cap: string;
network_utilization_percentage: number;
Expand All @@ -16,9 +19,15 @@ export type HomeStats = {
}

export type GasPrices = {
average: number | null;
fast: number | null;
slow: number | null;
average: GasPriceInfo | null;
fast: GasPriceInfo | null;
slow: GasPriceInfo | null;
}

export interface GasPriceInfo {
fiat_price: string | null;
price: number | null;
time: number | null;
}

export type Counters = {
Expand Down
5 changes: 5 additions & 0 deletions ui/blocks/BlocksTabSlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ interface Props {

const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
placeholderData: HOMEPAGE_STATS,
},
Expand Down
6 changes: 6 additions & 0 deletions ui/home/LatestBlocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ const LatestBlocks = () => {

const queryClient = useQueryClient();
const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});
Expand Down
24 changes: 21 additions & 3 deletions ui/home/Stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ const hasGasTracker = config.UI.homepage.showGasTracker;
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;

const Stats = () => {
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
const { data, isPlaceholderData, isError, dataUpdatedAt } = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});
Expand Down Expand Up @@ -45,7 +51,19 @@ const Stats = () => {
!data.gas_prices && itemsCount--;
data.rootstock_locked_btc && itemsCount++;
const isOdd = Boolean(itemsCount % 2);
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent gasPrices={ data.gas_prices }/> : null;
const gasLabel = hasGasTracker && data.gas_prices ? <GasInfoTooltipContent data={ data } dataUpdatedAt={ dataUpdatedAt }/> : null;

const gasPriceText = (() => {
if (data.gas_prices?.average?.fiat_price) {
return `$${ data.gas_prices.average.fiat_price }`;
}

if (data.gas_prices?.average?.price) {
return `${ data.gas_prices.average.price.toLocaleString() } Gwei`;
}

return 'N/A';
})();

content = (
<>
Expand Down Expand Up @@ -92,7 +110,7 @@ const Stats = () => {
<StatsItem
icon="gas"
title="Gas tracker"
value={ data.gas_prices.average !== null ? `${ Number(data.gas_prices.average).toLocaleString() } Gwei` : 'N/A' }
value={ gasPriceText }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel }
isLoading={ isPlaceholderData }
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion ui/home/indicators/ChainIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import React from 'react';

import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import Hint from 'ui/shared/Hint';

import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
Expand All @@ -29,7 +30,17 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator);

const queryResult = useFetchChartData(indicator);
const statsQueryResult = useApiQuery('homepage_stats');
const statsQueryResult = useApiQuery('homepage_stats', {
fetchParams: {
headers: {
'updated-gas-oracle': 'true',
},
},
queryOptions: {
refetchOnMount: false,
placeholderData: HOMEPAGE_STATS,
},
});

const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ui/pages/__screenshots__/Home.pw.tsx_default_mobile-base-view-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions ui/shared/GasInfoTooltipContent/GasInfoRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { GridItem, chakra } from '@chakra-ui/react';
import React from 'react';

import type { GasPriceInfo } from 'types/api/stats';

import { asymp, space } from 'lib/html-entities';

interface Props {
name: string;
info: GasPriceInfo | null;
}

const GasInfoRow = ({ name, info }: Props) => {
const content = (() => {
if (!info || info.price === null) {
return 'N/A';
}

return (
<>
<span>{ info.fiat_price ? `$${ info.fiat_price }` : `${ info.price } Gwei` }</span>
{ info.time && (
<chakra.span color="text_secondary">
{ space }per tx { asymp } { (info.time / 1000).toLocaleString(undefined, { maximumFractionDigits: 1 }) }s
</chakra.span>
) }
</>
);
})();

return (
<>
<GridItem color="blue.100">{ name }</GridItem>
<GridItem color="text" textAlign="right">{ content }</GridItem>
</>
);
};

export default React.memo(GasInfoRow);
48 changes: 34 additions & 14 deletions ui/shared/GasInfoTooltipContent/GasInfoTooltipContent.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import { Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import { DarkMode, Grid, GridItem } from '@chakra-ui/react';
import React from 'react';

import type { GasPrices } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';

const GasInfoTooltipContent = ({ gasPrices }: {gasPrices: GasPrices}) => {
const nameStyleProps = {
color: useColorModeValue('blue.100', 'blue.600'),
};
import dayjs from 'lib/date/dayjs';

import GasInfoRow from './GasInfoRow';
import GasInfoUpdateTimer from './GasInfoUpdateTimer';

interface Props {
data: HomeStats;
dataUpdatedAt: number;
}

const GasInfoTooltipContent = ({ data, dataUpdatedAt }: Props) => {

if (!data.gas_prices) {
return null;
}

return (
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs">
<GridItem { ...nameStyleProps }>Slow</GridItem>
<GridItem>{ gasPrices.slow !== null ? `${ gasPrices.slow } Gwei` : 'N/A' }</GridItem>
<GridItem { ...nameStyleProps }>Average</GridItem>
<GridItem>{ gasPrices.average !== null ? `${ gasPrices.average } Gwei` : 'N/A' }</GridItem>
<GridItem { ...nameStyleProps }>Fast</GridItem>
<GridItem>{ gasPrices.fast !== null ? `${ gasPrices.fast } Gwei` : 'N/A' }</GridItem>
</Grid>
<DarkMode>
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs" lineHeight={ 4 }>
{ data.gas_price_updated_at && (
<>
<GridItem color="text_secondary">Last update</GridItem>
<GridItem color="text_secondary" display="flex" justifyContent="flex-end" columnGap={ 2 }>
{ dayjs(data.gas_price_updated_at).format('MMM DD, HH:mm:ss') }
{ data.gas_prices_update_in !== 0 &&
<GasInfoUpdateTimer key={ dataUpdatedAt } startTime={ dataUpdatedAt } duration={ data.gas_prices_update_in }/> }
</GridItem>
</>
) }
<GasInfoRow name="Slow" info={ data.gas_prices.slow }/>
<GasInfoRow name="Normal" info={ data.gas_prices.average }/>
<GasInfoRow name="Fast" info={ data.gas_prices.fast }/>
</Grid>
</DarkMode>
);
};

Expand Down
45 changes: 45 additions & 0 deletions ui/shared/GasInfoTooltipContent/GasInfoUpdateTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CircularProgress } from '@chakra-ui/react';
import React from 'react';

import dayjs from 'lib/date/dayjs';

interface Props {
startTime: number;
duration: number;
}

const getValue = (startDate: dayjs.Dayjs, duration: number) => {
const now = dayjs();
const diff = now.diff(startDate, 'ms');
const value = diff / duration * 100;

if (value >= 99) {
return 99;
}

return value;
};

const GasInfoUpdateTimer = ({ startTime, duration }: Props) => {
const [ value, setValue ] = React.useState(getValue(dayjs(startTime), duration));

React.useEffect(() => {
const startDate = dayjs(startTime);

const intervalId = window.setInterval(() => {
const nextValue = getValue(startDate, duration);
setValue(nextValue);
if (nextValue === 99) {
window.clearInterval(intervalId);
}
}, 100);

return () => {
window.clearInterval(intervalId);
};
}, [ startTime, duration ]);

return <CircularProgress value={ value } trackColor="whiteAlpha.100" size={ 4 }/>;
};

export default React.memo(GasInfoUpdateTimer);
Loading
Loading