Commit 45591ba9 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into search-bar

parents 9e9955b1 bf706c90
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.224 8.076V6.048h9.557v2.028H13.22v9.608h-2.432V8.076H7.224Z" fill="currentColor"/>
</svg>
...@@ -17,7 +17,7 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch ...@@ -17,7 +17,7 @@ import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/ch
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl'; import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponse } 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 { Stats, Charts, HomeStats } from 'types/api/stats'; import type { Stats, Charts, HomeStats } from 'types/api/stats';
...@@ -157,6 +157,11 @@ export const RESOURCES = { ...@@ -157,6 +157,11 @@ export const RESOURCES = {
address_coin_balance_chart: { address_coin_balance_chart: {
path: '/api/v2/addresses/:id/coin-balance-history-by-day', path: '/api/v2/addresses/:id/coin-balance-history-by-day',
}, },
address_logs: {
path: '/api/v2/addresses/:id/logs',
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ],
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
...@@ -230,6 +235,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -230,6 +235,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'address_logs' |
'search'; 'search';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -258,7 +264,7 @@ Q extends 'txs_validated' ? TransactionsResponseValidated : ...@@ -258,7 +264,7 @@ Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'tx' ? Transaction : Q extends 'tx' ? Transaction :
Q extends 'tx_internal_txs' ? InternalTransactionsResponse : Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponse : Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse : Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse : Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'address' ? Address : Q extends 'address' ? Address :
...@@ -270,6 +276,7 @@ Q extends 'address_token_transfers' ? AddressTokenTransferResponse : ...@@ -270,6 +276,7 @@ Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse : Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
never; never;
......
...@@ -17,7 +17,7 @@ const relativeTimeConfig = { ...@@ -17,7 +17,7 @@ const relativeTimeConfig = {
{ l: 'dd', r: 29, d: 'day' }, { l: 'dd', r: 29, d: 'day' },
{ l: 'M', r: 1 }, { l: 'M', r: 1 },
{ l: 'MM', r: 11, d: 'month' }, { l: 'MM', r: 11, d: 'month' },
{ l: 'y' }, { l: 'y', r: 17 },
{ l: 'yy', d: 'year' }, { l: 'yy', d: 'year' },
], ],
}; };
......
export default function formatNumberToMetricPrefix(number: number) {
return Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 3,
}).format(number);
}
...@@ -9,7 +9,7 @@ import Stats from '../ui/pages/Stats'; ...@@ -9,7 +9,7 @@ import Stats from '../ui/pages/Stats';
const StatsPage: NextPage = () => { const StatsPage: NextPage = () => {
return ( return (
<> <>
<Head><title>{ appConfig.network.name } Stats</title></Head> <Head><title>{ appConfig.network.name } stats</title></Head>
<Stats/> <Stats/>
</> </>
); );
......
...@@ -9,7 +9,7 @@ export interface Log { ...@@ -9,7 +9,7 @@ export interface Log {
decoded: DecodedInput | null; decoded: DecodedInput | null;
} }
export interface LogsResponse { export interface LogsResponseTx {
items: Array<Log>; items: Array<Log>;
next_page_params: { next_page_params: {
index: number; index: number;
...@@ -17,3 +17,13 @@ export interface LogsResponse { ...@@ -17,3 +17,13 @@ export interface LogsResponse {
transaction_hash: string; transaction_hash: string;
}; };
} }
export interface LogsResponseAddress {
items: Array<Log>;
next_page_params: {
index: number;
items_count: number;
transaction_index: number;
block_number: number;
};
}
...@@ -4,6 +4,7 @@ export enum StatsSectionId { ...@@ -4,6 +4,7 @@ export enum StatsSectionId {
'all', 'all',
'accounts', 'accounts',
'blocks', 'blocks',
'tokens',
'transactions', 'transactions',
'gas', 'gas',
} }
...@@ -19,7 +20,7 @@ export enum StatsIntervalId { ...@@ -19,7 +20,7 @@ export enum StatsIntervalId {
} }
export type StatsChart = { export type StatsChart = {
id: string; apiId: string;
title: string; title: string;
description: string; description: string;
} }
import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react'; import { Box, Hide, Show, Table, Tbody, Th, Tr } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressBlocksValidatedResponse } from 'types/api/address'; import type { AddressBlocksValidatedResponse } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
...@@ -22,20 +22,15 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -22,20 +22,15 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem'; import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem'; import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
interface Props { const AddressBlocksValidated = () => {
addressQuery: UseQueryResult<Address>;
}
const AddressBlocksValidated = ({ addressQuery }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter();
const addressHash = String(router.query?.id);
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { id: addressQuery.data?.hash }, pathParams: { id: addressHash },
options: {
enabled: Boolean(addressQuery.data),
},
}); });
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
...@@ -46,7 +41,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -46,7 +41,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { id: addressQuery.data?.hash } }), getResourceKey('address_blocks_validated', { pathParams: { id: addressHash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => { (prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -57,13 +52,13 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -57,13 +52,13 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
items: [ payload.block, ...prevData.items ], items: [ payload.block, ...prevData.items ],
}; };
}); });
}, [ addressQuery.data?.hash, queryClient ]); }, [ addressHash, queryClient ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`, topic: `blocks:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: addressQuery.isLoading || addressQuery.isError || !addressQuery.data.hash || query.pagination.page !== 1, isDisabled: !addressHash || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressCoinBalanceHistoryResponse } from 'types/api/address'; import type { AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -14,20 +14,15 @@ import SocketAlert from 'ui/shared/SocketAlert'; ...@@ -14,20 +14,15 @@ import SocketAlert from 'ui/shared/SocketAlert';
import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart'; import AddressCoinBalanceChart from './coinBalance/AddressCoinBalanceChart';
import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory'; import AddressCoinBalanceHistory from './coinBalance/AddressCoinBalanceHistory';
interface Props { const AddressCoinBalance = () => {
addressQuery: UseQueryResult<Address>;
}
const AddressCoinBalance = ({ addressQuery }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter();
const addressHash = String(router.query?.id);
const coinBalanceQuery = useQueryWithPages({ const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance', resourceName: 'address_coin_balance',
pathParams: { id: addressQuery.data?.hash }, pathParams: { id: addressHash },
options: {
enabled: Boolean(addressQuery.data),
},
}); });
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
...@@ -38,7 +33,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -38,7 +33,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_coin_balance', { pathParams: { id: addressQuery.data?.hash } }), getResourceKey('address_coin_balance', { pathParams: { id: addressHash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => { (prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -52,13 +47,13 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -52,13 +47,13 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
], ],
}; };
}); });
}, [ addressQuery.data?.hash, queryClient ]); }, [ addressHash, queryClient ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`, topic: `addresses:${ addressHash.toLowerCase() }`,
onSocketClose: handleSocketError, onSocketClose: handleSocketError,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: addressQuery.isLoading || addressQuery.isError || !addressQuery.data.hash || coinBalanceQuery.pagination.page !== 1, isDisabled: !addressHash || coinBalanceQuery.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -69,7 +64,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -69,7 +64,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
return ( return (
<> <>
{ socketAlert && <SocketAlert mb={ 6 }/> } { socketAlert && <SocketAlert mb={ 6 }/> }
<AddressCoinBalanceChart addressQuery={ addressQuery }/> <AddressCoinBalanceChart addressHash={ addressHash }/>
<AddressCoinBalanceHistory query={ coinBalanceQuery }/> <AddressCoinBalanceHistory query={ coinBalanceQuery }/>
</> </>
); );
......
...@@ -98,7 +98,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -98,7 +98,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
> >
<AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/> <AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at </Text> <Text whiteSpace="pre"> at </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } truncation="constant"/> <AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ addressQuery.data }/>
......
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { Address } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import ChartWidget from 'ui/shared/chart/ChartWidget'; import ChartWidget from 'ui/shared/chart/ChartWidget';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
interface Props { interface Props {
addressQuery: UseQueryResult<Address>; addressHash: string;
} }
const AddressCoinBalanceChart = ({ addressQuery }: Props) => { const AddressCoinBalanceChart = ({ addressHash }: Props) => {
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', { const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressQuery.data?.hash }, pathParams: { id: addressHash },
queryOptions: { enabled: Boolean(addressQuery.data?.hash) },
}); });
const items = React.useMemo(() => data?.map(({ date, value }) => ({ const items = React.useMemo(() => data?.map(({ date, value }) => ({
...@@ -23,12 +20,16 @@ const AddressCoinBalanceChart = ({ addressQuery }: Props) => { ...@@ -23,12 +20,16 @@ const AddressCoinBalanceChart = ({ addressQuery }: Props) => {
value: BigNumber(value).div(10 ** appConfig.network.currency.decimals).toNumber(), value: BigNumber(value).div(10 ** appConfig.network.currency.decimals).toNumber(),
})), [ data ]); })), [ data ]);
if (isError) {
return <DataFetchAlert/>;
}
return ( return (
<ChartWidget <ChartWidget
chartHeight="200px" chartHeight="200px"
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading || isError } isLoading={ isLoading }
/> />
); );
}; };
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination';
const SCROLL_PARAMS = {
elem: 'address-logs',
offset: -100,
};
const AddressLogs = () => {
const router = useRouter();
const addressHash = String(router.query?.id);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs',
pathParams: { id: addressHash },
scroll: SCROLL_PARAMS,
});
if (isError) {
return <DataFetchAlert/>;
}
const bar = isPaginationVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
if (isLoading) {
return (
<Box>
{ bar }
<LogSkeleton/>
<LogSkeleton/>
</Box>
);
}
if (data.items.length === 0) {
return <span>There are no logs for this address.</span>;
}
return (
<Element name={ SCROLL_PARAMS.elem }>
{ bar }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) }
</Element>
);
};
export default AddressLogs;
...@@ -26,7 +26,6 @@ test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, p ...@@ -26,7 +26,6 @@ test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, p
<ChainIndicators/> <ChainIndicators/>
</TestApp>, </TestApp>,
); );
await page.waitForResponse(STATS_API_URL),
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } }); await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } });
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
import { Flex, Tag } from '@chakra-ui/react'; import { Flex, Skeleton, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import notEmpty from 'lib/notEmpty';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs'; import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import AddressLogs from 'ui/address/logs/AddressLogs';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -30,28 +33,37 @@ const AddressPageContent = () => { ...@@ -30,28 +33,37 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []), ...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const tabs: Array<RoutedTab> = [ const isContract = addressQuery.data?.is_contract;
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> }, const tabs: Array<RoutedTab> = React.useMemo(() => {
{ id: 'tokens', title: 'Tokens', component: null }, return [
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
// temporary show this tab in all address { id: 'tokens', title: 'Tokens', component: null },
// later api will return info about available tabs { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> },
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated addressQuery={ addressQuery }/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
]; // temporary show this tab in all address
// later api will return info about available tabs
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> },
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs/> } : undefined,
].filter(notEmpty);
}, [ isContract ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
return ( return (
<Page> <Page>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle { addressQuery.isLoading ? (
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } <Skeleton h={ 10 } w="260px" mb={ 6 }/>
additionals={ tagsNode } ) : (
/> <PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionals={ tagsNode }
/>
) }
<AddressDetails addressQuery={ addressQuery }/> <AddressDetails addressQuery={ addressQuery }/>
<RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> { addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
</Page> </Page>
); );
}; };
......
import { test as base, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as textAdMock from 'mocks/ad/textAd';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
...@@ -26,6 +27,19 @@ const test = base.extend<socketServer.SocketServerFixture>({ ...@@ -26,6 +27,19 @@ const test = base.extend<socketServer.SocketServerFixture>({
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test.beforeEach(async({ page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: JSON.stringify(textAdMock.duck),
}));
await page.route(textAdMock.duck.ad.thumbnail, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
});
test('base view +@mobile +@dark-mode', async({ mount, page }) => { test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(BLOCKS_API_URL, (route) => route.fulfill({ await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200, status: 200,
......
...@@ -22,7 +22,7 @@ const Stats = () => { ...@@ -22,7 +22,7 @@ const Stats = () => {
return ( return (
<Page> <Page>
<PageTitle text={ `${ appConfig.network.name } Stats` }/> <PageTitle text={ `${ appConfig.network.name } stats` }/>
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<NumberWidgetsList/> <NumberWidgetsList/>
......
import { chakra, Icon, IconButton, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react'; import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import crossIcon from 'icons/cross.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import InputClearButton from 'ui/shared/InputClearButton';
type Props = { type Props = {
onChange: (searchTerm: string) => void; onChange: (searchTerm: string) => void;
...@@ -54,16 +54,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) = ...@@ -54,16 +54,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
{ filterQuery ? ( { filterQuery ? (
<InputRightElement> <InputRightElement>
<IconButton <InputClearButton onClick={ handleFilterQueryClear }/>
colorScheme="gray"
aria-label="Clear the filter input"
title="Clear the filter input"
w={ 6 }
h={ 6 }
icon={ <Icon as={ crossIcon } w={ 4 } h={ 4 } color={ iconColor }/> }
size="sm"
onClick={ handleFilterQueryClear }
/>
</InputRightElement> </InputRightElement>
) : null } ) : null }
</InputGroup> </InputGroup>
......
import { chakra, Icon, IconButton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import crossIcon from 'icons/cross.svg';
interface Props {
onClick: () => void;
}
const InputClearButton = ({ onClick }: Props) => {
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
return (
<IconButton
colorScheme="gray"
aria-label="Clear input"
title="Clear input"
boxSize={ 6 }
icon={ <Icon as={ crossIcon } boxSize={ 4 } color={ iconColor }/> }
size="sm"
onClick={ onClick }
/>
);
};
export default chakra(InputClearButton);
...@@ -30,7 +30,5 @@ test('with indexing alert +@mobile', async({ mount, page }) => { ...@@ -30,7 +30,5 @@ test('with indexing alert +@mobile', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Image, Center, chakra, useColorModeValue } from '@chakra-ui/react'; import { Image, chakra, useColorModeValue, Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import tokenPlaceholderIcon from 'icons/token-placeholder.svg';
const EmptyElement = ({ className, letter }: { className?: string; letter: string }) => { const EmptyElement = ({ className }: { className?: string }) => {
const bgColor = useColorModeValue('gray.200', 'gray.600'); const bgColor = useColorModeValue('gray.200', 'gray.600');
const color = useColorModeValue('gray.400', 'gray.200'); const color = useColorModeValue('gray.400', 'gray.200');
return ( return (
<Center <Icon
className={ className } className={ className }
fontWeight={ 600 } fontWeight={ 600 }
bgColor={ bgColor } bgColor={ bgColor }
color={ color } color={ color }
borderRadius="base" borderRadius="base"
> as={ tokenPlaceholderIcon }
{ letter.toUpperCase() } />
</Center>
); );
}; };
...@@ -41,7 +41,7 @@ const TokenLogo = ({ hash, name, className }: Props) => { ...@@ -41,7 +41,7 @@ const TokenLogo = ({ hash, name, className }: Props) => {
className={ className } className={ className }
src={ logoSrc } src={ logoSrc }
alt={ `${ name || 'token' } logo` } alt={ `${ name || 'token' } logo` }
fallback={ <EmptyElement className={ className } letter={ name?.slice(0, 1) || 'U' }/> } fallback={ <EmptyElement className={ className }/> }
/> />
); );
}; };
......
...@@ -9,12 +9,13 @@ interface Props { ...@@ -9,12 +9,13 @@ interface Props {
hash: string; hash: string;
name?: string | null; name?: string | null;
className?: string; className?: string;
logoSize?: number;
} }
const TokenSnippet = ({ symbol, hash, name, className }: Props) => { const TokenSnippet = ({ symbol, hash, name, className, logoSize = 6 }: Props) => {
return ( return (
<Flex className={ className } alignItems="center" columnGap={ 1 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/> <TokenLogo boxSize={ logoSize } hash={ hash } name={ name }/>
<AddressLink hash={ hash } alias={ name } type="token"/> <AddressLink hash={ hash } alias={ name } type="token"/>
{ symbol && <Text variant="secondary">({ symbol })</Text> } { symbol && <Text variant="secondary">({ symbol })</Text> }
</Flex> </Flex>
......
...@@ -51,28 +51,31 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -51,28 +51,31 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
}, []); }, []);
const handleFileSaveClick = useCallback(() => { const handleFileSaveClick = useCallback(() => {
if (ref.current) { // wait for context menu to close
domToImage.toPng(ref.current, setTimeout(() => {
{ if (ref.current) {
quality: 100, domToImage.toPng(ref.current,
bgcolor: pngBackgroundColor, {
width: ref.current.offsetWidth * DOWNLOAD_IMAGE_SCALE, quality: 100,
height: ref.current.offsetHeight * DOWNLOAD_IMAGE_SCALE, bgcolor: pngBackgroundColor,
filter: (node) => node.nodeName !== 'BUTTON', width: ref.current.offsetWidth * DOWNLOAD_IMAGE_SCALE,
style: { height: ref.current.offsetHeight * DOWNLOAD_IMAGE_SCALE,
borderColor: 'transparent', filter: (node) => node.nodeName !== 'BUTTON',
transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`, style: {
'transform-origin': 'top left', borderColor: 'transparent',
}, transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`,
}) 'transform-origin': 'top left',
.then((dataUrl) => { },
const link = document.createElement('a'); })
link.download = `${ title } (Blockscout chart).png`; .then((dataUrl) => {
link.href = dataUrl; const link = document.createElement('a');
link.click(); link.download = `${ title } (Blockscout chart).png`;
link.remove(); link.href = dataUrl;
}); link.click();
} link.remove();
});
}
}, 100);
}, [ pngBackgroundColor, title ]); }, [ pngBackgroundColor, title ]);
const handleSVGSavingClick = useCallback(() => { const handleSVGSavingClick = useCallback(() => {
......
...@@ -29,9 +29,9 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -29,9 +29,9 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const color = useToken('colors', 'blue.200'); const color = useToken('colors', 'blue.200');
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const chartId = useMemo(() => `chart-${ title.split(' ').join('') }`, [ title ]);
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);
const chartId = `chart-${ title.split(' ').join('') }-${ isEnlarged ? 'fullscreen' : 'small' }`;
const displayedData = useMemo(() => items.slice(range[0], range[1]), [ items, range ]); const displayedData = useMemo(() => items.slice(range[0], range[1]), [ items, range ]);
const chartData = [ { items: items, name: 'Value', color } ]; const chartData = [ { items: items, name: 'Value', color } ];
......
...@@ -3,6 +3,8 @@ import { useMemo } from 'react'; ...@@ -3,6 +3,8 @@ import { useMemo } from 'react';
import type { TimeChartData } from 'ui/shared/chart/types'; import type { TimeChartData } from 'ui/shared/chart/types';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
interface Props { interface Props {
data: TimeChartData; data: TimeChartData;
width: number; width: number;
...@@ -50,7 +52,7 @@ export default function useTimeChartController({ data, width, height }: Props) { ...@@ -50,7 +52,7 @@ export default function useTimeChartController({ data, width, height }: Props) {
); );
const xTickFormat = (d: d3.AxisDomain) => d.toLocaleString(); const xTickFormat = (d: d3.AxisDomain) => d.toLocaleString();
const yTickFormat = (d: d3.AxisDomain) => d.toLocaleString(); const yTickFormat = (d: d3.AxisDomain) => formatNumberToMetricPrefix(Number(d));
return { return {
xTickFormat, xTickFormat,
......
...@@ -4,12 +4,12 @@ import React from 'react'; ...@@ -4,12 +4,12 @@ import React from 'react';
import * as mocks from 'mocks/txs/decodedInputData'; import * as mocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import TxDecodedInputData from './TxDecodedInputData'; import LogDecodedInputData from './LogDecodedInputData';
test('with indexed fields +@mobile +@dark-mode', async({ mount }) => { test('with indexed fields +@mobile +@dark-mode', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDecodedInputData data={ mocks.withIndexedFields }/> <LogDecodedInputData data={ mocks.withIndexedFields }/>
</TestApp>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -18,7 +18,7 @@ test('with indexed fields +@mobile +@dark-mode', async({ mount }) => { ...@@ -18,7 +18,7 @@ test('with indexed fields +@mobile +@dark-mode', async({ mount }) => {
test('without indexed fields +@mobile', async({ mount }) => { test('without indexed fields +@mobile', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxDecodedInputData data={ mocks.withoutIndexedFields }/> <LogDecodedInputData data={ mocks.withoutIndexedFields }/>
</TestApp>, </TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
...@@ -68,7 +68,7 @@ interface Props { ...@@ -68,7 +68,7 @@ interface Props {
data: DecodedInput; data: DecodedInput;
} }
const TxDecodedInputData = ({ data }: Props) => { const LogDecodedInputData = ({ data }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hasIndexed = data.parameters.some(({ indexed }) => indexed !== undefined); const hasIndexed = data.parameters.some(({ indexed }) => indexed !== undefined);
...@@ -175,4 +175,4 @@ const TxDecodedInputData = ({ data }: Props) => { ...@@ -175,4 +175,4 @@ const TxDecodedInputData = ({ data }: Props) => {
); );
}; };
export default TxDecodedInputData; export default LogDecodedInputData;
...@@ -5,7 +5,7 @@ import * as addressMocks from 'mocks/address/address'; ...@@ -5,7 +5,7 @@ import * as addressMocks from 'mocks/address/address';
import * as inputDataMocks from 'mocks/txs/decodedInputData'; import * as inputDataMocks from 'mocks/txs/decodedInputData';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import TxLogItem from './TxLogItem'; import LogItem from './LogItem';
const TOPICS = [ const TOPICS = [
'0x3a4ec416703c36a61a4b1f690847f1963a6829eac0b52debd40a23b66c142a56', '0x3a4ec416703c36a61a4b1f690847f1963a6829eac0b52debd40a23b66c142a56',
...@@ -18,12 +18,13 @@ const DATA = '0x0000000000000000000000000000000000000000000000000070265bf0112cee ...@@ -18,12 +18,13 @@ const DATA = '0x0000000000000000000000000000000000000000000000000070265bf0112cee
test('with decoded input data +@mobile +@dark-mode', async({ mount }) => { test('with decoded input data +@mobile +@dark-mode', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxLogItem <LogItem
index={ 42 } index={ 42 }
decoded={ inputDataMocks.withIndexedFields } decoded={ inputDataMocks.withIndexedFields }
address={ addressMocks.withName } address={ addressMocks.withName }
topics={ TOPICS } topics={ TOPICS }
data={ DATA } data={ DATA }
type="tx"
/> />
</TestApp>, </TestApp>,
); );
...@@ -33,12 +34,13 @@ test('with decoded input data +@mobile +@dark-mode', async({ mount }) => { ...@@ -33,12 +34,13 @@ test('with decoded input data +@mobile +@dark-mode', async({ mount }) => {
test('without decoded input data +@mobile', async({ mount }) => { test('without decoded input data +@mobile', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxLogItem <LogItem
index={ 42 } index={ 42 }
decoded={ null } decoded={ null }
address={ addressMocks.withoutName } address={ addressMocks.withoutName }
topics={ TOPICS } topics={ TOPICS }
data={ DATA } data={ DATA }
type="tx"
/> />
</TestApp>, </TestApp>,
); );
......
...@@ -10,10 +10,12 @@ import notEmpty from 'lib/notEmpty'; ...@@ -10,10 +10,12 @@ import notEmpty from 'lib/notEmpty';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic'; import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import DecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData'; import LogTopic from 'ui/shared/logs/LogTopic';
type Props = Log; type Props = Log & {
type: 'address' | 'tx';
};
const RowHeader = ({ children }: { children: React.ReactNode }) => ( const RowHeader = ({ children }: { children: React.ReactNode }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}> <GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
...@@ -21,7 +23,7 @@ const RowHeader = ({ children }: { children: React.ReactNode }) => ( ...@@ -21,7 +23,7 @@ const RowHeader = ({ children }: { children: React.ReactNode }) => (
</GridItem> </GridItem>
); );
const TxLogItem = ({ address, index, topics, data, decoded }: Props) => { const TxLogItem = ({ address, index, topics, data, decoded, type }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
...@@ -39,7 +41,7 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => { ...@@ -39,7 +41,7 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
pt: 0, pt: 0,
}} }}
> >
{ !decoded && ( { !decoded && type === 'tx' && (
<GridItem colSpan={{ base: 1, lg: 2 }}> <GridItem colSpan={{ base: 1, lg: 2 }}>
<Alert status="warning" display="inline-table" whiteSpace="normal"> <Alert status="warning" display="inline-table" whiteSpace="normal">
To see accurate decoded input data, the contract must be verified.{ space } To see accurate decoded input data, the contract must be verified.{ space }
...@@ -69,14 +71,14 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => { ...@@ -69,14 +71,14 @@ const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
<> <>
<RowHeader>Decode input data</RowHeader> <RowHeader>Decode input data</RowHeader>
<GridItem> <GridItem>
<DecodedInputData data={ decoded }/> <LogDecodedInputData data={ decoded }/>
</GridItem> </GridItem>
</> </>
) } ) }
<RowHeader>Topics</RowHeader> <RowHeader>Topics</RowHeader>
<GridItem> <GridItem>
{ topics.filter(notEmpty).map((item, index) => ( { topics.filter(notEmpty).map((item, index) => (
<TxLogTopic <LogTopic
key={ index } key={ index }
hex={ item } hex={ item }
index={ index } index={ index }
......
...@@ -15,7 +15,7 @@ const TopicRow = () => ( ...@@ -15,7 +15,7 @@ const TopicRow = () => (
</Flex> </Flex>
); );
const TxLogSkeleton = () => { const LogSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
...@@ -55,4 +55,4 @@ const TxLogSkeleton = () => { ...@@ -55,4 +55,4 @@ const TxLogSkeleton = () => {
); );
}; };
export default TxLogSkeleton; export default LogSkeleton;
...@@ -3,12 +3,12 @@ import React from 'react'; ...@@ -3,12 +3,12 @@ import React from 'react';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import TxLogTopic from './TxLogTopic'; import LogTopic from './LogTopic';
test('address view +@mobile -@default', async({ mount }) => { test('address view +@mobile -@default', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/> <LogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>, </TestApp>,
); );
await component.locator('select[aria-label="Data type"]').selectOption('address'); await component.locator('select[aria-label="Data type"]').selectOption('address');
...@@ -19,7 +19,7 @@ test('address view +@mobile -@default', async({ mount }) => { ...@@ -19,7 +19,7 @@ test('address view +@mobile -@default', async({ mount }) => {
test('hex view +@mobile -@default', async({ mount }) => { test('hex view +@mobile -@default', async({ mount }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<TxLogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/> <LogTopic hex="0x000000000000000000000000d789a607ceac2f0e14867de4eb15b15c9ffb5859" index={ 42 }/>
</TestApp>, </TestApp>,
); );
await component.locator('select[aria-label="Data type"]').selectOption('hex'); await component.locator('select[aria-label="Data type"]').selectOption('hex');
......
...@@ -24,7 +24,7 @@ const VALUE_CONVERTERS: Record<DataType, (hex: string) => string> = { ...@@ -24,7 +24,7 @@ const VALUE_CONVERTERS: Record<DataType, (hex: string) => string> = {
}; };
const OPTIONS: Array<DataType> = [ 'hex', 'address', 'text', 'number' ]; const OPTIONS: Array<DataType> = [ 'hex', 'address', 'text', 'number' ];
const TxLogTopic = ({ hex, index }: Props) => { const LogTopic = ({ hex, index }: Props) => {
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('hex'); const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('hex');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
...@@ -84,4 +84,4 @@ const TxLogTopic = ({ hex, index }: Props) => { ...@@ -84,4 +84,4 @@ const TxLogTopic = ({ hex, index }: Props) => {
); );
}; };
export default React.memo(TxLogTopic); export default React.memo(LogTopic);
import { Flex, Skeleton, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
}
const SkeletonTabs = ({ className }: Props) => {
return (
<Flex my={ 8 } className={ className }>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px"/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="120px"/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="80px" display={{ base: 'none', lg: 'block' }}/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="100px" display={{ base: 'none', lg: 'block' }}/>
<Skeleton h={ 6 } my={ 2 } mx={ 4 } w="140px" display={{ base: 'none', lg: 'block' }}/>
</Flex>
);
};
export default chakra(SkeletonTabs);
...@@ -44,10 +44,10 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => { ...@@ -44,10 +44,10 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
> >
{ section.charts.map((chart) => ( { section.charts.map((chart) => (
<GridItem <GridItem
key={ chart.id } key={ chart.apiId }
> >
<ChartWidgetContainer <ChartWidgetContainer
id={ chart.id } id={ chart.apiId }
title={ chart.title } title={ chart.title }
description={ chart.description } description={ chart.description }
interval={ interval } interval={ interval }
......
...@@ -2,6 +2,7 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Grid } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
import { numberWidgetsScheme } from './constants/number-widgets-scheme'; import { numberWidgetsScheme } from './constants/number-widgets-scheme';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
...@@ -12,15 +13,32 @@ const skeletonsCount = 8; ...@@ -12,15 +13,32 @@ const skeletonsCount = 8;
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const { data, isLoading } = useApiQuery('stats_counters'); const { data, isLoading } = useApiQuery('stats_counters');
const skeletonElement = [ ...Array(skeletonsCount) ]
.map((e, i) => <NumberWidgetSkeleton key={ i }/>);
return ( return (
<Grid <Grid
gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }} gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
gridGap={ 4 } gridGap={ 4 }
> >
{ isLoading ? [ ...Array(skeletonsCount) ] { isLoading ? skeletonElement :
.map((e, i) => <NumberWidgetSkeleton key={ i }/>) : numberWidgetsScheme.map(({ id, title, formatFn }) => {
numberWidgetsScheme.map(({ id, title }) => if (!data?.counters[id]) {
data?.counters[id] ? <NumberWidget key={ id } label={ title } value={ Number(data.counters[id]).toLocaleString() }/> : null) } return null;
}
const value = formatNumberToMetricPrefix(Number(data.counters[id]));
return (
<NumberWidget
key={ id }
label={ title }
value={ formatFn ?
formatFn(value) :
value }
/>
);
}) }
</Grid> </Grid>
); );
}; };
......
import type { StatsSection } from 'types/client/stats'; import type { StatsSection } from 'types/client/stats';
export const statsChartsScheme: Array<StatsSection> = [ 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', id: 'blocks',
title: 'Blocks', title: 'Blocks',
charts: [ charts: [
{ {
id: 'newBlocksPerDay', apiId: 'newBlocksPerDay',
title: 'New blocks', title: 'New blocks',
description: 'New blocks number per day', 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',
}, },
// {
// id: 'average-block-size',
// title: 'Average block size',
// description: 'Average size of blocks in bytes',
// },
], ],
}, },
// {
// id: 'transactions',
// title: 'Transactions',
// charts: [
// {
// id: 'average-transaction-fee',
// title: 'Average transaction fee',
// description: 'The average amount in USD spent per transaction',
// },
// {
// id: 'transactions-fees',
// title: 'Transactions fees',
// description: 'Amount of tokens paid as fees',
// },
// {
// id: 'new-transactions',
// title: 'Transactions fees',
// description: 'New transactions number per period',
// },
// {
// id: 'transactions-growth',
// title: 'Transactions growth',
// description: 'Cumulative transactions number per period',
// },
// ],
// },
// {
// id: 'accounts',
// title: 'Accounts',
// charts: [
// {
// id: 'active-accounts',
// title: 'Active accounts',
// description: 'Active accounts number per period',
// },
// {
// id: 'accounts-growth',
// title: 'Accounts growth',
// description: 'Cumulative accounts number per period',
// },
// ],
// },
]; ];
...@@ -2,7 +2,7 @@ import type { Stats } from 'types/api/stats'; ...@@ -2,7 +2,7 @@ import type { Stats } from 'types/api/stats';
type Key = keyof Stats['counters']; type Key = keyof Stats['counters'];
export const numberWidgetsScheme: Array<{id: Key; title: string}> = [ export const numberWidgetsScheme: Array<{id: Key; title: string; formatFn?: (n: string) => string}> = [
{ {
id: 'totalBlocks', id: 'totalBlocks',
title: 'Total blocks', title: 'Total blocks',
...@@ -10,6 +10,7 @@ export const numberWidgetsScheme: Array<{id: Key; title: string}> = [ ...@@ -10,6 +10,7 @@ export const numberWidgetsScheme: Array<{id: Key; title: string}> = [
{ {
id: 'averageBlockTime', id: 'averageBlockTime',
title: 'Average block time', title: 'Average block time',
formatFn: (n) => `${ n } s`,
}, },
{ {
id: 'totalTransactions', id: 'totalTransactions',
...@@ -35,8 +36,4 @@ export const numberWidgetsScheme: Array<{id: Key; title: string}> = [ ...@@ -35,8 +36,4 @@ export const numberWidgetsScheme: Array<{id: Key; title: string}> = [
id: 'totalNativeCoinTransfers', id: 'totalNativeCoinTransfers',
title: 'Total native coin transfers', title: 'Total native coin transfers',
}, },
{
id: 'totalAccounts',
title: 'Total accounts',
},
]; ];
...@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props) ...@@ -26,7 +26,7 @@ const NftTokenTransferSnippet = ({ value, name, hash, symbol, tokenId }: Props)
<Link href={ url } fontWeight={ 600 }>{ tokenId }</Link> <Link href={ url } fontWeight={ 600 }>{ tokenId }</Link>
</Box> </Box>
{ name ? ( { name ? (
<TokenSnippet symbol={ symbol } hash={ hash } name={ name } w="auto"/> <TokenSnippet symbol={ symbol } hash={ hash } name={ name } w="auto" logoSize={ 5 } columnGap={ 1 }/>
) : ( ) : (
<AddressLink hash={ hash } truncation="constant" type="token"/> <AddressLink hash={ hash } truncation="constant" type="token"/>
) } ) }
......
...@@ -35,6 +35,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; ...@@ -35,6 +35,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
// import PrevNext from 'ui/shared/PrevNext'; // import PrevNext from 'ui/shared/PrevNext';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
...@@ -42,7 +43,6 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -42,7 +43,6 @@ import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton'; import TxDetailsSkeleton from 'ui/tx/details/TxDetailsSkeleton';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers'; import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
...@@ -383,7 +383,7 @@ const TxDetails = () => { ...@@ -383,7 +383,7 @@ const TxDetails = () => {
title="Decoded input data" title="Decoded input data"
hint="Decoded input data" hint="Decoded input data"
> >
<TxDecodedInputData data={ data.decoded_input }/> <LogDecodedInputData data={ data.decoded_input }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
</> </>
......
...@@ -5,9 +5,9 @@ import { SECOND } from 'lib/consts'; ...@@ -5,9 +5,9 @@ import { SECOND } from 'lib/consts';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import LogItem from 'ui/shared/logs/LogItem';
import LogSkeleton from 'ui/shared/logs/LogSkeleton';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
...@@ -33,8 +33,8 @@ const TxLogs = () => { ...@@ -33,8 +33,8 @@ const TxLogs = () => {
if (isLoading || txInfo.isLoading) { if (isLoading || txInfo.isLoading) {
return ( return (
<Box> <Box>
<TxLogSkeleton/> <LogSkeleton/>
<TxLogSkeleton/> <LogSkeleton/>
</Box> </Box>
); );
} }
...@@ -50,7 +50,7 @@ const TxLogs = () => { ...@@ -50,7 +50,7 @@ const TxLogs = () => {
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) } ) }
{ data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) } { data.items.map((item, index) => <LogItem key={ index } { ...item } type="tx"/>) }
</Box> </Box>
); );
}; };
......
...@@ -25,7 +25,15 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => { ...@@ -25,7 +25,15 @@ const TxDetailsTokenTransfer = ({ token, total, to, from }: Props) => {
<Text fontWeight={ 500 } as="span">For:{ space } <Text fontWeight={ 500 } as="span">For:{ space }
<CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/> <CurrencyValue value={ payload.value } exchangeRate={ token.exchange_rate } fontWeight={ 600 }/>
</Text> </Text>
<TokenSnippet symbol={ token.symbol } hash={ token.address } name={ token.name } w="auto" flexGrow="1"/> <TokenSnippet
symbol={ token.symbol }
hash={ token.address }
name={ token.name }
w="auto"
flexGrow="1"
columnGap={ 1 }
logoSize={ 5 }
/>
</Flex> </Flex>
); );
} }
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { TransactionRevertReason } from 'types/api/transaction'; import type { TransactionRevertReason } from 'types/api/transaction';
import hexToUtf8 from 'lib/hexToUtf8'; import hexToUtf8 from 'lib/hexToUtf8';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData/TxDecodedInputData'; import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
type Props = TransactionRevertReason; type Props = TransactionRevertReason;
...@@ -31,7 +31,7 @@ const TxRevertReason = (props: Props) => { ...@@ -31,7 +31,7 @@ const TxRevertReason = (props: Props) => {
); );
} }
return <TxDecodedInputData data={ props }/>; return <LogDecodedInputData data={ props }/>;
}; };
export default React.memo(TxRevertReason); export default React.memo(TxRevertReason);
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