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 = {
creator_address_hash: null,
exchange_rate: 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 {
creator_address_hash: string | null;
creation_tx_hash: 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;
implementation_address: string | null;
implementation_name: string | null;
......
......@@ -22,7 +22,11 @@ import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
const AddressBlocksValidated = () => {
interface Props {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressBlocksValidated = ({ scrollRef }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(false);
const queryClient = useQueryClient();
const router = useRouter();
......@@ -31,6 +35,7 @@ const AddressBlocksValidated = () => {
const query = useQueryWithPages({
resourceName: 'address_blocks_validated',
pathParams: { id: addressHash },
scrollRef,
});
const handleSocketError = React.useCallback(() => {
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -11,6 +10,7 @@ import blockIcon from 'icons/block.svg';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
......@@ -29,9 +29,10 @@ import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
addressQuery: UseQueryResult<TAddress>;
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressDetails = ({ addressQuery }: Props) => {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter();
const isMobile = useIsMobile();
......@@ -41,27 +42,27 @@ const AddressDetails = ({ addressQuery }: Props) => {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
});
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
});
const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little
scrollRef?.current?.scrollIntoView({ behavior: 'smooth' });
}, 500);
}, [ scrollRef ]);
if (addressQuery.isError) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
}
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
if (addressQuery.isLoading) {
return <AddressDetailsSkeleton/>;
}
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) {
if (addressQuery.isError) {
return <DataFetchAlert/>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const validationsCount = Number(countersQuery.data.validations_count);
return (
<Box>
......@@ -104,6 +105,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
{ addressQuery.data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
......@@ -112,30 +114,33 @@ const AddressDetails = ({ addressQuery }: Props) => {
>
<TokenSelect/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
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>
{ addressQuery.data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfers_count).toLocaleString() }
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Gas used"
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>
{ !Object.is(validationsCount, NaN) && validationsCount > 0 && (
{ addressQuery.data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
{ validationsCount.toLocaleString() }
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/>
</DetailsInfoItem>
) }
{ 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 { useQueryClient, useIsFetching } from '@tanstack/react-query';
import NextLink from 'next/link';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -9,6 +10,7 @@ import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -21,7 +23,8 @@ const TokenSelect = () => {
const queryClient = useQueryClient();
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);
......@@ -75,6 +78,8 @@ const TokenSelect = () => {
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
}
<Tooltip label="Show all tokens">
<Box>
<NextLink href={ link('address_index', { id: addressHash }, { tab: 'tokens' }) } passHref>
<IconButton
aria-label="Show all tokens"
variant="outline"
......@@ -82,7 +87,10 @@ const TokenSelect = () => {
pl="6px"
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
/>
</NextLink>
</Box>
</Tooltip>
</Flex>
);
......
......@@ -21,15 +21,6 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
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 router = useRouter();
......@@ -46,27 +37,54 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []),
].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(() => {
return [
{ id: 'txs', title: 'Transactions', component: <AddressTxs scrollRef={ tabsScrollRef }/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> },
{ id: 'tokens', title: 'Tokens', component: null },
addressQuery.data?.has_token_transfers ?
{ 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: '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 scrollRef={ tabsScrollRef }/> } : undefined,
isContract ? {
addressQuery.data?.has_validated_blocks ?
{ id: 'blocks_validated', title: 'Blocks validated', component: <AddressBlocksValidated scrollRef={ tabsScrollRef }/> } :
undefined,
addressQuery.data?.has_logs ? { id: 'logs', title: 'Logs', component: <AddressLogs scrollRef={ tabsScrollRef }/> } : undefined,
addressQuery.data?.is_contract ? {
id: 'contract',
title: 'Contract',
component: <AddressContract tabs={ CONTRACT_TABS }/>,
subTabs: CONTRACT_TABS,
component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs,
} : undefined,
].filter(notEmpty);
}, [ isContract ]);
}, [ addressQuery.data, contractTabs ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
......@@ -81,7 +99,7 @@ const AddressPageContent = () => {
additionals={ tagsNode }
/>
) }
<AddressDetails addressQuery={ addressQuery }/>
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> }
......
......@@ -30,7 +30,7 @@ const ActionBar = ({ children, className }: Props) => {
position="sticky"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow"
transitionDuration="normal"
zIndex={{ base: 'sticky2', lg: 'docked' }}
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
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