Commit c2165f1e authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into txs-fixes

parents 495970c3 6c2f65d5
...@@ -35,7 +35,31 @@ const oldUrls = [ ...@@ -35,7 +35,31 @@ const oldUrls = [
}, },
{ {
oldPath: '/block/:id/transactions', oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`, newPath: `${ PATHS.block }`,
},
{
oldPath: '/address/:id/transactions',
newPath: `${ PATHS.address_index }`,
},
{
oldPath: '/address/:id/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers`,
},
{
oldPath: '/address/:id/tokens',
newPath: `${ PATHS.address_index }?tab=tokens`,
},
{
oldPath: '/address/:id/internal-transactions',
newPath: `${ PATHS.address_index }?tab=internal_txns`,
},
{
oldPath: '/address/:id/coin-balances',
newPath: `${ PATHS.address_index }?tab=coin_balance_history`,
},
{
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
}, },
]; ];
......
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12.18 3.765-1.944-1.944a1.674 1.674 0 0 0-1.179-.488H4.333c-.92 0-1.666.746-1.666 1.667v10c0 .92.746 1.667 1.667 1.667H11c.917 0 1.667-.75 1.667-1.667V4.943c0-.44-.175-.865-.487-1.178ZM11.417 13c0 .23-.187.417-.417.417H4.334A.417.417 0 0 1 3.917 13V3.003c0-.23.186-.416.416-.416H8.5v2.08c0 .46.373.833.833.833h2.06V13h.024ZM8.273 8.938a.365.365 0 0 0-.303.162l-1.032 1.55-.305-.456a.363.363 0 0 0-.606-.001l-1.215 1.823a.364.364 0 0 0-.018.374c.063.12.186.192.297.192h5.104a.365.365 0 0 0 .304-.566L8.554 9.1c-.044-.103-.158-.162-.28-.162ZM6 8.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.667 2A.667.667 0 0 0 2 2.667V6a.667.667 0 0 0 1.333 0V3.333H6A.667.667 0 1 0 6 2H2.667Zm0 12A.667.667 0 0 1 2 13.333V10a.667.667 0 0 1 1.333 0v2.667H6A.667.667 0 1 1 6 14H2.667ZM14 2.667A.667.667 0 0 0 13.333 2H10a.667.667 0 0 0 0 1.333h2.667V6A.667.667 0 1 0 14 6V2.667ZM13.333 14a.667.667 0 0 0 .667-.667V10a.667.667 0 0 0-1.333 0v2.667H10A.667.667 0 1 0 10 14h3.333Z" fill="currentColor"/>
</svg>
...@@ -41,10 +41,11 @@ function getUpdateParams(ts: string) { ...@@ -41,10 +41,11 @@ function getUpdateParams(ts: string) {
}; };
} }
export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) { export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(dayjs(ts).fromNow()); const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null);
React.useEffect(() => { React.useEffect(() => {
if (ts !== null) {
const timeouts: Array<number> = []; const timeouts: Array<number> = [];
const intervals: Array<number> = []; const intervals: Array<number> = [];
...@@ -81,6 +82,7 @@ export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) { ...@@ -81,6 +82,7 @@ export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) {
timeouts.forEach(window.clearTimeout); timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval); intervals.forEach(window.clearInterval);
}; };
}
}, [ isEnabled, ts ]); }, [ isEnabled, ts ]);
return value; return value;
......
...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e ...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage { export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>; export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
......
...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = { ...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = {
}, },
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = { ...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = {
}, },
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = { ...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = {
}, },
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting', type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155multiple: TokenTransfer = { export const erc1155multiple: TokenTransfer = {
......
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/indexing-status';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
export type IndexingStatus = {
finished_indexing: boolean;
finished_indexing_blocks: boolean;
indexed_blocks_ratio: string;
indexed_inernal_transactions_ratio: string;
}
...@@ -36,6 +36,7 @@ interface TokenTransferBase { ...@@ -36,6 +36,7 @@ interface TokenTransferBase {
tx_hash: string; tx_hash: string;
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
timestamp: string;
} }
export type TokenTransferPagination = { export type TokenTransferPagination = {
......
...@@ -4,6 +4,7 @@ export enum QueryKeys { ...@@ -4,6 +4,7 @@ export enum QueryKeys {
txsValidate = 'txs-validated', txsValidate = 'txs-validated',
txsPending = 'txs-pending', txsPending = 'txs-pending',
homeStats='homeStats', homeStats='homeStats',
indexingStatus='indexingStatus',
stats='stats', stats='stats',
charts='stats', charts='stats',
tx = 'tx', tx = 'tx',
......
...@@ -16,6 +16,7 @@ const AddressTokenTransfers = () => { ...@@ -16,6 +16,7 @@ const AddressTokenTransfers = () => {
queryName={ QueryKeys.addressTokenTransfers } queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) } queryIds={ castArray(router.query.id) }
baseAddress={ typeof hash === 'string' ? hash : undefined } baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
/> />
); );
}; };
......
...@@ -64,6 +64,7 @@ const AddressTxs = () => { ...@@ -64,6 +64,7 @@ const AddressTxs = () => {
query={ addressTxsQuery } query={ addressTxsQuery }
showSocketInfo={ false } showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined } currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement
/> />
</Element> </Element>
); );
......
...@@ -6,7 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -6,7 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
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';
...@@ -36,12 +36,14 @@ const AddressIntTxsTableItem = ({ ...@@ -36,12 +36,14 @@ const AddressIntTxsTableItem = ({
const isOut = Boolean(currentAddress && currentAddress === from.hash); const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash); const isIn = Boolean(currentAddress && currentAddress === to?.hash);
const timeAgo = useTimeAgoIncrement(timestamp, true);
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap"> <Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/> <AddressLink fontWeight="700" hash={ txnHash } type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text> { timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
......
import { Alert, AlertIcon, AlertTitle, chakra } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { IndexingStatus } from 'types/api/indexingStatus';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
const IndexingAlert = ({ className }: { className?: string }) => {
const fetch = useFetch();
const isMobile = useIsMobile();
const { data } = useQuery<unknown, unknown, IndexingStatus>(
[ QueryKeys.indexingStatus ],
async() => await fetch(`/node-api/index/indexing-status`),
);
const queryClient = useQueryClient();
const handleBlocksIndexStatus: SocketMessage.BlocksIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing_blocks = payload.finished;
newData.indexed_blocks_ratio = payload.ratio;
return newData;
});
}, [ queryClient ]);
const blockIndexingChannel = useSocketChannel({
topic: 'blocks:indexing',
isDisabled: !data || data.finished_indexing_blocks,
});
useSocketMessage({
channel: blockIndexingChannel,
event: 'block_index_status',
handler: handleBlocksIndexStatus,
});
const handleIntermalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished;
newData.indexed_inernal_transactions_ratio = payload.ratio;
return newData;
});
}, [ queryClient ]);
const internalTxsIndexingChannel = useSocketChannel({
topic: 'blocks:indexing_internal_transactions',
isDisabled: !data || data.finished_indexing,
});
useSocketMessage({
channel: internalTxsIndexingChannel,
event: 'internal_txs_index_status',
handler: handleIntermalTxsIndexStatus,
});
if (!data) {
return null;
}
let content;
if (data.finished_indexing_blocks === false) {
content = `${ data.indexed_blocks_ratio && `${ (Number(data.indexed_blocks_ratio) * 100).toFixed() }% Blocks Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.` ;
} else if (data.finished_indexing === false) {
content = `${ data.indexed_inernal_transactions_ratio &&
`${ (Number(data.indexed_inernal_transactions_ratio) * 100).toFixed() }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.`;
}
if (!content) {
return null;
}
return (
<Alert status="warning" variant="solid" py={ 3 } borderRadius="12px" mb={ 6 } className={ className }>
{ !isMobile && <AlertIcon/> }
<AlertTitle>
{ content }
</AlertTitle>
</Alert>
);
};
export default chakra(IndexingAlert);
...@@ -40,7 +40,7 @@ const AddressPageContent = () => { ...@@ -40,7 +40,7 @@ const AddressPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: <AddressInternalTxs/> }, { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> },
// temporary show this tab in all address // temporary show this tab in all address
// later api will return info about available tabs // later api will return info about available tabs
......
...@@ -26,7 +26,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; ...@@ -26,7 +26,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> }, { id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> }, { id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready // will be implemented later, api is not ready
// { id: 'state', title: 'State', component: <TxState/> }, // { id: 'state', title: 'State', component: <TxState/> },
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import Page from './Page';
const API_URL = '/node-api/index/indexing-status';
test('without indexing alert +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Page>Page Content</Page>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with indexing alert +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ finished_indexing_blocks: false, indexed_blocks_ratio: 0.1 }),
}));
const component = await mount(
<TestApp>
<Page>Page Content</Page>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import IndexingAlert from 'ui/home/IndexingAlert';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
isHomePage?: boolean; isHomePage?: boolean;
...@@ -13,8 +15,9 @@ const PageContent = ({ children, isHomePage }: Props) => { ...@@ -13,8 +15,9 @@ const PageContent = ({ children, isHomePage }: Props) => {
w="100%" w="100%"
paddingX={{ base: 4, lg: 12 }} paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 } paddingBottom={ 10 }
paddingTop={{ base: isHomePage ? '88px' : '138px', lg: isHomePage ? 9 : 0 }} paddingTop={{ base: isHomePage ? '88px' : '138px', lg: 0 }}
> >
<IndexingAlert display={{ base: 'block', lg: 'none' }}/>
{ children } { children }
</Box> </Box>
); );
......
...@@ -32,3 +32,25 @@ test('without tx info +@mobile', async({ mount, page }) => { ...@@ -32,3 +32,25 @@ test('without tx info +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenTransferMock.mixTokens),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
showTxInfo={ true }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
...@@ -43,9 +43,19 @@ interface Props { ...@@ -43,9 +43,19 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
txHash?: string; txHash?: string;
enableTimeIncrement?: boolean;
} }
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => { const TokenTransfer = ({
isLoading: isLoadingProp,
isDisabled,
queryName,
queryIds,
path,
baseAddress,
showTxInfo = true,
enableTimeIncrement,
}: Props) => {
const router = useRouter(); const router = useRouter();
const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>( const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>(
{ type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) }, { type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
...@@ -107,10 +117,10 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -107,10 +117,10 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return ( return (
<> <>
<Hide below="lg"> <Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/> <TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 } enableTimeIncrement={ enableTimeIncrement }/>
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/> <TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
</Show> </Show>
</> </>
); );
......
...@@ -9,12 +9,21 @@ interface Props { ...@@ -9,12 +9,21 @@ interface Props {
data: Array<TokenTransfer>; data: Array<TokenTransfer>;
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferList = ({ data, baseAddress, showTxInfo }: Props) => { const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item, index) => <TokenTransferListItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>) } { data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
baseAddress={ baseAddress }
showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box> </Box>
); );
}; };
......
import { Text, Flex, Tag, Icon } from '@chakra-ui/react'; import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
...@@ -18,9 +20,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; ...@@ -18,9 +20,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => { const TokenTransferListItem = ({
token,
total,
tx_hash: txHash,
from,
to,
baseAddress,
showTxInfo,
type,
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
return null; return null;
...@@ -29,6 +43,10 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -29,6 +43,10 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat(); return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})(); })();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`; const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return ( return (
<AccountListItemMobile rowGap={ 3 }> <AccountListItemMobile rowGap={ 3 }>
...@@ -40,12 +58,25 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -40,12 +58,25 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
</Flex> </Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
{ showTxInfo && ( { showTxInfo && (
<Flex columnGap={ 2 } w="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn hash</Text> <Flex>
<Address display="inline-flex" maxW="100%"> <Icon
<AddressLink type="transaction" hash={ txHash }/> as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address> </Address>
</Flex> </Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex>
) } ) }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth }>
......
...@@ -11,9 +11,10 @@ interface Props { ...@@ -11,9 +11,10 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
top: number; top: number;
enableTimeIncrement?: boolean;
} }
const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => { const TokenTransferTable = ({ data, baseAddress, showTxInfo, top, enableTimeIncrement }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
...@@ -31,7 +32,7 @@ const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => { ...@@ -31,7 +32,7 @@ const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item, index) => ( { data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/> <TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Flex } from '@chakra-ui/react'; import { Tr, Td, Tag, Flex, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
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';
...@@ -16,9 +17,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; ...@@ -16,9 +17,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => { const TokenTransferTableItem = ({
token,
total,
tx_hash: txHash,
from,
to,
baseAddress,
showTxInfo,
type,
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
return '-'; return '-';
...@@ -27,6 +40,8 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -27,6 +40,8 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat(); return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})(); })();
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
{ showTxInfo && ( { showTxInfo && (
...@@ -49,6 +64,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -49,6 +64,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> }
</Td> </Td>
) } ) }
<Td> <Td>
......
import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react'; import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import domToImage from 'dom-to-image';
import React, { useRef, useCallback, useState } from 'react';
import type { TimeChartItem } from './types'; import type { TimeChartItem } from './types';
import imageIcon from 'icons/image.svg';
import repeatArrow from 'icons/repeat_arrow.svg'; import repeatArrow from 'icons/repeat_arrow.svg';
import scopeIcon from 'icons/scope.svg';
import dotsIcon from 'icons/vertical_dots.svg'; import dotsIcon from 'icons/vertical_dots.svg';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
...@@ -17,7 +20,10 @@ type Props = { ...@@ -17,7 +20,10 @@ type Props = {
isLoading: boolean; isLoading: boolean;
} }
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading }: Props) => { const ChartWidget = ({ items, title, description, isLoading }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -47,6 +53,30 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -47,6 +53,30 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
} }
}, []); }, []);
const handleFileSaveClick = useCallback(() => {
if (ref.current) {
domToImage.toPng(ref.current,
{
quality: 100,
bgcolor: 'white',
width: ref.current.offsetWidth * DOWNLOAD_IMAGE_SCALE,
height: ref.current.offsetHeight * DOWNLOAD_IMAGE_SCALE,
filter: (node) => node.nodeName !== 'BUTTON',
style: {
transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`,
'transform-origin': 'top left',
},
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = `${ title } (Blockscout chart).png`;
link.href = dataUrl;
link.click();
link.remove();
});
}
}, [ title ]);
if (isLoading) { if (isLoading) {
return <ChartWidgetSkeleton/>; return <ChartWidgetSkeleton/>;
} }
...@@ -55,6 +85,7 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -55,6 +85,7 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
return ( return (
<> <>
<Box <Box
ref={ ref }
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
border="1px" border="1px"
...@@ -119,7 +150,22 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -119,7 +150,22 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
</VisuallyHidden> </VisuallyHidden>
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem onClick={ showChartFullscreen }>View fullscreen</MenuItem> <MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<Icon as={ scopeIcon } boxSize={ 4 } mr={ 3 }/>
View fullscreen
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleFileSaveClick }
>
<Icon as={ imageIcon } boxSize={ 4 } mr={ 3 }/>
Save as PNG
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</Grid> </Grid>
......
...@@ -2,6 +2,7 @@ import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import IndexingAlert from 'ui/home/IndexingAlert';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
...@@ -44,6 +45,12 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -44,6 +45,12 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
</Flex> </Flex>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> } { !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box> </Box>
<Box
paddingX={ 12 }
paddingTop={ 9 }
display={{ base: 'none', lg: 'block' }}
>
<IndexingAlert/>
{ !isHomePage && ( { !isHomePage && (
<HStack <HStack
as="header" as="header"
...@@ -51,9 +58,6 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -51,9 +58,6 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
gap={ 12 } gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px" paddingBottom="52px"
> >
<Box width="100%"> <Box width="100%">
...@@ -63,6 +67,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -63,6 +67,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
<ProfileMenuDesktop/> <ProfileMenuDesktop/>
</HStack> </HStack>
) } ) }
</Box>
</> </>
); );
}; };
......
...@@ -26,9 +26,10 @@ type Props = { ...@@ -26,9 +26,10 @@ type Props = {
showSocketInfo?: boolean; showSocketInfo?: boolean;
currentAddress?: string; currentAddress?: string;
filter?: React.ReactNode; filter?: React.ReactNode;
enableTimeIncrement?: boolean;
} }
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress }: Props) => { const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage; const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -62,7 +63,15 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -62,7 +63,15 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
{ ({ content }) => <Box>{ content }</Box> } { ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice> </TxsNewItemNotice>
) } ) }
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo } currentAddress={ currentAddress }/>) } { txs.map(tx => (
<TxsListItem
tx={ tx }
key={ tx.hash }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box> </Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
...@@ -74,6 +83,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -74,6 +83,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 } top={ isPaginatorHidden ? 0 : 80 }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/> />
</Hide> </Hide>
</> </>
......
...@@ -17,8 +17,8 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -17,8 +17,8 @@ import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
...@@ -33,12 +33,13 @@ type Props = { ...@@ -33,12 +33,13 @@ type Props = {
tx: Transaction; tx: Transaction;
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TAG_WIDTH = 48; const TAG_WIDTH = 48;
const ARROW_WIDTH = 24; const ARROW_WIDTH = 24;
const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
...@@ -48,6 +49,8 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -48,6 +49,8 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash); const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement);
return ( return (
<> <>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}> <Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
...@@ -58,7 +61,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -58,7 +61,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
</HStack> </HStack>
<AdditionalInfoButton onClick={ onOpen }/> <AdditionalInfoButton onClick={ onOpen }/>
</Flex> </Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }> <Flex justifyContent="space-between" lineHeight="24px" mt={ 3 } alignItems="center">
<Flex> <Flex>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
...@@ -75,7 +78,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -75,7 +78,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
/> />
</Address> </Address>
</Flex> </Flex>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(tx.timestamp).fromNow() }</Text> { tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex> </Flex>
{ tx.method && ( { tx.method && (
<Flex mt={ 3 }> <Flex mt={ 3 }>
......
...@@ -19,9 +19,10 @@ type Props = { ...@@ -19,9 +19,10 @@ type Props = {
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean; showSocketInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress, enableTimeIncrement }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }> <TheadSticky top={ top }>
...@@ -62,6 +63,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr ...@@ -62,6 +63,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -21,7 +21,7 @@ import React from 'react'; ...@@ -21,7 +21,7 @@ import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
...@@ -39,12 +39,15 @@ type Props = { ...@@ -39,12 +39,15 @@ type Props = {
tx: Transaction; tx: Transaction;
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash); const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement);
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
...@@ -93,7 +96,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -93,7 +96,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
fontWeight="700" fontWeight="700"
/> />
</Address> </Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> { tx.timestamp && <Text color="gray.500" fontWeight="400">{ timeAgo }</Text> }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
......
...@@ -3476,6 +3476,11 @@ ...@@ -3476,6 +3476,11 @@
"@types/d3-transition" "*" "@types/d3-transition" "*"
"@types/d3-zoom" "*" "@types/d3-zoom" "*"
"@types/dom-to-image@^2.6.4":
version "2.6.4"
resolved "https://registry.yarnpkg.com/@types/dom-to-image/-/dom-to-image-2.6.4.tgz#008411e23903cb0ee9e51a42ab8358c609541ee8"
integrity sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw==
"@types/estree@^1.0.0": "@types/estree@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
...@@ -5050,6 +5055,11 @@ dom-serializer@^1.0.1: ...@@ -5050,6 +5055,11 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0" domhandler "^4.2.0"
entities "^2.0.0" entities "^2.0.0"
dom-to-image@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
domelementtype@^2.0.1, domelementtype@^2.2.0: domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
......
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