Commit 0bfec08a authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #494 from blockscout/address-tabs-conditions

address tabs improvements
parents aed32ea0 3f526187
...@@ -41,4 +41,15 @@ export const withToken: Address = { ...@@ -41,4 +41,15 @@ export const withToken: Address = {
creator_address_hash: null, creator_address_hash: null,
exchange_rate: null, exchange_rate: null,
implementation_address: null, implementation_address: null,
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: true,
has_validated_blocks: false,
}; };
...@@ -12,6 +12,17 @@ export interface Address { ...@@ -12,6 +12,17 @@ export interface Address {
creator_address_hash: string | null; creator_address_hash: string | null;
creation_tx_hash: string | null; creation_tx_hash: string | null;
exchange_rate: string | null; exchange_rate: string | null;
has_custom_methods_read: boolean;
has_custom_methods_write: boolean;
has_decompiled_code: boolean;
has_logs: boolean;
has_methods_read: boolean;
has_methods_read_proxy: boolean;
has_methods_write: boolean;
has_methods_write_proxy: boolean;
has_token_transfers: boolean;
has_tokens: boolean;
has_validated_blocks: boolean;
hash: string; hash: string;
implementation_address: string | null; implementation_address: string | null;
implementation_name: string | null; implementation_name: string | null;
......
...@@ -22,7 +22,11 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -22,7 +22,11 @@ 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';
const AddressBlocksValidated = () => { interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressBlocksValidated = ({ scrollRef }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false); const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
...@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => { ...@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => {
const query = useQueryWithPages({ const query = useQueryWithPages({
resourceName: 'address_blocks_validated', resourceName: 'address_blocks_validated',
pathParams: { id: addressHash }, pathParams: { id: addressHash },
scrollRef,
}); });
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg'; ...@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
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 CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect'; ...@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
addressQuery: UseQueryResult<TAddress>; addressQuery: UseQueryResult<TAddress>;
scrollRef?: React.RefObject<HTMLDivElement>;
} }
const AddressDetails = ({ addressQuery }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
}); });
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() }, const handleCounterItemClick = React.useCallback(() => {
queryOptions: { window.setTimeout(() => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), // cannot do scroll instantly, have to wait a little
}, scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}); }, 500);
}, [ scrollRef ]);
if (addressQuery.isError) { if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
} }
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) { if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>; return <AddressDetailsSkeleton/>;
} }
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) { if (addressQuery.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address); const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const validationsCount = Number(countersQuery.data.validations_count);
return ( return (
<Box> <Box>
...@@ -104,38 +105,42 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -104,38 +105,42 @@ const AddressDetails = ({ addressQuery }: Props) => {
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ addressQuery.data }/>
<DetailsInfoItem { addressQuery.data.has_tokens && (
title="Tokens" <DetailsInfoItem
hint="All tokens in the account and total value." title="Tokens"
alignSelf="center" hint="All tokens in the account and total value."
py={ 0 } alignSelf="center"
> py={ 0 }
<TokenSelect/> >
</DetailsInfoItem> <TokenSelect/>
</DetailsInfoItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address." hint="Number of transactions related to this address."
> >
{ Number(countersQuery.data.transactions_count).toLocaleString() } <AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfers_count).toLocaleString() }
</DetailsInfoItem> </DetailsInfoItem>
{ addressQuery.data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
) }
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
hint="Gas used by the address." hint="Gas used by the address."
> >
{ BigNumber(countersQuery.data.gas_usage_count).toFormat() } <AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem> </DetailsInfoItem>
{ !Object.is(validationsCount, NaN) && validationsCount > 0 && ( { addressQuery.data.has_validated_blocks && (
<DetailsInfoItem <DetailsInfoItem
title="Blocks validated" title="Blocks validated"
hint="Number of blocks validated by this validator." hint="Number of blocks validated by this validator."
> >
{ validationsCount.toLocaleString() } <AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.block_number_balance_updated_at && ( { addressQuery.data.block_number_balance_updated_at && (
......
import { Link, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import NextLink from 'next/link';
import React from 'react';
import type { AddressCounters } from 'types/api/address';
import link from 'lib/link/link';
interface Props {
prop: keyof AddressCounters;
query: UseQueryResult<AddressCounters>;
address: string;
onClick: () => void;
}
const PROP_TO_TAB = {
transactions_count: 'txs',
token_transfers_count: 'token_transfers',
validations_count: 'blocks_validated',
};
const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
if (query.isLoading) {
return <Skeleton h={ 5 } w="80px" borderRadius="full"/>;
}
const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) {
return <span>no data</span>;
}
switch (prop) {
case 'gas_usage_count':
return <span>{ BigNumber(data).toFormat() }</span>;
case 'transactions_count':
case 'token_transfers_count':
case 'validations_count': {
if (data === '0') {
return <span>0</span>;
}
return (
<NextLink href={ link('address_index', { id: address }, { tab: PROP_TO_TAB[prop] }) } passHref>
<Link onClick={ onClick }>
{ Number(data).toLocaleString() }
</Link>
</NextLink>
);
}
}
};
export default React.memo(AddressCounterItem);
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address'; ...@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -21,7 +23,8 @@ const TokenSelect = () => { ...@@ -21,7 +23,8 @@ const TokenSelect = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>(); const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressResourceKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } }); const addressHash = router.query.id?.toString();
const addressResourceKey = getResourceKey('address', { pathParams: { id: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
...@@ -75,14 +78,19 @@ const TokenSelect = () => { ...@@ -75,14 +78,19 @@ const TokenSelect = () => {
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
} }
<Tooltip label="Show all tokens"> <Tooltip label="Show all tokens">
<IconButton <Box>
aria-label="Show all tokens" <NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref>
variant="outline" <IconButton
size="sm" aria-label="Show all tokens"
pl="6px" variant="outline"
pr="6px" size="sm"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } pl="6px"
/> pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
/>
</NextLink>
</Box>
</Tooltip> </Tooltip>
</Flex> </Flex>
); );
......
...@@ -21,15 +21,6 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -21,15 +21,6 @@ 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'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
const CONTRACT_TABS = [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> },
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> },
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> },
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> },
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> },
];
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -46,27 +37,54 @@ const AddressPageContent = () => { ...@@ -46,27 +37,54 @@ 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 isContract = addressQuery.data?.is_contract; const contractTabs = React.useMemo(() => {
return [
{ id: 'contact_code', title: 'Code', component: <ContractCode/> },
addressQuery.data?.has_decompiled_code ?
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
undefined,
addressQuery.data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> } :
undefined,
addressQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> } :
undefined,
addressQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom methods', component: <div>Read custom methods</div> } :
undefined,
addressQuery.data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> } :
undefined,
addressQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> } :
undefined,
addressQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom methods', component: <div>Write custom methods</div> } :
undefined,
].filter(notEmpty);
}, [ addressQuery.data ]);
const tabs: Array<RoutedTab> = React.useMemo(() => { const tabs: Array<RoutedTab> = React.useMemo(() => {
return [ return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> }, addressQuery.data?.has_token_transfers ?
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: null } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> }, { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
// temporary show this tab in all address addressQuery.data?.has_validated_blocks ?
// later api will return info about available tabs { id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } :
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated/> }, undefined,
isContract ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined, addressQuery.data?.has_logs ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: 'Contract', title: 'Contract',
component: <AddressContract tabs={ CONTRACT_TABS }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: CONTRACT_TABS, subTabs: contractTabs,
} : undefined, } : undefined,
].filter(notEmpty); ].filter(notEmpty);
}, [ isContract ]); }, [ addressQuery.data, contractTabs ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
...@@ -81,7 +99,7 @@ const AddressPageContent = () => { ...@@ -81,7 +99,7 @@ const AddressPageContent = () => {
additionals={ tagsNode } additionals={ tagsNode }
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> } { addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
......
...@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => { ...@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => {
position="sticky" position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }} top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow,background-color,color" transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow" transitionDuration="normal"
zIndex={{ base: 'sticky2', lg: 'docked' }} zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }} boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
ref={ ref } ref={ ref }
......
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