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

Merge pull request #821 from blockscout/skeletons/blocks-and-txs

skeletons: blocks and txs
parents ee3337ae 10176478
...@@ -47,7 +47,7 @@ ...@@ -47,7 +47,7 @@
}, },
{ {
"type": "shell", "type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:goerliApiHost} yarn dev:goerli", "command": "NEXT_PUBLIC_API_HOST=${input:apiHost} yarn dev:goerli",
"problemMatcher": [], "problemMatcher": [],
"label": "dev server: goerli", "label": "dev server: goerli",
"detail": "start local dev server for Goerli network", "detail": "start local dev server for Goerli network",
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
}, },
{ {
"type": "shell", "type": "shell",
"command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:goerliApiHost} yarn dev:goerli:optimism", "command": "NEXT_PUBLIC_API_HOST=${input:L2ApiHost} NEXT_PUBLIC_L1_BASE_URL=https://${input:apiHost} yarn dev:goerli:optimism",
"problemMatcher": [], "problemMatcher": [],
"label": "dev server: goerli optimism", "label": "dev server: goerli optimism",
"detail": "start local dev server for Goerli Optimism network", "detail": "start local dev server for Goerli Optimism network",
...@@ -371,11 +371,12 @@ ...@@ -371,11 +371,12 @@
}, },
{ {
"type": "pickString", "type": "pickString",
"id": "goerliApiHost", "id": "apiHost",
"description": "Choose API host:", "description": "Choose API host:",
"options": [ "options": [
"blockscout-main.test.aws-k8s.blockscout.com", "blockscout-main.test.aws-k8s.blockscout.com",
"eth-goerli.blockscout.com", "eth-goerli.blockscout.com",
"eth.blockscout.com",
], ],
"default": "" "default": ""
}, },
......
...@@ -21,6 +21,7 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/ ...@@ -21,6 +21,7 @@ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_HAS_BEACON_CHAIN=false
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
...@@ -158,6 +158,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -158,6 +158,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
resetPage, resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false, hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
canGoBackwards: canGoBackwards.current, canGoBackwards: canGoBackwards.current,
isLoading: queryResult.isPlaceholderData && !hasPagination,
}; };
const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage); const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage);
......
...@@ -47,6 +47,7 @@ export const base: Block = { ...@@ -47,6 +47,7 @@ export const base: Block = {
tx_fees: '26853607500000000', tx_fees: '26853607500000000',
type: 'block', type: 'block',
uncles_hashes: [], uncles_hashes: [],
has_beacon_chain_withdrawals: false,
}; };
export const genesis: Block = { export const genesis: Block = {
...@@ -83,6 +84,7 @@ export const genesis: Block = { ...@@ -83,6 +84,7 @@ export const genesis: Block = {
tx_fees: '0', tx_fees: '0',
type: 'block', type: 'block',
uncles_hashes: [], uncles_hashes: [],
has_beacon_chain_withdrawals: false,
}; };
export const base2: Block = { export const base2: Block = {
......
...@@ -65,6 +65,7 @@ export const base: Transaction = { ...@@ -65,6 +65,7 @@ export const base: Transaction = {
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
actions: [], actions: [],
has_error_in_internal_txs: false,
}; };
export const withContractCreation: Transaction = { export const withContractCreation: Transaction = {
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes'; import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/block/getSeo'; import getSeo from 'lib/next/block/getSeo';
import Block from 'ui/pages/Block';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => { const BlockPage: NextPage<RoutedQuery<'/block/[height]'>> = ({ height }: RoutedQuery<'/block/[height]'>) => {
const { title, description } = getSeo({ height }); const { title, description } = getSeo({ height });
return ( return (
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/blocks/getSeo'; import getSeo from 'lib/next/blocks/getSeo';
import Blocks from 'ui/pages/Blocks'; import Page from 'ui/shared/Page/Page';
const Blocks = dynamic(() => import('ui/pages/Blocks'), { ssr: false });
const BlockPage: NextPage = () => { const BlockPage: NextPage = () => {
const { title } = getSeo(); const { title } = getSeo();
...@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const BlockPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Blocks/> <Page>
<Blocks/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import type { RoutedQuery } from 'nextjs-routes'; import type { RoutedQuery } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import getSeo from 'lib/next/tx/getSeo'; import getSeo from 'lib/next/tx/getSeo';
import Transaction from 'ui/pages/Transaction'; import Page from 'ui/shared/Page/Page';
const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false });
const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => { const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQuery<'/tx/[hash]'>) => {
const { title, description } = getSeo({ hash }); const { title, description } = getSeo({ hash });
...@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu ...@@ -15,7 +18,9 @@ const TransactionPage: NextPage<RoutedQuery<'/tx/[hash]'>> = ({ hash }: RoutedQu
<title>{ title }</title> <title>{ title }</title>
<meta name="description" content={ description }/> <meta name="description" content={ description }/>
</Head> </Head>
<Transaction/> <Page>
<Transaction/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions'; import Page from 'ui/shared/Page/Page';
const Transactions = dynamic(() => import('ui/pages/Transactions'), { ssr: false });
const TxsPage: NextPage = () => { const TxsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<Transactions/> <Page>
<Transactions/>
</Page>
</> </>
); );
}; };
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Withdrawals from 'ui/pages/Withdrawals'; import Page from 'ui/shared/Page/Page';
const Withdrawals = dynamic(() => import('ui/pages/Withdrawals'), { ssr: false });
const WithdrawalsPage: NextPage = () => { const WithdrawalsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
...@@ -12,7 +15,9 @@ const WithdrawalsPage: NextPage = () => { ...@@ -12,7 +15,9 @@ const WithdrawalsPage: NextPage = () => {
<Head> <Head>
<title>{ title }</title> <title>{ title }</title>
</Head> </Head>
<Withdrawals/> <Page>
<Withdrawals/>
</Page>
</> </>
); );
}; };
......
import type { Block } from 'types/api/block';
import { ADDRESS_PARAMS } from './addressParams';
export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70'; export const BLOCK_HASH = '0x8fa7b9e5e5e79deeb62d608db22ba9a5cb45388c7ebb9223ae77331c6080dc70';
export const BLOCK: Block = {
base_fee_per_gas: '14',
burnt_fees: '92834504000000000',
burnt_fees_percentage: 42.2,
difficulty: '340282366920938463463374607431768211451',
extra_data: 'TODO',
gas_limit: '30000000',
gas_target_percentage: 55.79,
gas_used: '6631036',
gas_used_percentage: 22.10,
has_beacon_chain_withdrawals: null,
hash: BLOCK_HASH,
height: 8988736,
miner: ADDRESS_PARAMS,
nonce: '0x0000000000000000',
parent_hash: BLOCK_HASH,
priority_fee: '19241635454943109',
rewards: [
{
reward: '19241635454943109',
type: 'Validator Reward',
},
],
size: 46406,
state_root: 'TODO',
timestamp: '2023-05-12T19:29:12.000000Z',
total_difficulty: '10837812015930321201107455268036056402048391639',
tx_count: 142,
tx_fees: '19241635547777613',
type: 'block',
uncles_hashes: [],
};
import type { InternalTransaction } from 'types/api/internalTransaction';
import { ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const INTERNAL_TX: InternalTransaction = {
block: 9006105,
created_contract: null,
error: null,
from: ADDRESS_PARAMS,
gas_limit: '754278',
index: 1,
success: true,
timestamp: '2023-05-15T20:14:00.000000Z',
to: ADDRESS_PARAMS,
transaction_hash: TX_HASH,
type: 'staticcall',
value: '22324344900000000',
};
import type { Log } from 'types/api/log';
import { ADDRESS_PARAMS } from './addressParams';
import { TX_HASH } from './tx';
export const LOG: Log = {
address: ADDRESS_PARAMS,
data: '0x000000000000000000000000000000000000000000000000000000d75e4be200',
decoded: {
method_call: 'CreditSpended(uint256 indexed _type, uint256 _quantity)',
method_id: '58cdf94a',
parameters: [
{
indexed: true,
name: '_type',
type: 'uint256',
value: 'placeholder',
},
{
indexed: false,
name: '_quantity',
type: 'uint256',
value: 'placeholder',
},
],
},
index: 42,
topics: [
'0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
'0x000000000000000000000000c52ea157a7fb3e25a069d47df0428ac70cd656b1',
'0x000000000000000000000000302fd86163cb9ad5533b3952dafa3b633a82bc51',
null,
],
tx_hash: TX_HASH,
};
import type { HomeStats } from 'types/api/stats';
export const HOMEPAGE_STATS: HomeStats = {
average_block_time: 14346,
coin_price: '1807.68',
gas_prices: {
average: 0.1,
fast: 0.11,
slow: 0.1,
},
gas_used_today: '0',
market_cap: '0',
network_utilization_percentage: 22.56,
static_gas_price: null,
total_addresses: '28634064',
total_blocks: '8940150',
total_gas_used: '0',
total_transactions: '193823272',
transactions_today: '0',
};
import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenInventoryResponse, TokenType } from 'types/api/token'; import type { TokenCounters, TokenHolder, TokenHolders, TokenInfo, TokenInstance, TokenType } from 'types/api/token';
import type { TokenTransfer, TokenTransferResponse } from 'types/api/tokenTransfer'; import type { TokenTransfer, TokenTransferPagination, TokenTransferResponse } from 'types/api/tokenTransfer';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { BLOCK_HASH } from './block'; import { BLOCK_HASH } from './block';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
import { generateListStub } from './utils';
export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = { export const TOKEN_INFO_ERC_20: TokenInfo<'ERC-20'> = {
address: ADDRESS_HASH, address: ADDRESS_HASH,
...@@ -73,14 +74,14 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = { ...@@ -73,14 +74,14 @@ export const TOKEN_TRANSFER_ERC_1155: TokenTransfer = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
}; };
export const getTokenTransfersStub = (type?: TokenType): TokenTransferResponse => { export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) { switch (type) {
case 'ERC-721': case 'ERC-721':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_721), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, pagination);
case 'ERC-1155': case 'ERC-1155':
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_1155), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, pagination);
default: default:
return { items: Array(50).fill(TOKEN_TRANSFER_ERC_20), next_page_params: null }; return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, pagination);
} }
}; };
...@@ -101,8 +102,3 @@ export const TOKEN_INSTANCE: TokenInstance = { ...@@ -101,8 +102,3 @@ export const TOKEN_INSTANCE: TokenInstance = {
token: TOKEN_INFO_ERC_1155, token: TOKEN_INFO_ERC_1155,
holder_address_hash: ADDRESS_HASH, holder_address_hash: ADDRESS_HASH,
}; };
export const TOKEN_INSTANCES: TokenInventoryResponse = {
items: Array(50).fill(TOKEN_INSTANCE),
next_page_params: null,
};
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Transaction } from 'types/api/transaction';
import { ADDRESS_PARAMS } from './addressParams';
export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967'; export const TX_HASH = '0x3ed9d81e7c1001bdda1caa1dc62c0acbbe3d2c671cdc20dc1e65efdaa4186967';
export const TX: Transaction = {
timestamp: '2022-11-11T11:11:11.000000Z',
fee: {
type: 'actual',
value: '2100000000000000',
},
gas_limit: '21000',
block: 9004925,
status: 'ok',
method: 'placeholder',
confirmations: 71,
type: 0,
exchange_rate: '1828.71',
to: ADDRESS_PARAMS,
tx_burnt_fee: null,
max_fee_per_gas: null,
result: 'success',
hash: '0x2b824349b320cfa72f292ab26bf525adb00083ba9fa097141896c3c8c74567cc',
gas_price: '100000000000',
priority_fee: null,
base_fee_per_gas: '24',
from: ADDRESS_PARAMS,
token_transfers: null,
tx_types: [
'coin_transfer',
],
gas_used: '21000',
created_contract: null,
position: 0,
nonce: 295929,
has_error_in_internal_txs: false,
actions: [],
decoded_input: null,
token_transfers_overflow: false,
raw_input: '0x',
value: '42000420000000000000',
max_priority_fee_per_gas: null,
revert_reason: null,
confirmation_duration: [
0,
14545,
],
tx_tag: null,
};
export const TX_RAW_TRACE: RawTracesResponse = [];
import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges';
import { ADDRESS_PARAMS } from './addressParams';
import { TOKEN_INFO_ERC_721 } from './token';
export const STATE_CHANGE_MINER: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '124280364215547113',
balance_before: '123405277440098758',
change: '875086775448355',
is_miner: true,
token: null,
type: 'coin',
};
export const STATE_CHANGE_COIN: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '61659392141463351540',
balance_before: '61660292436225994690',
change: '-900294762600000',
is_miner: false,
token: null,
type: 'coin',
};
export const STATE_CHANGE_TOKEN: TxStateChange = {
address: ADDRESS_PARAMS,
balance_after: '43',
balance_before: '42',
change: [
{
direction: 'to',
total: {
token_id: '1621395',
},
},
],
is_miner: false,
token: TOKEN_INFO_ERC_721,
type: 'token',
};
export const TX_STATE_CHANGES: TxStateChanges = [
STATE_CHANGE_MINER,
STATE_CHANGE_COIN,
STATE_CHANGE_TOKEN,
];
import type { ArrayElement } from 'types/utils';
import type { PaginatedResources, PaginatedResponse } from 'lib/api/resources';
export function generateListStub<Resource extends PaginatedResources>(
stub: ArrayElement<PaginatedResponse<Resource>['items']>,
num = 50,
pagination: PaginatedResponse<Resource>['next_page_params'] = null,
) {
return {
items: Array(num).fill(stub),
next_page_params: pagination,
};
}
import type { WithdrawalsItem } from 'types/api/withdrawals';
import { ADDRESS_PARAMS } from './addressParams';
export const WITHDRAWAL: WithdrawalsItem = {
amount: '12565723',
index: 3810697,
receiver: ADDRESS_PARAMS,
validator_index: 25987,
block_number: 9005713,
timestamp: '2023-05-12T19:29:12.000000Z',
};
...@@ -10,7 +10,7 @@ export interface Block { ...@@ -10,7 +10,7 @@ export interface Block {
tx_count: number; tx_count: number;
miner: AddressParam; miner: AddressParam;
size: number; size: number;
has_beacon_chain_withdrawals?: boolean; has_beacon_chain_withdrawals: boolean | null;
hash: string; hash: string;
parent_hash: string; parent_hash: string;
difficulty: string; difficulty: string;
......
...@@ -16,7 +16,7 @@ export interface LogsResponseTx { ...@@ -16,7 +16,7 @@ export interface LogsResponseTx {
index: number; index: number;
items_count: number; items_count: number;
transaction_hash: string; transaction_hash: string;
}; } | null;
} }
export interface LogsResponseAddress { export interface LogsResponseAddress {
...@@ -26,5 +26,5 @@ export interface LogsResponseAddress { ...@@ -26,5 +26,5 @@ export interface LogsResponseAddress {
items_count: number; items_count: number;
transaction_index: number; transaction_index: number;
block_number: number; block_number: number;
}; } | null;
} }
...@@ -8,7 +8,7 @@ export type HomeStats = { ...@@ -8,7 +8,7 @@ export type HomeStats = {
transactions_today: string; transactions_today: string;
gas_used_today: string; gas_used_today: string;
gas_prices: GasPrices | null; gas_prices: GasPrices | null;
static_gas_price: string; static_gas_price: string | null;
market_cap: string; market_cap: string;
network_utilization_percentage: number; network_utilization_percentage: number;
} }
......
...@@ -47,6 +47,7 @@ export type Transaction = { ...@@ -47,6 +47,7 @@ export type Transaction = {
l1_fee_scalar?: string; l1_fee_scalar?: string;
l1_gas_price?: string; l1_gas_price?: string;
l1_gas_used?: string; l1_gas_used?: string;
has_error_in_internal_txs: boolean | null;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
import { chakra, Icon, Tooltip, Hide } from '@chakra-ui/react'; import { chakra, Icon, Tooltip, Hide, Skeleton, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -13,15 +13,27 @@ interface Props { ...@@ -13,15 +13,27 @@ interface Props {
address: string; address: string;
type: CsvExportType; type: CsvExportType;
className?: string; className?: string;
isLoading?: boolean;
} }
const AddressCsvExportLink = ({ className, address, type }: Props) => { const AddressCsvExportLink = ({ className, address, type, isLoading }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
if (!appConfig.reCaptcha.siteKey) { if (!appConfig.reCaptcha.siteKey) {
return null; return null;
} }
if (isLoading) {
return (
<Flex className={ className } flexShrink={ 0 } alignItems="center">
<Skeleton boxSize={{ base: '32px', lg: 6 }} borderRadius="base"/>
<Hide ssr={ false } below="lg">
<Skeleton w="112px" h={ 6 } ml={ 1 }/>
</Hide>
</Flex>
);
}
return ( return (
<Tooltip isDisabled={ !isMobile } label="Download CSV"> <Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal <LinkInternal
......
...@@ -3,20 +3,29 @@ import React from 'react'; ...@@ -3,20 +3,29 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import LogItem from 'ui/shared/logs/LogItem'; 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';
const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter(); const router = useRouter();
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_logs', resourceName: 'address_logs',
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_logs'>(LOG, 3, {
block_number: 9005750,
index: 42,
items_count: 50,
transaction_index: 23,
}),
},
}); });
const actionBar = isPaginationVisible ? ( const actionBar = isPaginationVisible ? (
...@@ -25,19 +34,17 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement> ...@@ -25,19 +34,17 @@ const AddressLogs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>
</ActionBar> </ActionBar>
) : null; ) : null;
const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address"/>) : null; const content = data?.items ? data.items.map((item, index) => <LogItem key={ index } { ...item } type="address" isLoading={ isPlaceholderData }/>) : null;
const skeleton = <><LogSkeleton/><LogSkeleton/></>;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
emptyText="There are no logs for this address." emptyText="There are no logs for this address."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
); );
}; };
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155A } from 'mocks/tokens/tokenTransfer'; import { erc1155A } from 'mocks/tokens/tokenTransfer';
...@@ -17,7 +17,7 @@ const hooksConfig = { ...@@ -17,7 +17,7 @@ const hooksConfig = {
}, },
}; };
test('with token filter and pagination +@mobile', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
...@@ -34,7 +34,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => { ...@@ -34,7 +34,7 @@ test('with token filter and pagination +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with token filter and no pagination +@mobile', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ erc1155A ] }),
...@@ -50,3 +50,41 @@ test('with token filter and no pagination +@mobile', async({ mount, page }) => { ...@@ -50,3 +50,41 @@ test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155A ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
...@@ -20,11 +20,12 @@ import getQueryParamString from 'lib/router/getQueryParamString'; ...@@ -20,11 +20,12 @@ import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
...@@ -81,11 +82,18 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -81,11 +82,18 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
}, },
); );
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isPlaceholderData, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers', resourceName: 'address_token_transfers',
pathParams: { hash: currentAddress }, pathParams: { hash: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters, filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef, scrollRef,
options: {
placeholderData: getTokenTransfersStub(undefined, {
block_number: 7793535,
index: 46,
items_count: 50,
}),
},
}); });
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => { const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
...@@ -172,16 +180,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -172,16 +180,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
showSocketInfo={ pagination.page === 1 && !tokenFilter } showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
isLoading={ isPlaceholderData }
/> />
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ pagination.page === 1 && !tokenFilter && ( { pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ newItemsCount } num={ newItemsCount }
alert={ socketAlert } alert={ socketAlert }
type="token_transfer" type="token_transfer"
borderBottomRadius={ 0 } isLoading={ isPlaceholderData }
/> />
) } ) }
<TokenTransferList <TokenTransferList
...@@ -189,6 +198,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -189,6 +198,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
enableTimeIncrement enableTimeIncrement
isLoading={ isPlaceholderData }
/> />
</Show> </Show>
</> </>
...@@ -227,7 +237,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -227,7 +237,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
<> <>
{ isMobile && tokenFilterComponent } { isMobile && tokenFilterComponent }
{ !isActionBarHidden && ( { !isActionBarHidden && (
<ActionBar mt={ -6 } showShadow={ isLoading }> <ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent } { !isMobile && tokenFilterComponent }
{ !tokenFilter && ( { !tokenFilter && (
<TokenTransferFilter <TokenTransferFilter
...@@ -237,9 +247,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -237,9 +247,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
withAddressFilter withAddressFilter
onAddressFilterChange={ handleAddressFilterChange } onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter } defaultAddressFilter={ filters.filter }
isLoading={ isPlaceholderData }
/>
) }
{ currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
type="token-transfers"
ml={{ base: 2, lg: 'auto' }}
isLoading={ isPlaceholderData }
/> />
) } ) }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="token-transfers" ml={{ base: 2, lg: 'auto' }}/> }
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> } { isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar> </ActionBar>
) } ) }
...@@ -249,7 +267,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -249,7 +267,7 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
...@@ -13,6 +13,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages'; ...@@ -13,6 +13,8 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
...@@ -41,6 +43,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -41,6 +43,13 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
pathParams: { hash: currentAddress }, pathParams: { hash: currentAddress },
filters: { filter: filterValue }, filters: { filter: filterValue },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_txs'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
}),
},
}); });
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
...@@ -112,7 +121,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -112,7 +121,7 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
topic: `addresses:${ currentAddress?.toLowerCase() }`, topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1, isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData,
}); });
useSocketMessage({ useSocketMessage({
...@@ -132,15 +141,23 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -132,15 +141,23 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
defaultFilter={ filterValue } defaultFilter={ filterValue }
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
isLoading={ addressTxsQuery.pagination.isLoading }
/> />
); );
return ( return (
<> <>
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 } showShadow={ addressTxsQuery.isLoading }> <ActionBar mt={ -6 }>
{ filter } { filter }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> } { currentAddress && (
<AddressCsvExportLink
address={ currentAddress }
type="transactions"
ml="auto"
isLoading={ addressTxsQuery.pagination.isLoading }
/>
) }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> } { addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
</ActionBar> </ActionBar>
) } ) }
......
...@@ -16,9 +16,10 @@ interface Props { ...@@ -16,9 +16,10 @@ interface Props {
isActive: boolean; isActive: boolean;
defaultFilter: AddressFromToFilter; defaultFilter: AddressFromToFilter;
onFilterChange: (nextValue: string | Array<string>) => void; onFilterChange: (nextValue: string | Array<string>) => void;
isLoading?: boolean;
} }
const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => { const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive, isLoading }: Props) => {
const { isOpen, onToggle } = useDisclosure(); const { isOpen, onToggle } = useDisclosure();
return ( return (
...@@ -26,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => ...@@ -26,6 +27,7 @@ const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) =>
<MenuButton> <MenuButton>
<FilterButton <FilterButton
isActive={ isOpen || isActive } isActive={ isOpen || isActive }
isLoading={ isLoading }
onClick={ onToggle } onClick={ onToggle }
as="div" as="div"
/> />
......
...@@ -4,6 +4,8 @@ import React from 'react'; ...@@ -4,6 +4,8 @@ import React from 'react';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -15,24 +17,37 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -15,24 +17,37 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_withdrawals', resourceName: 'address_withdrawals',
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: {
placeholderData: generateListStub<'address_withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
}); });
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="address"/>) } { data.items.map((item, index) => (
<WithdrawalsListItem
key={ item.index + Number(isPlaceholderData ? index : '') }
item={ item }
view="address"
isLoading={ isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 }/> <WithdrawalsTable items={ data.items } view="address" top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
const actionBar = isPaginationVisible ? ( const actionBar = isPaginationVisible ? (
<ActionBar mt={ -6 } showShadow={ isLoading }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) : null; ) : null;
...@@ -40,7 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE ...@@ -40,7 +55,7 @@ const AddressWithdrawals = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(5).fill(`${ 100 / 5 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this address." emptyText="There are no withdrawals for this address."
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, useColorModeValue } from '@chakra-ui/react'; import { Grid, GridItem, Text, Link, Box, Tooltip, useColorModeValue, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
...@@ -19,8 +19,8 @@ import dayjs from 'lib/date/dayjs'; ...@@ -19,8 +19,8 @@ import dayjs from 'lib/date/dayjs';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import BlockDetailsSkeleton from 'ui/block/details/BlockDetailsSkeleton';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
...@@ -42,7 +42,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -42,7 +42,7 @@ const BlockDetails = ({ query }: Props) => {
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const { data, isLoading, isError, error } = query; const { data, isPlaceholderData, isError, error } = query;
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
...@@ -63,10 +63,6 @@ const BlockDetails = ({ query }: Props) => { ...@@ -63,10 +63,6 @@ const BlockDetails = ({ query }: Props) => {
router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined); router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined);
}, [ data, router ]); }, [ data, router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
}
if (isError) { if (isError) {
if (error?.status === 404) { if (error?.status === 404) {
throw Error('Block not found', { cause: error as unknown as Error }); throw Error('Block not found', { cause: error as unknown as Error });
...@@ -79,6 +75,10 @@ const BlockDetails = ({ query }: Props) => { ...@@ -79,6 +75,10 @@ const BlockDetails = ({ query }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (!data) {
return null;
}
const sectionGap = ( const sectionGap = (
<GridItem <GridItem
colSpan={{ base: undefined, lg: 2 }} colSpan={{ base: undefined, lg: 2 }}
...@@ -92,13 +92,50 @@ const BlockDetails = ({ query }: Props) => { ...@@ -92,13 +92,50 @@ const BlockDetails = ({ query }: Props) => {
const validatorTitle = getNetworkValidatorTitle(); const validatorTitle = getNetworkValidatorTitle();
const rewardBreakDown = (() => {
if (appConfig.L2.isL2Network || totalReward.isEqualTo(ZERO) || txFees.isEqualTo(ZERO) || burntFees.isEqualTo(ZERO)) {
return null;
}
if (isPlaceholderData) {
return <Skeleton w="525px" h="20px"/>;
}
return (
<Text variant="secondary" whiteSpace="break-spaces">
<Tooltip label="Static block reward">
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
</Tooltip>
{ !txFees.isEqualTo(ZERO) && (
<>
{ space }+{ space }
<Tooltip label="Txn fees">
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
{ !burntFees.isEqualTo(ZERO) && (
<>
{ space }-{ space }
<Tooltip label="Burnt fees">
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
</Text>
);
})();
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem <DetailsInfoItem
title={ `${ data.type === 'reorg' ? 'Reorg' : 'Block' } height` } title={ `${ data.type === 'reorg' ? 'Reorg' : 'Block' } height` }
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain" hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
isLoading={ isPlaceholderData }
> >
{ data.height } <Skeleton isLoaded={ !isPlaceholderData }>
{ data.height }
</Skeleton>
{ data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> } { data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
<PrevNext <PrevNext
ml={ 6 } ml={ 6 }
...@@ -106,37 +143,50 @@ const BlockDetails = ({ query }: Props) => { ...@@ -106,37 +143,50 @@ const BlockDetails = ({ query }: Props) => {
prevLabel="View previous block" prevLabel="View previous block"
nextLabel="View next block" nextLabel="View next block"
isPrevDisabled={ data.height === 0 } isPrevDisabled={ data.height === 0 }
isLoading={ isPlaceholderData }
/> />
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Size" title="Size"
hint="Size of the block in bytes" hint="Size of the block in bytes"
isLoading={ isPlaceholderData }
> >
{ data.size.toLocaleString() } <Skeleton isLoaded={ !isPlaceholderData }>
{ data.size.toLocaleString() }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
hint="Date & time at which block was produced." hint="Date & time at which block was produced."
isLoading={ isPlaceholderData }
> >
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ clockIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text> <Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
{ dayjs(data.timestamp).fromNow() }
</Skeleton>
<TextSeparator/> <TextSeparator/>
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Text> <Skeleton isLoaded={ !isPlaceholderData } whiteSpace="normal">
{ dayjs(data.timestamp).format('LLLL') }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="The number of transactions in the block" hint="The number of transactions in the block"
isLoading={ isPlaceholderData }
> >
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'txs' } }) }> <Skeleton isLoaded={ !isPlaceholderData }>
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' } <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'txs' } }) }>
</LinkInternal> { data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' } title={ appConfig.network.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
hint="A block producer who successfully included the block onto the blockchain" hint="A block producer who successfully included the block onto the blockchain"
columnGap={ 1 } columnGap={ 1 }
isLoading={ isPlaceholderData }
> >
<AddressLink type="address" hash={ data.miner.hash }/> <AddressLink type="address" hash={ data.miner.hash } isLoading={ isPlaceholderData }/>
{ data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> } { data.miner.name && <Text>{ `(${ capitalize(validatorTitle) }: ${ data.miner.name })` }</Text> }
{ /* api doesn't return the block processing time yet */ } { /* api doesn't return the block processing time yet */ }
{ /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ } { /* <Text>{ dayjs.duration(block.minedIn, 'second').humanize(true) }</Text> */ }
...@@ -149,31 +199,12 @@ const BlockDetails = ({ query }: Props) => { ...@@ -149,31 +199,12 @@ const BlockDetails = ({ query }: Props) => {
on top of the fees paid for all transactions in the block` on top of the fees paid for all transactions in the block`
} }
columnGap={ 1 } columnGap={ 1 }
isLoading={ isPlaceholderData }
> >
<Text>{ totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !isPlaceholderData }>
{ (!txFees.isEqualTo(ZERO) || !burntFees.isEqualTo(ZERO)) && ( { totalReward.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
<Text variant="secondary" whiteSpace="break-spaces">( </Skeleton>
<Tooltip label="Static block reward"> { rewardBreakDown }
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
</Tooltip>
{ !txFees.isEqualTo(ZERO) && (
<>
{ space }+{ space }
<Tooltip label="Txn fees">
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
{ !burntFees.isEqualTo(ZERO) && (
<>
{ space }-{ space }
<Tooltip label="Burnt fees">
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
) }
)</Text>
) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.rewards { data.rewards
...@@ -195,35 +226,49 @@ const BlockDetails = ({ query }: Props) => { ...@@ -195,35 +226,49 @@ const BlockDetails = ({ query }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
hint="The total gas amount used in the block and its percentage of gas filled in the block" hint="The total gas amount used in the block and its percentage of gas filled in the block"
isLoading={ isPlaceholderData }
> >
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text> <Skeleton isLoaded={ !isPlaceholderData }>
{ BigNumber(data.gas_used || 0).toFormat() }
</Skeleton>
<Utilization <Utilization
ml={ 4 } ml={ 4 }
colorScheme="gray" colorScheme="gray"
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
isLoading={ isPlaceholderData }
/> />
{ data.gas_target_percentage && ( { data.gas_target_percentage && (
<> <>
<TextSeparator color={ separatorColor } mx={ 1 }/> <TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/> <GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isPlaceholderData }/>
</> </>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas limit" title="Gas limit"
hint="Total gas limit provided by all transactions in the block" hint="Total gas limit provided by all transactions in the block"
isLoading={ isPlaceholderData }
> >
<Text>{ BigNumber(data.gas_limit).toFormat() }</Text> <Skeleton isLoaded={ !isPlaceholderData }>
{ BigNumber(data.gas_limit).toFormat() }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
{ data.base_fee_per_gas && ( { data.base_fee_per_gas && (
<DetailsInfoItem <DetailsInfoItem
title="Base fee per gas" title="Base fee per gas"
hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion" hint="Minimum fee required per unit of gas. Fee adjusts based on network congestion"
isLoading={ isPlaceholderData }
> >
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text> { isPlaceholderData ? (
<Text variant="secondary" whiteSpace="pre"> <Skeleton isLoaded={ !isPlaceholderData } h="20px" maxW="380px" w="100%"/>
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei) ) : (
</Text> <>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } </Text>
<Text variant="secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)
</Text>
</>
) }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
...@@ -233,15 +278,19 @@ const BlockDetails = ({ query }: Props) => { ...@@ -233,15 +278,19 @@ const BlockDetails = ({ query }: Props) => {
Equals Block Base Fee per Gas * Gas Used` Equals Block Base Fee per Gas * Gas Used`
} }
isLoading={ isPlaceholderData }
> >
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Text ml={ 1 }>{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>
{ burntFees.dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</Skeleton>
{ !txFees.isEqualTo(ZERO) && ( { !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label="Burnt fees / Txn fees * 100%">
<Box> <Box>
<Utilization <Utilization
ml={ 4 } ml={ 4 }
value={ burntFees.dividedBy(txFees).toNumber() } value={ burntFees.dividedBy(txFees).toNumber() }
isLoading={ isPlaceholderData }
/> />
</Box> </Box>
</Tooltip> </Tooltip>
...@@ -251,8 +300,11 @@ const BlockDetails = ({ query }: Props) => { ...@@ -251,8 +300,11 @@ const BlockDetails = ({ query }: Props) => {
<DetailsInfoItem <DetailsInfoItem
title="Priority fee / Tip" title="Priority fee / Tip"
hint="User-defined tips sent to validator for transaction priority/inclusion" hint="User-defined tips sent to validator for transaction priority/inclusion"
isLoading={ isPlaceholderData }
> >
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol } <Skeleton isLoaded={ !isPlaceholderData }>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ /* api doesn't support extra data yet */ } { /* api doesn't support extra data yet */ }
...@@ -267,21 +319,21 @@ const BlockDetails = ({ query }: Props) => { ...@@ -267,21 +319,21 @@ const BlockDetails = ({ query }: Props) => {
{ /* CUT */ } { /* CUT */ }
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="BlockDetails__cutLink"> <Element name="BlockDetails__cutLink">
<Link <Skeleton isLoaded={ !isPlaceholderData } mt={ 6 } display="inline-block">
mt={ 6 } <Link
display="inline-block" fontSize="sm"
fontSize="sm" textDecorationLine="underline"
textDecorationLine="underline" textDecorationStyle="dashed"
textDecorationStyle="dashed" onClick={ handleCutClick }
onClick={ handleCutClick } >
> { isExpanded ? 'Hide details' : 'View details' }
{ isExpanded ? 'Hide details' : 'View details' } </Link>
</Link> </Skeleton>
</Element> </Element>
</GridItem> </GridItem>
{ /* ADDITIONAL INFO */ } { /* ADDITIONAL INFO */ }
{ isExpanded && ( { isExpanded && !isPlaceholderData && (
<> <>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/> <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
......
...@@ -22,10 +22,22 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -22,10 +22,22 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
const content = blockWithdrawalsQuery.data?.items ? ( const content = blockWithdrawalsQuery.data?.items ? (
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
{ blockWithdrawalsQuery.data.items.map((item) => <WithdrawalsListItem item={ item } key={ item.index } view="block"/>) } { blockWithdrawalsQuery.data.items.map((item, index) => (
<WithdrawalsListItem
key={ item.index + (blockWithdrawalsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
view="block"
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
/>
)) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ blockWithdrawalsQuery.data.items } view="block" top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }/> <WithdrawalsTable
items={ blockWithdrawalsQuery.data.items }
isLoading={ blockWithdrawalsQuery.isPlaceholderData }
top={ blockWithdrawalsQuery.isPaginationVisible ? 80 : 0 }
view="block"
/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
...@@ -33,7 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => { ...@@ -33,7 +45,7 @@ const BlockWithdrawals = ({ blockWithdrawalsQuery }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ blockWithdrawalsQuery.isError } isError={ blockWithdrawalsQuery.isError }
isLoading={ blockWithdrawalsQuery.isLoading } isLoading={ false }
items={ blockWithdrawalsQuery.data?.items } items={ blockWithdrawalsQuery.data?.items }
skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ isLongSkeleton: true, skeletonDesktopColumns: Array(4).fill(`${ 100 / 4 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals for this block." emptyText="There are no withdrawals for this block."
......
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const BlockDetailsSkeleton = () => {
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="65%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow/>
<DetailsSkeletonRow/>
{ sectionGap }
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default BlockDetailsSkeleton;
import { Text } from '@chakra-ui/react'; import { Skeleton } from '@chakra-ui/react';
import type { TypographyProps } from '@chakra-ui/react'; import type { TypographyProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -7,13 +7,18 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props { interface Props {
ts: string; ts: string;
isEnabled?: boolean; isEnabled?: boolean;
isLoading?: boolean;
fontSize?: TypographyProps['fontSize']; fontSize?: TypographyProps['fontSize'];
} }
const BlockTimestamp = ({ ts, isEnabled, fontSize }: Props) => { const BlockTimestamp = ({ ts, isEnabled, isLoading, fontSize }: Props) => {
const timeAgo = useTimeAgoIncrement(ts, isEnabled); const timeAgo = useTimeAgoIncrement(ts, isEnabled);
return <Text variant="secondary" fontWeight={ 400 } fontSize={ fontSize }>{ timeAgo }</Text>; return (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight={ 400 } fontSize={ fontSize } display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
);
}; };
export default React.memo(BlockTimestamp); export default React.memo(BlockTimestamp);
import { Show, Hide, Alert } from '@chakra-ui/react'; import { Alert, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -66,7 +66,7 @@ const BlocksContent = ({ type, query }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: query.isLoading || query.isError || query.pagination.page !== 1, isDisabled: query.isPlaceholderData || query.isError || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -77,12 +77,12 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -77,12 +77,12 @@ const BlocksContent = ({ type, query }: Props) => {
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile" ssr={ false }> <Box display={{ base: 'block', lg: 'none' }}>
<BlocksList data={ query.data.items }/> <BlocksList data={ query.data.items } isLoading={ query.isPlaceholderData } page={ query.pagination.page }/>
</Show> </Box>
<Hide below="lg" key="content-desktop" ssr={ false }> <Box display={{ base: 'none', lg: 'block' }}>
<BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page }/> <BlocksTable data={ query.data.items } top={ query.isPaginationVisible ? 80 : 0 } page={ query.pagination.page } isLoading={ query.isPlaceholderData }/>
</Hide> </Box>
</> </>
) : null; ) : null;
...@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -95,7 +95,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ query.isError } isError={ query.isError }
isLoading={ query.isLoading } isLoading={ false }
items={ query.data?.items } items={ query.data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }} skeletonProps={{ skeletonDesktopColumns: [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }}
emptyText="There are no blocks." emptyText="There are no blocks."
......
...@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem'; ...@@ -8,14 +8,22 @@ import BlocksListItem from 'ui/blocks/BlocksListItem';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
isLoading: boolean;
page: number;
} }
const BlocksList = ({ data }: Props) => { const BlocksList = ({ data, isLoading, page }: Props) => {
return ( return (
<Box> <Box>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ } { data.map((item, index) => (
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) } <BlocksListItem
key={ item.height + (isLoading ? String(index) : '') }
data={ item }
isLoading={ isLoading }
enableTimeIncrement={ page === 1 && !isLoading }
/>
)) }
</AnimatePresence> </AnimatePresence>
</Box> </Box>
); );
......
import { Flex, Spinner, Text, Box, Icon, useColorModeValue } from '@chakra-ui/react'; import { Flex, Skeleton, Text, Box, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -13,6 +13,7 @@ import { WEI } from 'lib/consts'; ...@@ -13,6 +13,7 @@ import { WEI } from 'lib/consts';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -21,11 +22,11 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -21,11 +22,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isLoading?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { const BlocksListItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
...@@ -36,30 +37,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -36,30 +37,35 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated> <ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> } <Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal <LinkInternal
fontWeight={ 600 } fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) } href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
> >
{ data.height } { data.height }
</LinkInternal> </LinkInternal>
</Skeleton>
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
<Text variant="secondary">{ data.size.toLocaleString() } bytes</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ data.size.toLocaleString() } bytes</span>
</Skeleton>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text> <Text fontWeight={ 500 }>{ capitalize(getNetworkValidatorTitle()) }</Text>
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant"/> <AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" isLoading={ isLoading }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Txn</Text> <Text fontWeight={ 500 }>Txn</Text>
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }> <Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.tx_count } <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
</LinkInternal> { data.tx_count }
</LinkInternal>
</Skeleton>
) : ) :
<Text variant="secondary">{ data.tx_count }</Text> <Text variant="secondary">{ data.tx_count }</Text>
} }
...@@ -67,12 +73,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -67,12 +73,14 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Box> <Box>
<Text fontWeight={ 500 }>Gas used</Text> <Text fontWeight={ 500 }>Gas used</Text>
<Flex mt={ 2 }> <Flex mt={ 2 }>
<Text variant="secondary" mr={ 4 }>{ BigNumber(data.gas_used || 0).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" mr={ 4 }>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() }/> <span>{ BigNumber(data.gas_used || 0).toFormat() }</span>
</Skeleton>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).div(BigNumber(data.gas_limit)).toNumber() } isLoading={ isLoading }/>
{ data.gas_target_percentage && ( { data.gas_target_percentage && (
<> <>
<TextSeparator color={ separatorColor } mx={ 1 }/> <TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/> <GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</> </>
) } ) }
</Flex> </Flex>
...@@ -80,7 +88,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -80,7 +88,9 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward.toFixed() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ totalReward.toFixed() }</span>
</Skeleton>
</Flex> </Flex>
) } ) }
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
...@@ -88,10 +98,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -88,10 +98,12 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
<Flex columnGap={ 4 } mt={ 2 }> <Flex columnGap={ 4 } mt={ 2 }>
<Flex> <Flex>
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500" isLoading={ isLoading }/>
<Text variant="secondary" ml={ 1 }>{ burntFees.div(WEI).toFixed() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary" ml={ 1 }>
<span>{ burntFees.div(WEI).toFixed() }</span>
</Skeleton>
</Flex> </Flex>
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/> <Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Flex> </Flex>
</Box> </Box>
) } ) }
......
...@@ -2,8 +2,8 @@ import { Flex, Box, Text, Skeleton } from '@chakra-ui/react'; ...@@ -2,8 +2,8 @@ import { Flex, Box, Text, Skeleton } 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 useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import { HOMEPAGE_STATS } from 'stubs/stats';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -13,24 +13,22 @@ interface Props { ...@@ -13,24 +13,22 @@ interface Props {
} }
const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => { const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile(); const statsQuery = useApiQuery('homepage_stats', {
const statsQuery = useApiQuery('homepage_stats'); queryOptions: {
placeholderData: HOMEPAGE_STATS,
if (isMobile) { },
return null; });
}
return ( return (
<Flex alignItems="center" columnGap={ 8 }> <Flex alignItems="center" columnGap={ 8 } display={{ base: 'none', lg: 'flex' }}>
{ statsQuery.isLoading && <Skeleton w="175px" h="24px"/> }
{ statsQuery.data?.network_utilization_percentage !== undefined && ( { statsQuery.data?.network_utilization_percentage !== undefined && (
<Box> <Box>
<Text as="span" fontSize="sm"> <Text as="span" fontSize="sm">
Network utilization (last 50 blocks):{ nbsp } Network utilization (last 50 blocks):{ nbsp }
</Text> </Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 600 }> <Skeleton display="inline-block" fontSize="sm" color="blue.400" fontWeight={ 600 } isLoaded={ !statsQuery.isPlaceholderData }>
{ statsQuery.data.network_utilization_percentage.toFixed(2) }% <span>{ statsQuery.data.network_utilization_percentage.toFixed(2) }%</span>
</Text> </Skeleton>
</Box> </Box>
) } ) }
{ isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> } { isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> }
......
...@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky'; ...@@ -12,11 +12,12 @@ import { default as Thead } from 'ui/shared/TheadSticky';
interface Props { interface Props {
data: Array<Block>; data: Array<Block>;
isLoading?: boolean;
top: number; top: number;
page: number; page: number;
} }
const BlocksTable = ({ data, top, page }: Props) => { const BlocksTable = ({ data, isLoading, top, page }: Props) => {
return ( return (
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }>
...@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => { ...@@ -33,7 +34,14 @@ const BlocksTable = ({ data, top, page }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement={ page === 1 }/>) } { data.map((item, index) => (
<BlocksTableItem
key={ item.height + (isLoading ? `${ index }_${ page }` : '') }
data={ item }
enableTimeIncrement={ page === 1 && !isLoading }
isLoading={ isLoading }
/>
)) }
</AnimatePresence> </AnimatePresence>
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Flex, Box, Tooltip, Skeleton, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -12,6 +12,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward'; ...@@ -12,6 +12,7 @@ import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
...@@ -19,17 +20,18 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -19,17 +20,18 @@ import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isLoading?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
} }
const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { const BlocksTableItem = ({ data, isLoading, enableTimeIncrement }: Props) => {
const totalReward = getBlockTotalReward(data); const totalReward = getBlockTotalReward(data);
const burntFees = BigNumber(data.burnt_fees || 0); const burntFees = BigNumber(data.burnt_fees || 0);
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
const separatorColor = useColorModeValue('gray.200', 'gray.700'); const separatorColor = useColorModeValue('gray.200', 'gray.700');
const burntFeesIconColor = useColorModeValue('gray.500', 'inherit'); const burntFeesIconColor = useColorModeValue('gray.500', 'inherit');
return ( return (
<Tr <Tr
as={ motion.tr } as={ motion.tr }
...@@ -41,57 +43,82 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -41,57 +43,82 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
> >
<Td fontSize="sm"> <Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center" mb={ 2 }> <Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations"> <Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal <Skeleton isLoaded={ !isLoading } display="inline-block">
fontWeight={ 600 } <LinkInternal
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) } fontWeight={ 600 }
> href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
{ data.height } >
</LinkInternal> { data.height }
</LinkInternal>
</Skeleton>
</Tooltip> </Tooltip>
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement } isLoading={ isLoading }/>
</Td>
<Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.size.toLocaleString() }
</Skeleton>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString() }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink type="address" alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/> <AddressLink
type="address"
alias={ data.miner.name }
hash={ data.miner.hash }
truncation="constant"
display="inline-flex"
maxW="100%"
isLoading={ isLoading }
/>
</Td> </Td>
<Td isNumeric fontSize="sm"> <Td isNumeric fontSize="sm">
{ data.tx_count > 0 ? ( { data.tx_count > 0 ? (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }> <Skeleton isLoaded={ !isLoading } display="inline-block">
{ data.tx_count } <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.height), tab: 'txs' } }) }>
</LinkInternal> { data.tx_count }
</LinkInternal>
</Skeleton>
) : data.tx_count } ) : data.tx_count }
</Td> </Td>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Td fontSize="sm"> <Td fontSize="sm">
<Box>{ BigNumber(data.gas_used || 0).toFormat() }</Box> <Skeleton isLoaded={ !isLoading } display="inline-block">{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<Flex mt={ 2 }> <Flex mt={ 2 }>
<Tooltip label="Gas Used %"> <Tooltip label={ isLoading ? undefined : 'Gas Used %' }>
<Box> <Box>
<Utilization colorScheme="gray" value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/> <Utilization
colorScheme="gray"
value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }
isLoading={ isLoading }
/>
</Box> </Box>
</Tooltip> </Tooltip>
{ data.gas_target_percentage && ( { data.gas_target_percentage && (
<> <>
<TextSeparator color={ separatorColor } mx={ 1 }/> <TextSeparator color={ separatorColor } mx={ 1 }/>
<GasUsedToTargetRatio value={ data.gas_target_percentage }/> <GasUsedToTargetRatio value={ data.gas_target_percentage } isLoading={ isLoading }/>
</> </>
) } ) }
</Flex> </Flex>
</Td> </Td>
) } ) }
<Td fontSize="sm">{ totalReward.toFixed(8) }</Td> <Td fontSize="sm">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ totalReward.toFixed(8) }
</Skeleton>
</Td>
{ !appConfig.L2.isL2Network && ( { !appConfig.L2.isL2Network && (
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }> <Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor }/> <Icon as={ flameIcon } boxSize={ 5 } color={ burntFeesIconColor } isLoading={ isLoading }/>
{ burntFees.dividedBy(WEI).toFixed(8) } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ burntFees.dividedBy(WEI).toFixed(8) }
</Skeleton>
</Flex> </Flex>
<Tooltip label="Burnt fees / Txn fees * 100%"> <Tooltip label={ isLoading ? undefined : 'Burnt fees / Txn fees * 100%' }>
<Box w="min-content"> <Box w="min-content">
<Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() }/> <Utilization mt={ 2 } value={ burntFees.div(txFees).toNumber() } isLoading={ isLoading }/>
</Box> </Box>
</Tooltip> </Tooltip>
</Td> </Td>
......
import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -10,6 +9,10 @@ import { useAppContext } from 'lib/appContext'; ...@@ -10,6 +9,10 @@ import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals'; import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
...@@ -36,14 +39,22 @@ const BlockPageContent = () => { ...@@ -36,14 +39,22 @@ const BlockPageContent = () => {
const blockQuery = useApiQuery('block', { const blockQuery = useApiQuery('block', {
pathParams: { height }, pathParams: { height },
queryOptions: { enabled: Boolean(height) }, queryOptions: {
enabled: Boolean(height),
placeholderData: BLOCK,
},
}); });
const blockTxsQuery = useQueryWithPages({ const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs', resourceName: 'block_txs',
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(blockQuery.data?.height && tab === 'txs'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && tab === 'txs'),
placeholderData: generateListStub<'block_txs'>(TX, 50, {
block_number: 9004925,
index: 49,
items_count: 50,
}),
}, },
}); });
...@@ -51,7 +62,11 @@ const BlockPageContent = () => { ...@@ -51,7 +62,11 @@ const BlockPageContent = () => {
resourceName: 'block_withdrawals', resourceName: 'block_withdrawals',
pathParams: { height }, pathParams: { height },
options: { options: {
enabled: Boolean(blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'), enabled: Boolean(!blockQuery.isPlaceholderData && blockQuery.data?.height && appConfig.beaconChain.hasBeaconChain && tab === 'withdrawals'),
placeholderData: generateListStub<'block_withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
}, },
}); });
...@@ -98,17 +113,14 @@ const BlockPageContent = () => { ...@@ -98,17 +113,14 @@ const BlockPageContent = () => {
return ( return (
<> <>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } <TextAd mb={ 6 }/>
{ blockQuery.isLoading ? ( <PageTitle
<Skeleton h={ 10 } w="300px" mb={ 6 }/> title={ `Block #${ blockQuery.data?.height }` }
) : ( backLink={ backLink }
<PageTitle contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> }
title={ `Block #${ blockQuery.data?.height }` } isLoading={ blockQuery.isPlaceholderData }
backLink={ backLink } />
contentAfter={ <NetworkExplorers type="block" pathParam={ height } ml={{ base: 'initial', lg: 'auto' }}/> } { blockQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : (
/>
) }
{ blockQuery.isLoading ? <SkeletonTabs/> : (
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
...@@ -58,7 +58,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -58,7 +58,7 @@ test('base view +@mobile +@dark-mode', async({ mount, page }) => {
); );
await page.waitForResponse(BLOCKS_API_URL); await page.waitForResponse(BLOCKS_API_URL);
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('new item from socket', async({ mount, page, createSocket }) => { test('new item from socket', async({ mount, page, createSocket }) => {
...@@ -85,7 +85,7 @@ test('new item from socket', async({ mount, page, createSocket }) => { ...@@ -85,7 +85,7 @@ test('new item from socket', async({ mount, page, createSocket }) => {
}, },
}); });
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('socket error', async({ mount, page, createSocket }) => { test('socket error', async({ mount, page, createSocket }) => {
...@@ -105,5 +105,5 @@ test('socket error', async({ mount, page, createSocket }) => { ...@@ -105,5 +105,5 @@ test('socket error', async({ mount, page, createSocket }) => {
await socketServer.joinChannel(socket, 'blocks:new_block'); await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close(); socket.close();
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,9 +6,10 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -6,9 +6,10 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent'; import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot'; import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
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/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
...@@ -32,6 +33,12 @@ const BlocksPageContent = () => { ...@@ -32,6 +33,12 @@ const BlocksPageContent = () => {
const blocksQuery = useQueryWithPages({ const blocksQuery = useQueryWithPages({
resourceName: 'blocks', resourceName: 'blocks',
filters: { type }, filters: { type },
options: {
placeholderData: generateListStub<'blocks'>(BLOCK, 50, {
block_number: 8988686,
items_count: 50,
}),
},
}); });
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
...@@ -41,7 +48,7 @@ const BlocksPageContent = () => { ...@@ -41,7 +48,7 @@ const BlocksPageContent = () => {
]; ];
return ( return (
<Page> <>
<PageTitle title="Blocks" withTextAd/> <PageTitle title="Blocks" withTextAd/>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
...@@ -49,7 +56,7 @@ const BlocksPageContent = () => { ...@@ -49,7 +56,7 @@ const BlocksPageContent = () => {
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> } rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</Page> </>
); );
}; };
......
...@@ -20,6 +20,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -20,6 +20,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import * as addressStubs from 'stubs/address'; import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token'; import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
...@@ -145,7 +146,7 @@ const TokenPageContent = () => { ...@@ -145,7 +146,7 @@ const TokenPageContent = () => {
scrollRef, scrollRef,
options: { options: {
enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData), enabled: Boolean(router.query.hash && router.query.tab === 'inventory' && hasData),
placeholderData: tokenStubs.TOKEN_INSTANCES, placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE),
}, },
}); });
......
...@@ -7,10 +7,10 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,10 +7,10 @@ import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import EntityTags from 'ui/shared/EntityTags'; import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
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/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
...@@ -38,7 +38,10 @@ const TransactionPageContent = () => { ...@@ -38,7 +38,10 @@ const TransactionPageContent = () => {
const { data, isPlaceholderData } = useApiQuery('tx', { const { data, isPlaceholderData } = useApiQuery('tx', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { enabled: Boolean(hash) }, queryOptions: {
enabled: Boolean(hash),
placeholderData: TX,
},
}); });
const tags = ( const tags = (
...@@ -65,7 +68,7 @@ const TransactionPageContent = () => { ...@@ -65,7 +68,7 @@ const TransactionPageContent = () => {
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
return ( return (
<Page> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title="Transaction details" title="Transaction details"
...@@ -73,7 +76,7 @@ const TransactionPageContent = () => { ...@@ -73,7 +76,7 @@ const TransactionPageContent = () => {
contentAfter={ tags } contentAfter={ tags }
/> />
<RoutedTabs tabs={ TABS }/> <RoutedTabs tabs={ TABS }/>
</Page> </>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,7 +8,8 @@ import useHasAccount from 'lib/hooks/useHasAccount'; ...@@ -9,7 +8,8 @@ import useHasAccount from 'lib/hooks/useHasAccount';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page'; import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent'; import TxsContent from 'ui/txs/TxsContent';
...@@ -31,6 +31,12 @@ const Transactions = () => { ...@@ -31,6 +31,12 @@ const Transactions = () => {
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' }, filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
options: { options: {
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending', enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
placeholderData: generateListStub<'txs_validated'>(TX, 50, {
block_number: 9005713,
index: 5,
items_count: 50,
filter: 'validated',
}),
}, },
}); });
...@@ -71,17 +77,15 @@ const Transactions = () => { ...@@ -71,17 +77,15 @@ const Transactions = () => {
].filter(Boolean); ].filter(Boolean);
return ( return (
<Page> <>
<Box h="100%"> <PageTitle title="Transactions" withTextAd/>
<PageTitle title="Transactions" withTextAd/> <RoutedTabs
<RoutedTabs tabs={ tabs }
tabs={ tabs } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> } stickyEnabled={ !isMobile }
stickyEnabled={ !isMobile } />
/> </>
</Box>
</Page>
); );
}; };
......
...@@ -32,5 +32,5 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -32,5 +32,5 @@ test('base view +@mobile', async({ mount, page }) => {
</TestApp>, </TestApp>,
); );
await expect(component.locator('main')).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,9 +6,10 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -6,9 +6,10 @@ import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem'; import WithdrawalsListItem from 'ui/withdrawals/WithdrawalsListItem';
...@@ -17,16 +18,33 @@ import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable'; ...@@ -17,16 +18,33 @@ import WithdrawalsTable from 'ui/withdrawals/WithdrawalsTable';
const Withdrawals = () => { const Withdrawals = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { data, isError, isLoading, isPaginationVisible, pagination } = useQueryWithPages({ const { data, isError, isPlaceholderData, isPaginationVisible, pagination } = useQueryWithPages({
resourceName: 'withdrawals', resourceName: 'withdrawals',
options: {
placeholderData: generateListStub<'withdrawals'>(WITHDRAWAL, 50, {
index: 5,
items_count: 50,
}),
},
}); });
const countersQuery = useApiQuery('withdrawals_counters'); const countersQuery = useApiQuery('withdrawals_counters');
const content = data?.items ? ( const content = data?.items ? (
<> <>
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show> <Show below="lg" ssr={ false }>
<Hide below="lg" ssr={ false }><WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 }/></Hide> { data.items.map(((item, index) => (
<WithdrawalsListItem
key={ item.index + (isPlaceholderData ? String(index) : '') }
item={ item }
view="list"
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<WithdrawalsTable items={ data.items } view="list" top={ isPaginationVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</> </>
) : null; ) : null;
...@@ -69,18 +87,18 @@ const Withdrawals = () => { ...@@ -69,18 +87,18 @@ const Withdrawals = () => {
); );
return ( return (
<Page> <>
<PageTitle title="Withdrawals" withTextAd/> <PageTitle title="Withdrawals" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }} skeletonProps={{ skeletonDesktopColumns: Array(6).fill(`${ 100 / 6 }%`), skeletonDesktopMinW: '950px' }}
emptyText="There are no withdrawals." emptyText="There are no withdrawals."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
</Page> </>
); );
}; };
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
useColorModeValue, useColorModeValue,
chakra, chakra,
Button, Button,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -10,14 +11,19 @@ import infoIcon from 'icons/info.svg'; ...@@ -10,14 +11,19 @@ import infoIcon from 'icons/info.svg';
interface Props { interface Props {
isOpen?: boolean; isOpen?: boolean;
isLoading?: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const AdditionalInfoButton = ({ isOpen, onClick, className, isLoading }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600'); const infoBgColor = useColorModeValue('blue.50', 'gray.600');
if (isLoading) {
return <Skeleton boxSize={ 6 } borderRadius="sm" flexShrink={ 0 }/>;
}
return ( return (
<Button <Button
variant="unstyled" variant="unstyled"
......
import { Box, HStack, Icon, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { Box, HStack, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import keyIcon from 'icons/key.svg'; import keyIcon from 'icons/key.svg';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
...@@ -13,9 +14,7 @@ interface Props { ...@@ -13,9 +14,7 @@ interface Props {
const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => { const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return ( return (
<HStack spacing={ 2 } alignItems="start"> <HStack spacing={ 2 } alignItems="start">
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block"> <Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') } isLoading={ isLoading }/>
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Box> <Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }> <Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }>
......
import { Text, Tooltip } from '@chakra-ui/react'; import { Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type Props = { type Props = {
value: number; value: number;
isLoading?: boolean;
} }
const GasUsedToTargetRatio = ({ value }: Props) => { const GasUsedToTargetRatio = ({ value, isLoading }: Props) => {
return ( return (
<Tooltip label="% of Gas Target"> <Tooltip label="% of Gas Target">
<Text variant="secondary"> <Skeleton color="text_secondary" isLoaded={ !isLoading }>
{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }% <span>{ (value > 0 ? '+' : '') + value.toLocaleString(undefined, { maximumFractionDigits: 2 }) }%</span>
</Text> </Skeleton>
</Tooltip> </Tooltip>
); );
}; };
......
import { Tag, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import Tag from 'ui/shared/chakra/Tag';
interface Props { interface Props {
isIn: boolean; isIn: boolean;
isOut: boolean; isOut: boolean;
className?: string; className?: string;
isLoading?: boolean;
} }
const InOutTag = ({ isIn, isOut, className }: Props) => { const InOutTag = ({ isIn, isOut, className, isLoading }: Props) => {
if (!isIn && !isOut) { if (!isIn && !isOut) {
return null; return null;
} }
...@@ -20,6 +23,7 @@ const InOutTag = ({ isIn, isOut, className }: Props) => { ...@@ -20,6 +23,7 @@ const InOutTag = ({ isIn, isOut, className }: Props) => {
colorScheme={ colorScheme } colorScheme={ colorScheme }
display="flex" display="flex"
justifyContent="center" justifyContent="center"
isLoading={ isLoading }
> >
{ isOut ? 'OUT' : 'IN' } { isOut ? 'OUT' : 'IN' }
</Tag> </Tag>
......
import { Grid, chakra, GridItem } from '@chakra-ui/react'; import { Grid, chakra, GridItem, Skeleton } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import React from 'react'; import React from 'react';
...@@ -38,13 +38,21 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) = ...@@ -38,13 +38,21 @@ const Container = chakra(({ isAnimated, children, className }: ContainerProps) =
interface LabelProps { interface LabelProps {
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean;
} }
const Label = chakra(({ children, className }: LabelProps) => { const Label = chakra(({ children, className, isLoading }: LabelProps) => {
return ( return (
<GridItem className={ className } fontWeight={ 500 } lineHeight="20px" py="5px"> <Skeleton
className={ className }
isLoaded={ !isLoading }
fontWeight={ 500 }
lineHeight="20px"
my="5px"
justifySelf="start"
>
{ children } { children }
</GridItem> </Skeleton>
); );
}); });
......
import { Button, Flex, Icon, IconButton, chakra } from '@chakra-ui/react'; import { Button, Skeleton, Flex, Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
...@@ -11,9 +11,10 @@ export type Props = { ...@@ -11,9 +11,10 @@ export type Props = {
hasNextPage: boolean; hasNextPage: boolean;
className?: string; className?: string;
canGoBackwards: boolean; canGoBackwards: boolean;
isLoading?: boolean;
} }
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, className, canGoBackwards }: Props) => { const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, className, canGoBackwards, isLoading }: Props) => {
return ( return (
<Flex <Flex
...@@ -21,46 +22,51 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -21,46 +22,51 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
fontSize="sm" fontSize="sm"
alignItems="center" alignItems="center"
> >
<Button <Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 4 } borderRadius="base">
variant="outline" <Button
size="sm" variant="outline"
onClick={ resetPage } size="sm"
isDisabled={ page === 1 } onClick={ resetPage }
mr={ 4 } isDisabled={ page === 1 }
> >
First First
</Button> </Button>
<IconButton </Skeleton>
variant="outline" <Skeleton isLoaded={ !isLoading } display="inline-block" mr={ 3 } borderRadius="base">
onClick={ onPrevPageClick } <IconButton
size="sm" variant="outline"
aria-label="Next page" onClick={ onPrevPageClick }
w="36px" size="sm"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> } aria-label="Next page"
mr={ 3 } w="36px"
isDisabled={ !canGoBackwards || page === 1 } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
/> isDisabled={ !canGoBackwards || page === 1 }
<Button />
variant="outline" </Skeleton>
size="sm" <Skeleton isLoaded={ !isLoading } display="inline-block" borderRadius="base">
isActive <Button
borderWidth="1px" variant="outline"
fontWeight={ 400 } size="sm"
h={ 8 } isActive
cursor="unset" borderWidth="1px"
> fontWeight={ 400 }
{ page } h={ 8 }
</Button> cursor="unset"
<IconButton >
variant="outline" { page }
onClick={ onNextPageClick } </Button>
size="sm" </Skeleton>
aria-label="Next page" <Skeleton isLoaded={ !isLoading } display="inline-block" ml={ 3 } borderRadius="base">
w="36px" <IconButton
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> } variant="outline"
ml={ 3 } onClick={ onNextPageClick }
isDisabled={ !hasNextPage } size="sm"
/> aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
isDisabled={ !hasNextPage }
/>
</Skeleton>
{ /* not implemented yet */ } { /* not implemented yet */ }
{ /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}> { /* <Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/> Go to <Input w="84px" size="xs" ml={ 2 }/>
......
import { Box, Icon, IconButton, chakra, Tooltip } from '@chakra-ui/react'; import { Box, Icon, IconButton, chakra, Tooltip, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrow from 'icons/arrows/east-mini.svg'; import eastArrow from 'icons/arrows/east-mini.svg';
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
nextLabel?: string; nextLabel?: string;
isPrevDisabled?: boolean; isPrevDisabled?: boolean;
isNextDisabled?: boolean; isNextDisabled?: boolean;
isLoading?: boolean;
} }
const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled }: Props) => { const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, isNextDisabled, isLoading }: Props) => {
const handelPrevClick = React.useCallback(() => { const handelPrevClick = React.useCallback(() => {
onClick('prev'); onClick('prev');
}, [ onClick ]); }, [ onClick ]);
...@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is ...@@ -21,6 +22,15 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
onClick('next'); onClick('next');
}, [ onClick ]); }, [ onClick ]);
if (isLoading) {
return (
<Flex columnGap="10px" className={ className }>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
</Flex>
);
}
return ( return (
<Box className={ className }> <Box className={ className }>
<Tooltip label={ prevLabel }> <Tooltip label={ prevLabel }>
......
...@@ -61,6 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, typ ...@@ -61,6 +61,7 @@ const SocketNewItemsNotice = chakra(({ children, className, url, num, alert, typ
py="6px" py="6px"
fontWeight={ 400 } fontWeight={ 400 }
fontSize="sm" fontSize="sm"
lineHeight={ 5 }
bgColor={ bgColor } bgColor={ bgColor }
color={ color } color={ color }
> >
...@@ -77,7 +78,7 @@ export const Desktop = ({ ...props }: Props) => { ...@@ -77,7 +78,7 @@ export const Desktop = ({ ...props }: Props) => {
return ( return (
<SocketNewItemsNotice <SocketNewItemsNotice
borderRadius={ props.isLoading ? 'sm' : 0 } borderRadius={ props.isLoading ? 'sm' : 0 }
h={ props.isLoading ? 4 : 'auto' } h={ props.isLoading ? 5 : 'auto' }
maxW={ props.isLoading ? '215px' : undefined } maxW={ props.isLoading ? '215px' : undefined }
w="100%" w="100%"
mx={ props.isLoading ? 4 : 0 } mx={ props.isLoading ? 4 : 0 }
......
...@@ -15,7 +15,7 @@ export interface Props { ...@@ -15,7 +15,7 @@ export interface Props {
const TokenLogo = ({ className, isLoading, data }: Props) => { const TokenLogo = ({ className, isLoading, data }: Props) => {
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } borderRadius="base"/>; return <Skeleton className={ className } borderRadius="base" flexShrink={ 0 }/>;
} }
const logoSrc = (() => { const logoSrc = (() => {
......
...@@ -12,14 +12,15 @@ interface Props { ...@@ -12,14 +12,15 @@ interface Props {
className?: string; className?: string;
logoSize?: number; logoSize?: number;
isDisabled?: boolean; isDisabled?: boolean;
isLoading?: boolean;
hideSymbol?: boolean; hideSymbol?: boolean;
} }
const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol }: Props) => { const TokenSnippet = ({ data, className, logoSize = 6, isDisabled, hideSymbol, isLoading }: Props) => {
return ( return (
<Flex className={ className } alignItems="center" columnGap={ 2 } w="100%"> <Flex className={ className } alignItems="center" columnGap={ 2 } w="100%">
<TokenLogo boxSize={ logoSize } data={ data }/> <TokenLogo boxSize={ logoSize } data={ data } isLoading={ isLoading }/>
<AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled }/> <AddressLink hash={ data?.address || '' } alias={ data?.name || 'Unnamed token' } type="token" isDisabled={ isDisabled } isLoading={ isLoading }/>
{ data?.symbol && !hideSymbol && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> } { data?.symbol && !hideSymbol && <Text variant="secondary">({ trimTokenSymbol(data.symbol) })</Text> }
</Flex> </Flex>
); );
......
...@@ -19,6 +19,7 @@ interface Props { ...@@ -19,6 +19,7 @@ interface Props {
withAddressFilter?: boolean; withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void; onAddressFilterChange?: (nextValue: string) => void;
defaultAddressFilter?: AddressFromToFilter; defaultAddressFilter?: AddressFromToFilter;
isLoading?: boolean;
} }
const TokenTransferFilter = ({ const TokenTransferFilter = ({
...@@ -28,10 +29,11 @@ const TokenTransferFilter = ({ ...@@ -28,10 +29,11 @@ const TokenTransferFilter = ({
withAddressFilter, withAddressFilter,
onAddressFilterChange, onAddressFilterChange,
defaultAddressFilter, defaultAddressFilter,
isLoading,
}: Props) => { }: Props) => {
return ( return (
<PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }}> <PopoverFilter appliedFiltersNum={ appliedFiltersNum } contentProps={{ w: '200px' }} isLoading={ isLoading }>
{ withAddressFilter && ( { withAddressFilter && (
<> <>
<Text variant="secondary" fontWeight={ 600 }>Address</Text> <Text variant="secondary" fontWeight={ 600 }>Address</Text>
......
...@@ -10,18 +10,20 @@ interface Props { ...@@ -10,18 +10,20 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => { const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement, isLoading }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item) => ( { data.map((item, index) => (
<TokenTransferListItem <TokenTransferListItem
key={ item.tx_hash + item.block_hash + item.log_index } key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item } { ...item }
baseAddress={ baseAddress } baseAddress={ baseAddress }
showTxInfo={ showTxInfo } showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/> />
)) } )) }
</Box> </Box>
......
import { Text, Flex, Tag, Icon } from '@chakra-ui/react'; import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -10,6 +10,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -10,6 +10,9 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
...@@ -17,12 +20,11 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; ...@@ -17,12 +20,11 @@ import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferListItem = ({ const TokenTransferListItem = ({
...@@ -36,6 +38,7 @@ const TokenTransferListItem = ({ ...@@ -36,6 +38,7 @@ const TokenTransferListItem = ({
type, type,
timestamp, timestamp,
enableTimeIncrement, enableTimeIncrement,
isLoading,
}: Props) => { }: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
...@@ -51,57 +54,62 @@ const TokenTransferListItem = ({ ...@@ -51,57 +54,62 @@ const TokenTransferListItem = ({
return ( return (
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" justifyContent="space-between"> <Flex w="100%" justifyContent="space-between">
<Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }> <Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 } columnGap={ 2 }>
<TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol/> <TokenSnippet data={ token } w="auto" maxW="calc(100% - 140px)" hideSymbol isLoading={ isLoading }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag> <Tag flexShrink={ 0 } isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag> <Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex> </Flex>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<TxAdditionalInfo hash={ txHash } isMobile/> <TxAdditionalInfo hash={ txHash } isMobile isLoading={ isLoading }/>
) } ) }
</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 } isLoading={ isLoading }/> }
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex> <Flex>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
boxSize="30px" boxSize="30px"
mr={ 2 }
color="link" color="link"
isLoading={ isLoading }
/> />
<Address width="100%"> <Address width="100%" ml={ 2 }>
<AddressLink <AddressLink
hash={ txHash } hash={ txHash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
) } ) }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isDisabled={ baseAddress === from.hash } isLoading={ isLoading }/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> } { baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" isLoading={ isLoading }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } isDisabled={ baseAddress === to.hash } isLoading={ isLoading }/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> } { baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Flex> </Flex>
{ value && ( { value && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Value</Text> <Skeleton isLoaded={ !isLoading } fontWeight={ 500 } flexShrink={ 0 }>Value</Skeleton>
<Text variant="secondary">{ value }</Text> <Skeleton isLoaded={ !isLoading } color="text_secondary"><span>{ value }</span></Skeleton>
</Flex> </Flex>
) } ) }
</ListItemMobile> </ListItemMobile>
......
import { Box, Icon, chakra, Skeleton } from '@chakra-ui/react'; import { Box, chakra, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
import nftPlaceholder from 'icons/nft_shield.svg'; import nftPlaceholder from 'icons/nft_shield.svg';
import Icon from 'ui/shared/chakra/Icon';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -29,10 +30,8 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncati ...@@ -29,10 +30,8 @@ const TokenTransferNft = ({ hash, id, className, isDisabled, isLoading, truncati
w="100%" w="100%"
className={ className } className={ className }
> >
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 1 } borderRadius="base"> <Icon as={ nftPlaceholder } boxSize="30px" color="inherit" isLoading={ isLoading } borderRadius="base"/>
<Icon as={ nftPlaceholder } boxSize="30px" color="inherit"/> <Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)" ml={ 1 }>
</Skeleton>
<Skeleton isLoaded={ !isLoading } maxW="calc(100% - 34px)">
{ truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> } { truncation === 'constant' ? <HashStringShorten hash={ id }/> : <HashStringShortenDynamic hash={ id } fontWeight={ 500 }/> }
</Skeleton> </Skeleton>
</Component> </Component>
......
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react'; import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem'; import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
...@@ -16,6 +16,7 @@ interface Props { ...@@ -16,6 +16,7 @@ interface Props {
showSocketInfo?: boolean; showSocketInfo?: boolean;
socketInfoAlert?: string; socketInfoAlert?: string;
socketInfoNum?: number; socketInfoNum?: number;
isLoading?: boolean;
} }
const TokenTransferTable = ({ const TokenTransferTable = ({
...@@ -27,6 +28,7 @@ const TokenTransferTable = ({ ...@@ -27,6 +28,7 @@ const TokenTransferTable = ({
showSocketInfo, showSocketInfo,
socketInfoAlert, socketInfoAlert,
socketInfoNum, socketInfoNum,
isLoading,
}: Props) => { }: Props) => {
return ( return (
...@@ -45,26 +47,22 @@ const TokenTransferTable = ({ ...@@ -45,26 +47,22 @@ const TokenTransferTable = ({
</Thead> </Thead>
<Tbody> <Tbody>
{ showSocketInfo && ( { showSocketInfo && (
<Tr> <SocketNewItemsNotice.Desktop
<Td colSpan={ 10 } p={ 0 }> url={ window.location.href }
<SocketNewItemsNotice alert={ socketInfoAlert }
borderRadius={ 0 } num={ socketInfoNum }
pl="10px" type="token_transfer"
url={ window.location.href } isLoading={ isLoading }
alert={ socketInfoAlert } />
num={ socketInfoNum }
type="token_transfer"
/>
</Td>
</Tr>
) } ) }
{ data.map((item) => ( { data.map((item, index) => (
<TokenTransferTableItem <TokenTransferTableItem
key={ item.tx_hash + item.block_hash + item.log_index } key={ item.tx_hash + item.block_hash + item.log_index + (isLoading ? index : '') }
{ ...item } { ...item }
baseAddress={ baseAddress } baseAddress={ baseAddress }
showTxInfo={ showTxInfo } showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { Tr, Td, Tag, Flex, Text } from '@chakra-ui/react'; import { Tr, Td, Flex, Skeleton, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -8,18 +8,19 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -8,18 +8,19 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
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 Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import CopyToClipboard from '../CopyToClipboard';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TokenTransferTableItem = ({ const TokenTransferTableItem = ({
...@@ -33,6 +34,7 @@ const TokenTransferTableItem = ({ ...@@ -33,6 +34,7 @@ const TokenTransferTableItem = ({
type, type,
timestamp, timestamp,
enableTimeIncrement, enableTimeIncrement,
isLoading,
}: Props) => { }: Props) => {
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement); const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
...@@ -40,48 +42,81 @@ const TokenTransferTableItem = ({ ...@@ -40,48 +42,81 @@ const TokenTransferTableItem = ({
<Tr alignItems="top"> <Tr alignItems="top">
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Td> <Td>
<TxAdditionalInfo hash={ txHash }/> <Box my="3px">
<TxAdditionalInfo hash={ txHash } isLoading={ isLoading }/>
</Box>
</Td> </Td>
) } ) }
<Td> <Td>
<Flex flexDir="column" alignItems="flex-start"> <Flex flexDir="column" alignItems="flex-start" my="3px" rowGap={ 2 }>
<TokenSnippet data={ token } lineHeight="30px" hideSymbol/> <TokenSnippet data={ token } isLoading={ isLoading } hideSymbol/>
<Tag mt={ 1 }>{ token.type }</Tag> <Tag isLoading={ isLoading }>{ token.type }</Tag>
<Tag colorScheme="orange" mt={ 2 }>{ getTokenTransferTypeText(type) }</Tag> <Tag colorScheme="orange" isLoading={ isLoading }>{ getTokenTransferTypeText(type) }</Tag>
</Flex> </Flex>
</Td> </Td>
<Td lineHeight="30px"> <Td>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id } isLoading={ isLoading }/> }
</Td> </Td>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Td> <Td>
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" maxW="100%" fontWeight={ 600 } mt="7px">
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash } isLoading={ isLoading }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> } { timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" mt="10px" display="inline-block">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Td> </Td>
) } ) }
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isDisabled={ baseAddress === from.hash }/> <AddressLink
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash }/> } type="address" ml={ 2 }
fontWeight="500"
hash={ from.hash }
alias={ from.name }
flexGrow={ 1 }
isDisabled={ baseAddress === from.hash }
isLoading={ isLoading }
/>
{ baseAddress !== from.hash && <CopyToClipboard text={ from.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Td> </Td>
{ baseAddress && ( { baseAddress && (
<Td px={ 0 }> <Td px={ 0 }>
<InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" mt="3px"/> <Box mt="3px">
<InOutTag
isIn={ baseAddress === to.hash }
isOut={ baseAddress === from.hash }
w="50px"
textAlign="center"
isLoading={ isLoading }
/>
</Box>
</Td> </Td>
) } ) }
<Td> <Td>
<Address display="inline-flex" maxW="100%" lineHeight="30px"> <Address display="inline-flex" maxW="100%" my="3px">
<AddressIcon address={ to }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ to.hash } alias={ to.name } flexGrow={ 1 } isDisabled={ baseAddress === to.hash }/> <AddressLink
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash }/> } type="address"
ml={ 2 }
fontWeight="500"
hash={ to.hash }
alias={ to.name }
flexGrow={ 1 }
isDisabled={ baseAddress === to.hash }
isLoading={ isLoading }
/>
{ baseAddress !== to.hash && <CopyToClipboard text={ to.hash } isLoading={ isLoading }/> }
</Address> </Address>
</Td> </Td>
<Td isNumeric verticalAlign="top" lineHeight="30px"> <Td isNumeric verticalAlign="top">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() } <Skeleton isLoaded={ !isLoading } display="inline-block" my="7px">
{ 'value' in total && BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat() }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Icon, Skeleton, useColorModeValue } from '@chakra-ui/react'; import { useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
...@@ -14,9 +15,7 @@ interface Props { ...@@ -14,9 +15,7 @@ interface Props {
const TransactionSnippet = ({ hash, isLoading }: Props) => { const TransactionSnippet = ({ hash, isLoading }: Props) => {
return ( return (
<Address maxW="100%"> <Address maxW="100%">
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } borderRadius="base"> <Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') } isLoading={ isLoading }/>
<Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<AddressLink hash={ hash } fontWeight="600" type="transaction" ml={ 2 } isLoading={ isLoading }/> <AddressLink hash={ hash } fontWeight="600" type="transaction" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ hash } isLoading={ isLoading }/> <CopyToClipboard text={ hash } isLoading={ isLoading }/>
</Address> </Address>
......
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react'; import { TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
...@@ -6,13 +6,15 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -6,13 +6,15 @@ import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg'; import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg'; import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
import Tag from 'ui/shared/chakra/Tag';
export interface Props { export interface Props {
status: Transaction['status']; status: Transaction['status'];
errorText?: string | null; errorText?: string | null;
isLoading?: boolean;
} }
const TxStatus = ({ status, errorText }: Props) => { const TxStatus = ({ status, errorText, isLoading }: Props) => {
let label; let label;
let icon; let icon;
let colorScheme; let colorScheme;
...@@ -39,7 +41,7 @@ const TxStatus = ({ status, errorText }: Props) => { ...@@ -39,7 +41,7 @@ const TxStatus = ({ status, errorText }: Props) => {
return ( return (
<Tooltip label={ errorText }> <Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex"> <Tag colorScheme={ colorScheme } display="inline-flex" isLoading={ isLoading }>
<TagLeftIcon boxSize={ 2.5 } as={ icon }/> <TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel> <TagLabel>{ label }</TagLabel>
</Tag> </Tag>
......
...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => { ...@@ -55,7 +55,7 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
} }
if (isLoading) { if (isLoading) {
return <Skeleton className={ className } h={{ base: 12, lg: 6 }} maxW="1000px"/>; return <Skeleton className={ className } h={{ base: 12, lg: 6 }} w="100%" maxW="1000px"/>;
} }
if (!adData) { if (!adData) {
......
import { Skeleton, Icon as ChakraIcon } from '@chakra-ui/react';
import type { IconProps, As } from '@chakra-ui/react';
import React from 'react';
interface Props extends IconProps {
isLoading?: boolean;
as: As;
}
const Icon = ({ isLoading, ...props }: Props, ref: React.LegacyRef<SVGSVGElement>) => {
return (
<Skeleton isLoaded={ !isLoading } boxSize={ props.boxSize } w={ props.w } h={ props.h } borderRadius={ props.borderRadius }>
<ChakraIcon { ...props } ref={ ref }/>
</Skeleton>
);
};
export default React.memo(React.forwardRef(Icon));
...@@ -8,7 +8,7 @@ interface Props extends TagProps { ...@@ -8,7 +8,7 @@ interface Props extends TagProps {
isLoading?: boolean; isLoading?: boolean;
} }
const Tag = ({ isLoading, ...props }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const Tag = ({ isLoading, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
if (props.isTruncated && typeof props.children === 'string') { if (props.isTruncated && typeof props.children === 'string') {
if (!props.children) { if (!props.children) {
......
import type { As } from '@chakra-ui/react'; import type { As } from '@chakra-ui/react';
import { Box, Button, Circle, Icon, useColorModeValue } from '@chakra-ui/react'; import { Skeleton, Box, Button, Circle, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import filterIcon from 'icons/filter.svg'; import filterIcon from 'icons/filter.svg';
...@@ -8,15 +8,20 @@ const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }} ...@@ -8,15 +8,20 @@ const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}
interface Props { interface Props {
isActive?: boolean; isActive?: boolean;
isLoading?: boolean;
appliedFiltersNum?: number; appliedFiltersNum?: number;
onClick: () => void; onClick: () => void;
as?: As; as?: As;
} }
const FilterButton = ({ isActive, appliedFiltersNum, onClick, as }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const FilterButton = ({ isActive, isLoading, appliedFiltersNum, onClick, as }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const badgeColor = useColorModeValue('white', 'black'); const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50'); const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
if (isLoading) {
return <Skeleton w={{ base: 9, lg: '78px' }} h={ 8 } borderRadius="base"/>;
}
return ( return (
<Button <Button
ref={ ref } ref={ ref }
......
...@@ -15,9 +15,10 @@ interface Props { ...@@ -15,9 +15,10 @@ interface Props {
isActive?: boolean; isActive?: boolean;
children: React.ReactNode; children: React.ReactNode;
contentProps?: PopoverContentProps; contentProps?: PopoverContentProps;
isLoading?: boolean;
} }
const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive }: Props) => { const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive, isLoading }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
return ( return (
...@@ -27,6 +28,7 @@ const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive }: ...@@ -27,6 +28,7 @@ const PopoverFilter = ({ appliedFiltersNum, children, contentProps, isActive }:
isActive={ isOpen || isActive || Number(appliedFiltersNum) > 0 } isActive={ isOpen || isActive || Number(appliedFiltersNum) > 0 }
onClick={ onToggle } onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum } appliedFiltersNum={ appliedFiltersNum }
isLoading={ isLoading }
/> />
</PopoverTrigger> </PopoverTrigger>
<PopoverContent { ...contentProps }> <PopoverContent { ...contentProps }>
......
import { Flex, Text, Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; import { Flex, Grid, GridItem, useColorModeValue, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { DecodedInput } from 'types/api/decodedInput'; import type { DecodedInput } from 'types/api/decodedInput';
...@@ -13,12 +13,13 @@ interface RowProps { ...@@ -13,12 +13,13 @@ interface RowProps {
name: string; name: string;
type: string; type: string;
indexed?: boolean; indexed?: boolean;
isLoading?: boolean;
} }
const PADDING = 4; const PADDING = 4;
const GAP = 5; const GAP = 5;
const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { const TableRow = ({ isLast, name, type, children, indexed, isLoading }: RowProps) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
...@@ -31,7 +32,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -31,7 +32,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
bgColor={ bgColor } bgColor={ bgColor }
borderBottomLeftRadius={ isLast ? 'md' : 'none' } borderBottomLeftRadius={ isLast ? 'md' : 'none' }
> >
{ name } <Skeleton isLoaded={ !isLoading } display="inline-block">{ name }</Skeleton>
</GridItem> </GridItem>
<GridItem <GridItem
pr={ GAP } pr={ GAP }
...@@ -39,7 +40,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -39,7 +40,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
pb={ isLast ? PADDING : 0 } pb={ isLast ? PADDING : 0 }
bgColor={ bgColor } bgColor={ bgColor }
> >
{ type } <Skeleton isLoaded={ !isLoading } display="inline-block">{ type }</Skeleton>
</GridItem> </GridItem>
{ indexed !== undefined && ( { indexed !== undefined && (
<GridItem <GridItem
...@@ -48,7 +49,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -48,7 +49,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
pb={ isLast ? PADDING : 0 } pb={ isLast ? PADDING : 0 }
bgColor={ bgColor } bgColor={ bgColor }
> >
{ indexed ? 'true' : 'false' } <Skeleton isLoaded={ !isLoading } display="inline-block">{ indexed ? 'true' : 'false' }</Skeleton>
</GridItem> </GridItem>
) } ) }
<GridItem <GridItem
...@@ -66,9 +67,10 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -66,9 +67,10 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
interface Props { interface Props {
data: DecodedInput; data: DecodedInput;
isLoading?: boolean;
} }
const LogDecodedInputData = ({ data }: Props) => { const LogDecodedInputData = ({ data, isLoading }: 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);
...@@ -81,10 +83,10 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -81,10 +83,10 @@ const LogDecodedInputData = ({ data }: Props) => {
<Grid gridTemplateColumns={ gridTemplateColumns } fontSize="sm" lineHeight={ 5 } w="100%"> <Grid gridTemplateColumns={ gridTemplateColumns } fontSize="sm" lineHeight={ 5 } w="100%">
{ /* FIRST PART OF BLOCK */ } { /* FIRST PART OF BLOCK */ }
<GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: colNumber, lg: undefined }}> <GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: colNumber, lg: undefined }}>
Method Id <Skeleton isLoaded={ !isLoading }>Method Id</Skeleton>
</GridItem> </GridItem>
<GridItem colSpan={{ base: colNumber, lg: colNumber - 1 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}> <GridItem colSpan={{ base: colNumber, lg: colNumber - 1 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>
{ data.method_id } <Skeleton isLoaded={ !isLoading } display="inline-block">{ data.method_id }</Skeleton>
</GridItem> </GridItem>
<GridItem <GridItem
py={ 2 } py={ 2 }
...@@ -96,7 +98,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -96,7 +98,7 @@ const LogDecodedInputData = ({ data }: Props) => {
borderTopWidth="1px" borderTopWidth="1px"
colSpan={{ base: colNumber, lg: undefined }} colSpan={{ base: colNumber, lg: undefined }}
> >
Call <Skeleton isLoaded={ !isLoading }>Call</Skeleton>
</GridItem> </GridItem>
<GridItem <GridItem
py={{ base: 0, lg: 2 }} py={{ base: 0, lg: 2 }}
...@@ -108,7 +110,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -108,7 +110,7 @@ const LogDecodedInputData = ({ data }: Props) => {
borderTopWidth={{ base: '0px', lg: '1px' }} borderTopWidth={{ base: '0px', lg: '1px' }}
whiteSpace="normal" whiteSpace="normal"
> >
{ data.method_call } <Skeleton isLoaded={ !isLoading } display="inline-block">{ data.method_call }</Skeleton>
</GridItem> </GridItem>
{ /* TABLE INSIDE OF BLOCK */ } { /* TABLE INSIDE OF BLOCK */ }
{ data.parameters.length > 0 && ( { data.parameters.length > 0 && (
...@@ -121,7 +123,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -121,7 +123,7 @@ const LogDecodedInputData = ({ data }: Props) => {
bgColor={ bgColor } bgColor={ bgColor }
fontWeight={ 600 } fontWeight={ 600 }
> >
Name <Skeleton isLoaded={ !isLoading } display="inline-block">Name</Skeleton>
</GridItem> </GridItem>
<GridItem <GridItem
pr={ GAP } pr={ GAP }
...@@ -130,7 +132,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -130,7 +132,7 @@ const LogDecodedInputData = ({ data }: Props) => {
bgColor={ bgColor } bgColor={ bgColor }
fontWeight={ 600 } fontWeight={ 600 }
> >
Type <Skeleton isLoaded={ !isLoading } display="inline-block">Type</Skeleton>
</GridItem> </GridItem>
{ hasIndexed && ( { hasIndexed && (
<GridItem <GridItem
...@@ -140,7 +142,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -140,7 +142,7 @@ const LogDecodedInputData = ({ data }: Props) => {
bgColor={ bgColor } bgColor={ bgColor }
fontWeight={ 600 } fontWeight={ 600 }
> >
Inde<wbr/>xed? <Skeleton isLoaded={ !isLoading } display="inline-block">Inde<wbr/>xed?</Skeleton>
</GridItem> </GridItem>
) } ) }
<GridItem <GridItem
...@@ -150,7 +152,7 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -150,7 +152,7 @@ const LogDecodedInputData = ({ data }: Props) => {
bgColor={ bgColor } bgColor={ bgColor }
fontWeight={ 600 } fontWeight={ 600 }
> >
Data <Skeleton isLoaded={ !isLoading } display="inline-block">Data</Skeleton>
</GridItem> </GridItem>
</> </>
) } ) }
...@@ -159,8 +161,8 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -159,8 +161,8 @@ const LogDecodedInputData = ({ data }: Props) => {
if (type === 'address' && typeof value === 'string') { if (type === 'address' && typeof value === 'string') {
return ( return (
<Address justifyContent="space-between"> <Address justifyContent="space-between">
<AddressLink type="address" hash={ value }/> <AddressLink type="address" hash={ value } isLoading={ isLoading }/>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value } isLoading={ isLoading }/>
</Address> </Address>
); );
} }
...@@ -169,22 +171,29 @@ const LogDecodedInputData = ({ data }: Props) => { ...@@ -169,22 +171,29 @@ const LogDecodedInputData = ({ data }: Props) => {
const text = JSON.stringify(value, undefined, 4); const text = JSON.stringify(value, undefined, 4);
return ( return (
<Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all"> <Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<div>{ text }</div> <Skeleton isLoaded={ !isLoading } display="inline-block">{ text }</Skeleton>
<CopyToClipboard text={ text }/> <CopyToClipboard text={ text } isLoading={ isLoading }/>
</Flex> </Flex>
); );
} }
return ( return (
<Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all"> <Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<Text>{ value }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ value }</Skeleton>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value } isLoading={ isLoading }/>
</Flex> </Flex>
); );
})(); })();
return ( return (
<TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }> <TableRow
key={ name }
name={ name }
type={ type }
isLast={ index === data.parameters.length - 1 }
indexed={ indexed }
isLoading={ isLoading }
>
{ content } { content }
</TableRow> </TableRow>
); );
......
import { Text, Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link } from '@chakra-ui/react'; import { Grid, GridItem, Tooltip, Button, useColorModeValue, Alert, Link, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -14,15 +14,16 @@ import LogTopic from 'ui/shared/logs/LogTopic'; ...@@ -14,15 +14,16 @@ import LogTopic from 'ui/shared/logs/LogTopic';
type Props = Log & { type Props = Log & {
type: 'address' | 'transaction'; type: 'address' | 'transaction';
isLoading?: boolean;
}; };
const RowHeader = ({ children }: { children: React.ReactNode }) => ( const RowHeader = ({ children, isLoading }: { children: React.ReactNode; isLoading?: boolean }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}> <GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
<Text fontWeight={ 500 }>{ children }</Text> <Skeleton fontWeight={ 500 } isLoaded={ !isLoading } display="inline-block">{ children }</Skeleton>
</GridItem> </GridItem>
); );
const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash }: Props) => { const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash, isLoading }: 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');
...@@ -50,14 +51,15 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash ...@@ -50,14 +51,15 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
</Alert> </Alert>
</GridItem> </GridItem>
) } ) }
{ hasTxInfo ? <RowHeader>Transaction</RowHeader> : <RowHeader>Address</RowHeader> } { hasTxInfo ? <RowHeader isLoading={ isLoading }>Transaction</RowHeader> : <RowHeader isLoading={ isLoading }>Address</RowHeader> }
<GridItem display="flex" alignItems="center"> <GridItem display="flex" alignItems="center">
<Address mr={{ base: 9, lg: 0 }}> <Address mr={{ base: 9, lg: 0 }}>
{ !hasTxInfo && <AddressIcon address={ address } mr={ 2 }/> } { !hasTxInfo && <AddressIcon address={ address } mr={ 2 } isLoading={ isLoading }/> }
<AddressLink <AddressLink
hash={ hasTxInfo ? txHash : address.hash } hash={ hasTxInfo ? txHash : address.hash }
alias={ hasTxInfo ? undefined : address.name } alias={ hasTxInfo ? undefined : address.name }
type={ type === 'address' ? 'transaction' : 'address' } type={ type === 'address' ? 'transaction' : 'address' }
isLoading={ isLoading }
/> />
</Address> </Address>
{ /* api doesn't have find topic feature yet */ } { /* api doesn't have find topic feature yet */ }
...@@ -66,34 +68,37 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash ...@@ -66,34 +68,37 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
<Icon as={ searchIcon } boxSize={ 5 }/> <Icon as={ searchIcon } boxSize={ 5 }/>
</Link> </Link>
</Tooltip> */ } </Tooltip> */ }
<Tooltip label="Log index"> <Skeleton isLoaded={ !isLoading } ml="auto" borderRadius="base">
<Button variant="outline" colorScheme="gray" isActive ml="auto" size="sm" fontWeight={ 400 }> <Tooltip label="Log index">
{ index } <Button variant="outline" colorScheme="gray" isActive size="sm" fontWeight={ 400 }>
</Button> { index }
</Tooltip> </Button>
</Tooltip>
</Skeleton>
</GridItem> </GridItem>
{ decoded && ( { decoded && (
<> <>
<RowHeader>Decode input data</RowHeader> <RowHeader isLoading={ isLoading }>Decode input data</RowHeader>
<GridItem> <GridItem>
<LogDecodedInputData data={ decoded }/> <LogDecodedInputData data={ decoded } isLoading={ isLoading }/>
</GridItem> </GridItem>
</> </>
) } ) }
<RowHeader>Topics</RowHeader> <RowHeader isLoading={ isLoading }>Topics</RowHeader>
<GridItem> <GridItem>
{ topics.filter(Boolean).map((item, index) => ( { topics.filter(Boolean).map((item, index) => (
<LogTopic <LogTopic
key={ index } key={ index }
hex={ item } hex={ item }
index={ index } index={ index }
isLoading={ isLoading }
/> />
)) } )) }
</GridItem> </GridItem>
<RowHeader>Data</RowHeader> <RowHeader isLoading={ isLoading }>Data</RowHeader>
<GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }> <Skeleton isLoaded={ !isLoading } p={ 4 } fontSize="sm" borderRadius="md" bgColor={ isLoading ? undefined : dataBgColor }>
{ data } { data }
</GridItem> </Skeleton>
</Grid> </Grid>
); );
}; };
......
import { Flex, Grid, GridItem, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const RowHeader = () => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }} _first={{ alignSelf: 'center' }}>
<Skeleton h={ 6 } borderRadius="full" w="150px"/>
</GridItem>
);
const TopicRow = () => (
<Flex columnGap={ 3 }>
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } w="70px" borderRadius="full"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full"/>
</Flex>
);
const LogSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Grid
gap={{ base: 2, lg: 8 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
<RowHeader/>
<GridItem display="flex" alignItems="center">
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full" ml={ 2 } mr={ 9 }/>
<Skeleton h={ 8 } w={ 8 } borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="150px" w="100%" borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem display="flex" flexDir="column" rowGap={ 3 }>
<TopicRow/>
<TopicRow/>
<TopicRow/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="60px" w="100%" borderRadius="base"/>
</GridItem>
</Grid>
);
};
export default LogSkeleton;
import { Flex, Button, Select, Box } from '@chakra-ui/react'; import { Flex, Button, Select, Skeleton } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
...@@ -12,6 +12,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -12,6 +12,7 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
hex: string; hex: string;
index: number; index: number;
isLoading?: boolean;
} }
type DataType = 'hex' | 'text' | 'address' | 'number'; type DataType = 'hex' | 'text' | 'address' | 'number';
...@@ -24,7 +25,7 @@ const VALUE_CONVERTERS: Record<DataType, (hex: string) => string> = { ...@@ -24,7 +25,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 LogTopic = ({ hex, index }: Props) => { const LogTopic = ({ hex, index, isLoading }: 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>) => {
...@@ -40,10 +41,10 @@ const LogTopic = ({ hex, index }: Props) => { ...@@ -40,10 +41,10 @@ const LogTopic = ({ hex, index }: Props) => {
case 'text': { case 'text': {
return ( return (
<> <>
<Box overflow="hidden" whiteSpace="nowrap"> <Skeleton isLoaded={ !isLoading } overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ value }/> <HashStringShortenDynamic hash={ value }/>
</Box> </Skeleton>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value } isLoading={ isLoading }/>
</> </>
); );
} }
...@@ -51,8 +52,8 @@ const LogTopic = ({ hex, index }: Props) => { ...@@ -51,8 +52,8 @@ const LogTopic = ({ hex, index }: Props) => {
case 'address': { case 'address': {
return ( return (
<Address> <Address>
<AddressLink type="address" hash={ value }/> <AddressLink type="address" hash={ value } isLoading={ isLoading }/>
<CopyToClipboard text={ value }/> <CopyToClipboard text={ value } isLoading={ isLoading }/>
</Address> </Address>
); );
} }
...@@ -61,22 +62,24 @@ const LogTopic = ({ hex, index }: Props) => { ...@@ -61,22 +62,24 @@ const LogTopic = ({ hex, index }: Props) => {
return ( return (
<Flex alignItems="center" px={{ base: 0, lg: 3 }} _notFirst={{ mt: 3 }} overflow="hidden" maxW="100%"> <Flex alignItems="center" px={{ base: 0, lg: 3 }} _notFirst={{ mt: 3 }} overflow="hidden" maxW="100%">
<Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }> <Skeleton isLoaded={ !isLoading } mr={ 3 } borderRadius="base">
{ index } <Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } w={ 6 }>
</Button> { index }
</Button>
</Skeleton>
{ index !== 0 && ( { index !== 0 && (
<Select <Skeleton isLoaded={ !isLoading } mr={ 3 } flexShrink={ 0 } borderRadius="base">
size="xs" <Select
borderRadius="base" size="xs"
value={ selectedDataType } borderRadius="base"
onChange={ handleSelectChange } value={ selectedDataType }
mr={ 3 } onChange={ handleSelectChange }
flexShrink={ 0 } w="auto"
w="auto" aria-label="Data type"
aria-label="Data type" >
> { OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) }
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ capitalize(option) }</option>) } </Select>
</Select> </Skeleton>
) } ) }
{ content } { content }
</Flex> </Flex>
......
...@@ -20,9 +20,10 @@ interface Props<Sort extends string> { ...@@ -20,9 +20,10 @@ interface Props<Sort extends string> {
options: Array<Option<Sort>>; options: Array<Option<Sort>>;
sort: Sort | undefined; sort: Sort | undefined;
setSort: (value: Sort | undefined) => void; setSort: (value: Sort | undefined) => void;
isLoading?: boolean;
} }
const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => { const Sort = <Sort extends string>({ sort, setSort, options, isLoading }: Props<Sort>) => {
const { isOpen, onToggle } = useDisclosure(); const { isOpen, onToggle } = useDisclosure();
const setSortingFromMenu = React.useCallback((val: string | Array<string>) => { const setSortingFromMenu = React.useCallback((val: string | Array<string>) => {
...@@ -36,6 +37,7 @@ const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => { ...@@ -36,6 +37,7 @@ const Sort = <Sort extends string>({ sort, setSort, options }: Props<Sort>) => {
<SortButton <SortButton
isActive={ isOpen || Boolean(sort) } isActive={ isOpen || Boolean(sort) }
onClick={ onToggle } onClick={ onToggle }
isLoading={ isLoading }
/> />
</MenuButton> </MenuButton>
<MenuList minWidth="240px" zIndex="popover"> <MenuList minWidth="240px" zIndex="popover">
......
import { Icon, IconButton, chakra } from '@chakra-ui/react'; import { Icon, IconButton, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg'; import upDownArrow from 'icons/arrows/up-down.svg';
...@@ -7,9 +7,14 @@ type Props = { ...@@ -7,9 +7,14 @@ type Props = {
onClick: () => void; onClick: () => void;
isActive: boolean; isActive: boolean;
className?: string; className?: string;
isLoading?: boolean;
} }
const SortButton = ({ onClick, isActive, className }: Props) => { const SortButton = ({ onClick, isActive, className, isLoading }: Props) => {
if (isLoading) {
return <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
}
return ( return (
<IconButton <IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> } icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
......
import { Flex, Icon, useColorModeValue, Skeleton } from '@chakra-ui/react'; import { Flex, useColorModeValue, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -11,6 +11,7 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol'; ...@@ -11,6 +11,7 @@ import trimTokenSymbol from 'lib/token/trimTokenSymbol';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
...@@ -45,14 +46,13 @@ const TokenTransferListItem = ({ ...@@ -45,14 +46,13 @@ const TokenTransferListItem = ({
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Flex> <Flex>
<Skeleton isLoaded={ !isLoading } boxSize="30px" mr={ 2 }> <Icon
<Icon as={ transactionIcon }
as={ transactionIcon } boxSize="30px"
boxSize="30px" color={ iconColor }
color={ iconColor } isLoading={ isLoading }
/> />
</Skeleton> <Address width="100%" ml={ 2 }>
<Address width="100%">
<AddressLink <AddressLink
hash={ txHash } hash={ txHash }
type="transaction" type="transaction"
...@@ -77,9 +77,7 @@ const TokenTransferListItem = ({ ...@@ -77,9 +77,7 @@ const TokenTransferListItem = ({
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash } isLoading={ isLoading }/> <CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 }> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
</Skeleton>
<Address width="50%"> <Address width="50%">
<AddressIcon address={ to } isLoading={ isLoading }/> <AddressIcon address={ to } isLoading={ isLoading }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/> <AddressLink ml={ 2 } fontWeight="500" hash={ to.hash } type="address_token" tokenHash={ token.address } isLoading={ isLoading }/>
......
import { Tr, Td, Icon, Grid, Skeleton, Box } from '@chakra-ui/react'; import { Tr, Td, Grid, Skeleton, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -9,6 +9,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -9,6 +9,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag'; import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
...@@ -69,9 +70,9 @@ const TokenTransferTableItem = ({ ...@@ -69,9 +70,9 @@ const TokenTransferTableItem = ({
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 }>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } my="3px"> <Box my="3px">
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
</Skeleton> </Box>
</Td> </Td>
<Td> <Td>
<Address display="inline-flex" maxW="100%" py="3px"> <Address display="inline-flex" maxW="100%" py="3px">
......
...@@ -3,7 +3,7 @@ import { ...@@ -3,7 +3,7 @@ import {
GridItem, GridItem,
Text, Text,
Box, Box,
Icon, Icon as ChakraIcon,
Link, Link,
Spinner, Spinner,
Tag, Tag,
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
Tooltip, Tooltip,
chakra, chakra,
useColorModeValue, useColorModeValue,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -28,6 +29,7 @@ import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; ...@@ -28,6 +29,7 @@ import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
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 Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -42,14 +44,13 @@ import TextSeparator from 'ui/shared/TextSeparator'; ...@@ -42,14 +44,13 @@ import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsActions from 'ui/tx/details/TxDetailsActions'; import TxDetailsActions from 'ui/tx/details/TxDetailsActions';
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 TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxDetails = () => { const TxDetails = () => {
const { data, isLoading, isError, socketStatus, error } = useFetchTxInfo(); const { data, isPlaceholderData, isError, socketStatus, error } = useFetchTxInfo();
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
...@@ -62,10 +63,6 @@ const TxDetails = () => { ...@@ -62,10 +63,6 @@ const TxDetails = () => {
}, []); }, []);
const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
if (isLoading) {
return <TxDetailsSkeleton/>;
}
if (isError) { if (isError) {
if (error?.status === 422) { if (error?.status === 422) {
throw Error('Invalid tx hash', { cause: error as unknown as Error }); throw Error('Invalid tx hash', { cause: error as unknown as Error });
...@@ -78,6 +75,10 @@ const TxDetails = () => { ...@@ -78,6 +75,10 @@ const TxDetails = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (!data) {
return null;
}
const addressFromTags = [ const addressFromTags = [
...data.from.private_tags || [], ...data.from.private_tags || [],
...data.from.public_tags || [], ...data.from.public_tags || [],
...@@ -96,14 +97,14 @@ const TxDetails = () => { ...@@ -96,14 +97,14 @@ const TxDetails = () => {
const executionSuccessBadge = toAddress?.is_contract && data.result === 'success' ? ( const executionSuccessBadge = toAddress?.is_contract && data.result === 'success' ? (
<Tooltip label="Contract execution completed"> <Tooltip label="Contract execution completed">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }> <chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ successIcon } boxSize={ 4 } color={ executionSuccessIconColor } cursor="pointer"/> <ChakraIcon as={ successIcon } boxSize={ 4 } color={ executionSuccessIconColor } cursor="pointer"/>
</chakra.span> </chakra.span>
</Tooltip> </Tooltip>
) : null; ) : null;
const executionFailedBadge = toAddress?.is_contract && Boolean(data.status) && data.result !== 'success' ? ( const executionFailedBadge = toAddress?.is_contract && Boolean(data.status) && data.result !== 'success' ? (
<Tooltip label="Error occurred during contract execution"> <Tooltip label="Error occurred during contract execution">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }> <chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ errorIcon } boxSize={ 4 } color="error" cursor="pointer"/> <ChakraIcon as={ errorIcon } boxSize={ 4 } color="error" cursor="pointer"/>
</chakra.span> </chakra.span>
</Tooltip> </Tooltip>
) : null; ) : null;
...@@ -129,20 +130,22 @@ const TxDetails = () => { ...@@ -129,20 +130,22 @@ const TxDetails = () => {
title="Transaction hash" title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction" hint="Unique character string (TxID) assigned to every verified transaction"
flexWrap="nowrap" flexWrap="nowrap"
isLoading={ isPlaceholderData }
> >
{ data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> } { data.status === null && <Spinner mr={ 2 } size="sm" flexShrink={ 0 }/> }
<Box overflow="hidden"> <Skeleton isLoaded={ !isPlaceholderData } overflow="hidden">
<HashStringShortenDynamic hash={ data.hash }/> <HashStringShortenDynamic hash={ data.hash }/>
</Box> </Skeleton>
<CopyToClipboard text={ data.hash }/> <CopyToClipboard text={ data.hash } isLoading={ isPlaceholderData }/>
{ /* api doesn't support navigation between certain address account tx */ } { /* api doesn't support navigation between certain address account tx */ }
{ /* <PrevNext ml={ 7 }/> */ } { /* <PrevNext ml={ 7 }/> */ }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Status" title="Status"
hint="Current transaction state: Success, Failed (Error), or Pending (In Process)" hint="Current transaction state: Success, Failed (Error), or Pending (In Process)"
isLoading={ isPlaceholderData }
> >
<TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined }/> <TxStatus status={ data.status } errorText={ data.status === 'error' ? data.result : undefined } isLoading={ isPlaceholderData }/>
</DetailsInfoItem> </DetailsInfoItem>
{ data.revert_reason && ( { data.revert_reason && (
<DetailsInfoItem <DetailsInfoItem
...@@ -155,16 +158,22 @@ const TxDetails = () => { ...@@ -155,16 +158,22 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Block" title="Block"
hint="Block number containing the transaction" hint="Block number containing the transaction"
isLoading={ isPlaceholderData }
> >
{ data.block === null ? { data.block === null ?
<Text>Pending</Text> : <Text>Pending</Text> : (
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.block) } }) }>{ data.block }</LinkInternal> } <Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: String(data.block) } }) }>
{ data.block }
</LinkInternal>
</Skeleton>
) }
{ Boolean(data.confirmations) && ( { Boolean(data.confirmations) && (
<> <>
<TextSeparator color="gray.500"/> <TextSeparator color="gray.500"/>
<Text variant="secondary"> <Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
{ data.confirmations } Block confirmations <span>{ data.confirmations } Block confirmations</span>
</Text> </Skeleton>
</> </>
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
...@@ -172,12 +181,16 @@ const TxDetails = () => { ...@@ -172,12 +181,16 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Timestamp" title="Timestamp"
hint="Date & time of transaction inclusion, including length of time for confirmation" hint="Date & time of transaction inclusion, including length of time for confirmation"
isLoading={ isPlaceholderData }
> >
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ clockIcon } boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Text ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Text> <Skeleton isLoaded={ !isPlaceholderData } ml={ 1 }>{ dayjs(data.timestamp).fromNow() }</Skeleton>
<TextSeparator/> <TextSeparator/>
<Text whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }<TextSeparator color="gray.500"/></Text> <Skeleton isLoaded={ !isPlaceholderData } whiteSpace="normal">{ dayjs(data.timestamp).format('LLLL') }</Skeleton>
<Text variant="secondary">{ getConfirmationDuration(data.confirmation_duration) }</Text> <TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
<span>{ getConfirmationDuration(data.confirmation_duration) }</span>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsSponsoredItem/> <DetailsSponsoredItem/>
...@@ -194,12 +207,13 @@ const TxDetails = () => { ...@@ -194,12 +207,13 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="From" title="From"
hint="Address (external or contract) sending the transaction" hint="Address (external or contract) sending the transaction"
isLoading={ isPlaceholderData }
columnGap={ 3 } columnGap={ 3 }
> >
<Address> <Address>
<AddressIcon address={ data.from }/> <AddressIcon address={ data.from } isLoading={ isPlaceholderData }/>
<AddressLink type="address" ml={ 2 } hash={ data.from.hash }/> <AddressLink type="address" ml={ 2 } hash={ data.from.hash } isLoading={ isPlaceholderData }/>
<CopyToClipboard text={ data.from.hash }/> <CopyToClipboard text={ data.from.hash } isLoading={ isPlaceholderData }/>
</Address> </Address>
{ data.from.name && <Text>{ data.from.name }</Text> } { data.from.name && <Text>{ data.from.name }</Text> }
{ addressFromTags.length > 0 && ( { addressFromTags.length > 0 && (
...@@ -211,6 +225,7 @@ const TxDetails = () => { ...@@ -211,6 +225,7 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title={ data.to?.is_contract ? 'Interacted with contract' : 'To' } title={ data.to?.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction" hint="Address (external or contract) receiving the transaction"
isLoading={ isPlaceholderData }
flexWrap={{ base: 'wrap', lg: 'nowrap' }} flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 } columnGap={ 3 }
> >
...@@ -218,11 +233,11 @@ const TxDetails = () => { ...@@ -218,11 +233,11 @@ const TxDetails = () => {
<> <>
{ data.to && data.to.hash ? ( { data.to && data.to.hash ? (
<Address alignItems="center"> <Address alignItems="center">
<AddressIcon address={ toAddress }/> <AddressIcon address={ toAddress } isLoading={ isPlaceholderData }/>
<AddressLink type="address" ml={ 2 } hash={ toAddress.hash }/> <AddressLink type="address" ml={ 2 } hash={ toAddress.hash } isLoading={ isPlaceholderData }/>
{ executionSuccessBadge } { executionSuccessBadge }
{ executionFailedBadge } { executionFailedBadge }
<CopyToClipboard text={ toAddress.hash }/> <CopyToClipboard text={ toAddress.hash } isLoading={ isPlaceholderData }/>
</Address> </Address>
) : ( ) : (
<Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre" alignItems="center"> <Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre" alignItems="center">
...@@ -252,48 +267,58 @@ const TxDetails = () => { ...@@ -252,48 +267,58 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="Value" title="Value"
hint="Value sent in the native token (and USD) if applicable" hint="Value sent in the native token (and USD) if applicable"
isLoading={ isPlaceholderData }
> >
<CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate }/> <CurrencyValue value={ data.value } currency={ appConfig.network.currency.symbol } exchangeRate={ data.exchange_rate } isLoading={ isPlaceholderData }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transaction fee" title="Transaction fee"
hint="Total transaction fee" hint="Total transaction fee"
isLoading={ isPlaceholderData }
> >
<CurrencyValue <CurrencyValue
value={ data.fee.value } value={ data.fee.value }
currency={ appConfig.network.currency.symbol } currency={ appConfig.network.currency.symbol }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
flexWrap="wrap" flexWrap="wrap"
isLoading={ isPlaceholderData }
/> />
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas price" title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage" hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage"
isLoading={ isPlaceholderData }
> >
<Text mr={ 1 }>{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !isPlaceholderData } mr={ 1 }>
<Text variant="secondary">({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text> { BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }
</Skeleton>
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
<span>({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</span>
</Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas usage & limit by txn" title="Gas usage & limit by txn"
hint="Actual gas amount used by the transaction" hint="Actual gas amount used by the transaction"
isLoading={ isPlaceholderData }
> >
<Text>{ BigNumber(data.gas_used || 0).toFormat() }</Text> <Skeleton isLoaded={ !isPlaceholderData }>{ BigNumber(data.gas_used || 0).toFormat() }</Skeleton>
<TextSeparator/> <TextSeparator/>
<Text >{ BigNumber(data.gas_limit).toFormat() }</Text> <Skeleton isLoaded={ !isPlaceholderData }>{ BigNumber(data.gas_limit).toFormat() }</Skeleton>
<Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() }/> <Utilization ml={ 4 } value={ BigNumber(data.gas_used || 0).dividedBy(BigNumber(data.gas_limit)).toNumber() } isLoading={ isPlaceholderData }/>
</DetailsInfoItem> </DetailsInfoItem>
{ (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && ( { (data.base_fee_per_gas || data.max_fee_per_gas || data.max_priority_fee_per_gas) && (
<DetailsInfoItem <DetailsInfoItem
title="Gas fees (Gwei)" title="Gas fees (Gwei)"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively" hint="Base Fee refers to the network Base Fee at the time of the block, while Max Fee & Max Priority Fee refer to the max amount a user is willing to pay for their tx & to give to the miner respectively"
isLoading={ isPlaceholderData }
> >
{ data.base_fee_per_gas && ( { data.base_fee_per_gas && (
<Box> <Skeleton isLoaded={ !isPlaceholderData }>
<Text as="span" fontWeight="500">Base: </Text> <Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text> <Text fontWeight="600" as="span">{ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() }</Text>
{ (data.max_fee_per_gas || data.max_priority_fee_per_gas) && <TextSeparator/> } { (data.max_fee_per_gas || data.max_priority_fee_per_gas) && <TextSeparator/> }
</Box> </Skeleton>
) } ) }
{ data.max_fee_per_gas && ( { data.max_fee_per_gas && (
<Box> <Box>
...@@ -330,6 +355,7 @@ const TxDetails = () => { ...@@ -330,6 +355,7 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="L1 gas used by txn" title="L1 gas used by txn"
hint="L1 gas used by transaction" hint="L1 gas used by transaction"
isLoading={ isPlaceholderData }
> >
<Text>{ BigNumber(data.l1_gas_used).toFormat() }</Text> <Text>{ BigNumber(data.l1_gas_used).toFormat() }</Text>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -338,6 +364,7 @@ const TxDetails = () => { ...@@ -338,6 +364,7 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="L1 gas price" title="L1 gas price"
hint="L1 gas price" hint="L1 gas price"
isLoading={ isPlaceholderData }
> >
<Text mr={ 1 }>{ BigNumber(data.l1_gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text> <Text mr={ 1 }>{ BigNumber(data.l1_gas_price).dividedBy(WEI).toFixed() } { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">({ BigNumber(data.l1_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text> <Text variant="secondary">({ BigNumber(data.l1_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</Text>
...@@ -348,6 +375,7 @@ const TxDetails = () => { ...@@ -348,6 +375,7 @@ const TxDetails = () => {
title="L1 fee" title="L1 fee"
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
hint={ `L1 Data Fee which is used to cover the L1 "security" cost from the batch submission mechanism. In combination with L2 execution fee, L1 fee makes the total amount of fees that a transaction pays.` } hint={ `L1 Data Fee which is used to cover the L1 "security" cost from the batch submission mechanism. In combination with L2 execution fee, L1 fee makes the total amount of fees that a transaction pays.` }
isLoading={ isPlaceholderData }
> >
<CurrencyValue <CurrencyValue
value={ data.l1_fee } value={ data.l1_fee }
...@@ -361,6 +389,7 @@ const TxDetails = () => { ...@@ -361,6 +389,7 @@ const TxDetails = () => {
<DetailsInfoItem <DetailsInfoItem
title="L1 fee scalar" title="L1 fee scalar"
hint="A Dynamic overhead (fee scalar) premium, which serves as a buffer in case L1 prices rapidly increase." hint="A Dynamic overhead (fee scalar) premium, which serves as a buffer in case L1 prices rapidly increase."
isLoading={ isPlaceholderData }
> >
<Text>{ data.l1_fee_scalar }</Text> <Text>{ data.l1_fee_scalar }</Text>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -369,16 +398,17 @@ const TxDetails = () => { ...@@ -369,16 +398,17 @@ const TxDetails = () => {
) } ) }
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="TxDetails__cutLink"> <Element name="TxDetails__cutLink">
<Link <Skeleton isLoaded={ !isPlaceholderData } mt={ 6 } display="inline-block">
mt={ 6 } <Link
display="inline-block" display="inline-block"
fontSize="sm" fontSize="sm"
textDecorationLine="underline" textDecorationLine="underline"
textDecorationStyle="dashed" textDecorationStyle="dashed"
onClick={ handleCutClick } onClick={ handleCutClick }
> >
{ isExpanded ? 'Hide details' : 'View details' } { isExpanded ? 'Hide details' : 'View details' }
</Link> </Link>
</Skeleton>
</Element> </Element>
</GridItem> </GridItem>
{ isExpanded && ( { isExpanded && (
......
...@@ -6,6 +6,8 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -6,6 +6,8 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
// import { apos } from 'lib/html-entities'; // import { apos } from 'lib/html-entities';
import { INTERNAL_TX } from 'stubs/internalTx';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
// import FilterInput from 'ui/shared/filters/FilterInput'; // import FilterInput from 'ui/shared/filters/FilterInput';
...@@ -70,11 +72,12 @@ const TxInternals = () => { ...@@ -70,11 +72,12 @@ const TxInternals = () => {
// const [ searchTerm, setSearchTerm ] = React.useState<string>(''); // const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_internal_txs', resourceName: 'tx_internal_txs',
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
placeholderData: generateListStub<'tx_internal_txs'>(INTERNAL_TX, 3),
}, },
}); });
...@@ -84,11 +87,14 @@ const TxInternals = () => { ...@@ -84,11 +87,14 @@ const TxInternals = () => {
const handleSortToggle = React.useCallback((field: SortField) => { const handleSortToggle = React.useCallback((field: SortField) => {
return () => { return () => {
if (isPlaceholderData) {
return;
}
setSort(getNextSortValue(field)); setSort(getNextSortValue(field));
}; };
}, []); }, [ isPlaceholderData ]);
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data?.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
} }
...@@ -100,9 +106,15 @@ const TxInternals = () => { ...@@ -100,9 +106,15 @@ const TxInternals = () => {
const content = filteredData ? ( const content = filteredData ? (
<> <>
<Show below="lg" ssr={ false }><TxInternalsList data={ filteredData }/></Show> <Show below="lg" ssr={ false }><TxInternalsList data={ filteredData } isLoading={ isPlaceholderData }/></Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginationVisible ? 80 : 0 }/> <TxInternalsTable
data={ filteredData }
sort={ sort }
onSortToggle={ handleSortToggle }
top={ isPaginationVisible ? 80 : 0 }
isLoading={ isPlaceholderData }
/>
</Hide> </Hide>
</> </>
) : null; ) : null;
...@@ -118,7 +130,7 @@ const TxInternals = () => { ...@@ -118,7 +130,7 @@ const TxInternals = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError || txInfo.isError } isError={ isError || txInfo.isError }
isLoading={ isLoading || txInfo.isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonDesktopColumns: [ '28%', '20%', '24px', '20%', '16%', '16%' ] }} skeletonProps={{ skeletonDesktopColumns: [ '28%', '20%', '24px', '20%', '16%', '16%' ] }}
emptyText="There are no internal transactions for this transaction." emptyText="There are no internal transactions for this transaction."
......
...@@ -3,10 +3,11 @@ import React from 'react'; ...@@ -3,10 +3,11 @@ import React from 'react';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { LOG } from 'stubs/log';
import { generateListStub } from 'stubs/utils';
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 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 TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
...@@ -14,15 +15,16 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; ...@@ -14,15 +15,16 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => { const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isPlaceholderData, isError, pagination, isPaginationVisible } = useQueryWithPages({
resourceName: 'tx_logs', resourceName: 'tx_logs',
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: !txInfo.isPlaceholderData && Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
placeholderData: generateListStub<'tx_logs'>(LOG, 3),
}, },
}); });
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
} }
...@@ -30,16 +32,7 @@ const TxLogs = () => { ...@@ -30,16 +32,7 @@ const TxLogs = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading || txInfo.isLoading) { if (!data?.items.length) {
return (
<Box>
<LogSkeleton/>
<LogSkeleton/>
</Box>
);
}
if (data.items.length === 0) {
return <Text as="span">There are no logs for this transaction.</Text>; return <Text as="span">There are no logs for this transaction.</Text>;
} }
...@@ -50,7 +43,7 @@ const TxLogs = () => { ...@@ -50,7 +43,7 @@ const TxLogs = () => {
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
) } ) }
{ data.items.map((item, index) => <LogItem key={ index } { ...item } type="transaction"/>) } { data?.items.map((item, index) => <LogItem key={ index } { ...item } type="transaction" isLoading={ isPlaceholderData }/>) }
</Box> </Box>
); );
}; };
......
import { Flex, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -10,6 +9,7 @@ import { SECOND } from 'lib/consts'; ...@@ -10,6 +9,7 @@ import { SECOND } from 'lib/consts';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX_RAW_TRACE } from 'stubs/tx';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -23,10 +23,11 @@ const TxRawTrace = () => { ...@@ -23,10 +23,11 @@ const TxRawTrace = () => {
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', { const { data, isPlaceholderData, isError } = useApiQuery('tx_raw_trace', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { queryOptions: {
enabled: Boolean(hash) && Boolean(txInfo.data?.status) && isSocketOpen, enabled: Boolean(hash) && Boolean(txInfo.data?.status) && isSocketOpen,
placeholderData: TX_RAW_TRACE,
}, },
}); });
...@@ -36,7 +37,7 @@ const TxRawTrace = () => { ...@@ -36,7 +37,7 @@ const TxRawTrace = () => {
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `transactions:${ hash }`, topic: `transactions:${ hash }`,
isDisabled: !hash || !txInfo.data?.status, isDisabled: !hash || !txInfo.isPlaceholderData || !txInfo.data?.status,
onJoin: () => setIsSocketOpen(true), onJoin: () => setIsSocketOpen(true),
}); });
useSocketMessage({ useSocketMessage({
...@@ -45,7 +46,7 @@ const TxRawTrace = () => { ...@@ -45,7 +46,7 @@ const TxRawTrace = () => {
handler: handleRawTraceMessage, handler: handleRawTraceMessage,
}); });
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
} }
...@@ -53,26 +54,15 @@ const TxRawTrace = () => { ...@@ -53,26 +54,15 @@ const TxRawTrace = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (isLoading || txInfo.isLoading) {
return (
<>
<Flex justifyContent="end" mb={ 2 }>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="500px"/>
</>
);
}
const dataToDisplay = rawTraces ? rawTraces : data; const dataToDisplay = rawTraces ? rawTraces : data;
if (dataToDisplay.length === 0) { if (!isPlaceholderData && dataToDisplay?.length === 0) {
return <span>No trace entries found.</span>; return <span>No trace entries found.</span>;
} }
const text = JSON.stringify(dataToDisplay, undefined, 4); const text = JSON.stringify(dataToDisplay, undefined, 4);
return <RawDataSnippet data={ text }/>; return <RawDataSnippet data={ text } isLoading={ isPlaceholderData }/>;
}; };
export default TxRawTrace; export default TxRawTrace;
import { Accordion, Hide, Show, Skeleton, Text } from '@chakra-ui/react'; import { Accordion, Hide, Show, Text } 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 { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import { TX_STATE_CHANGES } from 'stubs/txStateChanges';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TxStateList from 'ui/tx/state/TxStateList'; import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTable from 'ui/tx/state/TxStateTable'; import TxStateTable from 'ui/tx/state/TxStateTable';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
...@@ -15,59 +14,44 @@ import TxSocketAlert from './TxSocketAlert'; ...@@ -15,59 +14,44 @@ import TxSocketAlert from './TxSocketAlert';
const TxState = () => { const TxState = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useApiQuery('tx_state_changes', { const { data, isPlaceholderData, isError } = useApiQuery('tx_state_changes', {
pathParams: { hash: txInfo.data?.hash }, pathParams: { hash: txInfo.data?.hash },
queryOptions: { queryOptions: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
placeholderData: TX_STATE_CHANGES,
}, },
}); });
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isPlaceholderData && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
} }
const skeleton = ( const content = data ? (
<> <Accordion allowMultiple defaultIndex={ [] }>
<Show below="lg" ssr={ false }>
<Skeleton h={ 4 } borderRadius="full" w="100%"/>
<Skeleton h={ 4 } borderRadius="full" w="100%" mt={ 2 }/>
<Skeleton h={ 4 } borderRadius="full" w="100%" mt={ 2 }/>
<Skeleton h={ 4 } borderRadius="full" w="50%" mt={ 2 } mb={ 6 }/>
<SkeletonList/>
</Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Skeleton h={ 6 } borderRadius="full" w="90%" mb={ 6 }/> <TxStateTable data={ data } isLoading={ isPlaceholderData }/>
<SkeletonTable columns={ [ '140px', '146px', '33%', '33%', '33%', '150px' ] }/>
</Hide> </Hide>
</> <Show below="lg" ssr={ false }>
); <TxStateList data={ data } isLoading={ isPlaceholderData }/>
</Show>
</Accordion>
) : null;
const content = data ? ( return (
<> <>
<Text> <Text mb={ 6 }>
A set of information that represents the current state is updated when a transaction takes place on the network. A set of information that represents the current state is updated when a transaction takes place on the network.
The below is a summary of those changes. The below is a summary of those changes.
</Text> </Text>
<Accordion allowMultiple defaultIndex={ [] }> <DataListDisplay
<Hide below="lg" ssr={ false }> isError={ isError }
<TxStateTable data={ data }/> isLoading={ false }
</Hide> items={ data }
<Show below="lg" ssr={ false }> emptyText="There are no state changes for this transaction."
<TxStateList data={ data }/> content={ content }
</Show> skeletonProps={{ customSkeleton: null }}
</Accordion> />
</> </>
) : null;
return (
<DataListDisplay
isError={ isError }
isLoading={ isLoading }
items={ data }
emptyText="There are no state changes for this transaction."
content={ content }
skeletonProps={{ customSkeleton: skeleton }}
/>
); );
}; };
......
...@@ -9,6 +9,7 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; ...@@ -9,6 +9,7 @@ import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import TOKEN_TYPE from 'lib/token/tokenTypes'; import TOKEN_TYPE from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token';
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 DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
...@@ -34,7 +35,10 @@ const TxTokenTransfer = () => { ...@@ -34,7 +35,10 @@ const TxTokenTransfer = () => {
const tokenTransferQuery = useQueryWithPages({ const tokenTransferQuery = useQueryWithPages({
resourceName: 'tx_token_transfers', resourceName: 'tx_token_transfers',
pathParams: { hash: txsInfo.data?.hash.toString() }, pathParams: { hash: txsInfo.data?.hash.toString() },
options: { enabled: Boolean(txsInfo.data?.status && txsInfo.data?.hash) }, options: {
enabled: !txsInfo.isPlaceholderData && Boolean(txsInfo.data?.status && txsInfo.data?.hash),
placeholderData: getTokenTransfersStub(),
},
filters: { type: typeFilter }, filters: { type: typeFilter },
}); });
...@@ -43,7 +47,7 @@ const TxTokenTransfer = () => { ...@@ -43,7 +47,7 @@ const TxTokenTransfer = () => {
setTypeFilter(nextValue); setTypeFilter(nextValue);
}, [ tokenTransferQuery ]); }, [ tokenTransferQuery ]);
if (!txsInfo.isLoading && !txsInfo.isError && !txsInfo.data.status) { if (!txsInfo.isLoading && !txsInfo.isPlaceholderData && !txsInfo.isError && !txsInfo.data.status) {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>; return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
} }
...@@ -57,10 +61,10 @@ const TxTokenTransfer = () => { ...@@ -57,10 +61,10 @@ const TxTokenTransfer = () => {
const content = tokenTransferQuery.data?.items ? ( const content = tokenTransferQuery.data?.items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TokenTransferTable data={ tokenTransferQuery.data?.items } top={ isActionBarHidden ? 0 : 80 }/> <TokenTransferTable data={ tokenTransferQuery.data?.items } top={ isActionBarHidden ? 0 : 80 } isLoading={ tokenTransferQuery.isPlaceholderData }/>
</Hide> </Hide>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<TokenTransferList data={ tokenTransferQuery.data?.items }/> <TokenTransferList data={ tokenTransferQuery.data?.items } isLoading={ tokenTransferQuery.isPlaceholderData }/>
</Show> </Show>
</> </>
) : null; ) : null;
...@@ -71,6 +75,7 @@ const TxTokenTransfer = () => { ...@@ -71,6 +75,7 @@ const TxTokenTransfer = () => {
defaultTypeFilters={ typeFilter } defaultTypeFilters={ typeFilter }
onTypeFilterChange={ handleTypeFilterChange } onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters } appliedFiltersNum={ numActiveFilters }
isLoading={ tokenTransferQuery.isPlaceholderData }
/> />
{ tokenTransferQuery.isPaginationVisible && <Pagination ml="auto" { ...tokenTransferQuery.pagination }/> } { tokenTransferQuery.isPaginationVisible && <Pagination ml="auto" { ...tokenTransferQuery.pagination }/> }
</ActionBar> </ActionBar>
...@@ -79,7 +84,7 @@ const TxTokenTransfer = () => { ...@@ -79,7 +84,7 @@ const TxTokenTransfer = () => {
return ( return (
<DataListDisplay <DataListDisplay
isError={ txsInfo.isError || tokenTransferQuery.isError } isError={ txsInfo.isError || tokenTransferQuery.isError }
isLoading={ txsInfo.isLoading || tokenTransferQuery.isLoading } isLoading={ false }
items={ tokenTransferQuery.data?.items } items={ tokenTransferQuery.data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: true, isLongSkeleton: true,
......
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const TxDetailsSkeleton = () => {
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px" pt={{ base: 1, lg: 2 }}>
<DetailsSkeletonRow/>
<DetailsSkeletonRow w="20%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow/>
<DetailsSkeletonRow w="70%"/>
<DetailsSkeletonRow w="70%"/>
{ sectionGap }
<DetailsSkeletonRow w="40%"/>
<DetailsSkeletonRow w="40%"/>
<DetailsSkeletonRow w="40%"/>
<DetailsSkeletonRow w="40%"/>
<DetailsSkeletonRow w="40%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default TxDetailsSkeleton;
...@@ -5,10 +5,10 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -5,10 +5,10 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem'; import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => { const TxInternalsList = ({ data, isLoading }: { data: Array<InternalTransaction>; isLoading?: boolean }) => {
return ( return (
<Box> <Box>
{ data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) } { data.map((item, index) => <TxInternalsListItem key={ item.transaction_hash + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>) }
</Box> </Box>
); );
}; };
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react'; import { Flex, Box, HStack, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -9,47 +9,49 @@ import eastArrowIcon from 'icons/arrows/east.svg'; ...@@ -9,47 +9,49 @@ import eastArrowIcon from 'icons/arrows/east.svg';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction; type Props = InternalTransaction & { isLoading?: boolean };
const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => { const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract, isLoading }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract; const toData = to ? to : createdContract;
return ( return (
<ListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex> <Flex columnGap={ 2 }>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> } { typeTitle && <Tag colorScheme="cyan" isLoading={ isLoading }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex> </Flex>
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash }/> <CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
{ toData && ( { toData && (
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
<AddressIcon address={ toData }/> <AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ toData.hash } isLoading={ isLoading }/>
<CopyToClipboard text={ toData.hash }/> <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/>
</Address> </Address>
) } ) }
</Box> </Box>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Skeleton>
<Text fontSize="sm" variant="secondary"> <Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text> </Skeleton>
</HStack> </HStack>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" fontWeight={ 500 }>Gas limit</Skeleton>
<Text fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Skeleton>
</HStack> </HStack>
</ListItemMobile> </ListItemMobile>
); );
......
...@@ -14,9 +14,10 @@ interface Props { ...@@ -14,9 +14,10 @@ interface Props {
sort: Sort | undefined; sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void; onSortToggle: (field: SortField) => () => void;
top: number; top: number;
isLoading?: boolean;
} }
const TxInternalsTable = ({ data, sort, onSortToggle, top }: Props) => { const TxInternalsTable = ({ data, sort, onSortToggle, top, isLoading }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
...@@ -42,8 +43,8 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top }: Props) => { ...@@ -42,8 +43,8 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<TxInternalsTableItem key={ item.transaction_hash } { ...item }/> <TxInternalsTableItem key={ item.transaction_hash + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Icon, Box, Flex } from '@chakra-ui/react'; import { Tr, Td, Box, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -9,13 +9,17 @@ import rightArrowIcon from 'icons/arrows/east.svg'; ...@@ -9,13 +9,17 @@ import rightArrowIcon from 'icons/arrows/east.svg';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction type Props = InternalTransaction & {
isLoading?: boolean;
}
const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract }: Props) => { const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: gasLimit, created_contract: createdContract, isLoading }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract; const toData = to ? to : createdContract;
...@@ -25,36 +29,40 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -25,36 +29,40 @@ const TxInternalTableItem = ({ type, from, to, value, success, error, gas_limit:
<Flex rowGap={ 2 } flexWrap="wrap"> <Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && ( { typeTitle && (
<Box w="126px" display="inline-block"> <Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag> <Tag colorScheme="cyan" mr={ 5 } isLoading={ isLoading }>{ typeTitle }</Tag>
</Box> </Box>
) } ) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error } isLoading={ isLoading }/>
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ from }/> <AddressIcon address={ from } isLoading={ isLoading }/>
<AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/> <AddressLink type="address" ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 } isLoading={ isLoading }/>
<CopyToClipboard text={ from.hash }/> <CopyToClipboard text={ from.hash } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 } verticalAlign="middle"> <Td px={ 0 } verticalAlign="middle">
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
{ toData && ( { toData && (
<Address display="inline-flex" maxW="100%"> <Address display="inline-flex" maxW="100%">
<AddressIcon address={ toData }/> <AddressIcon address={ toData } isLoading={ isLoading }/>
<AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/> <AddressLink type="address" hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ toData.hash }/> <CopyToClipboard text={ toData.hash } isLoading={ isLoading }/>
</Address> </Address>
) } ) }
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Skeleton>
</Td> </Td>
<Td isNumeric verticalAlign="middle"> <Td isNumeric verticalAlign="middle">
{ BigNumber(gasLimit).toFormat() } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ BigNumber(gasLimit).toFormat() }
</Skeleton>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -7,12 +7,13 @@ import TxStateListItem from 'ui/tx/state/TxStateListItem'; ...@@ -7,12 +7,13 @@ import TxStateListItem from 'ui/tx/state/TxStateListItem';
interface Props { interface Props {
data: TxStateChanges; data: TxStateChanges;
isLoading?: boolean;
} }
const TxStateList = ({ data }: Props) => { const TxStateList = ({ data, isLoading }: Props) => {
return ( return (
<Box mt={ 6 }> <Box>
{ data.map((item, index) => <TxStateListItem key={ index } data={ item }/>) } { data.map((item, index) => <TxStateListItem key={ index } data={ item } isLoading={ isLoading }/>) }
</Box> </Box>
); );
}; };
......
...@@ -11,48 +11,49 @@ import { getStateElements } from './utils'; ...@@ -11,48 +11,49 @@ import { getStateElements } from './utils';
interface Props { interface Props {
data: TxStateChange; data: TxStateChange;
isLoading?: boolean;
} }
const TxStateListItem = ({ data }: Props) => { const TxStateListItem = ({ data, isLoading }: Props) => {
const { before, after, change, tag, tokenId } = getStateElements(data); const { before, after, change, tag, tokenId } = getStateElements(data, isLoading);
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<Address flexGrow={ 1 } w="100%" alignSelf="center"> <Address flexGrow={ 1 } w="100%" alignSelf="center">
<AddressIcon address={ data.address }/> <AddressIcon address={ data.address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ data.address.hash } ml={ 2 } truncation="constant" mr={ 3 }/> <AddressLink type="address" hash={ data.address.hash } ml={ 2 } truncation="constant" mr={ 3 } isLoading={ isLoading }/>
{ tag } { tag }
</Address> </Address>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ before && ( { before && (
<> <>
<ListItemMobileGrid.Label>Before</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Before</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ before }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>{ before }</ListItemMobileGrid.Value>
</> </>
) } ) }
{ after && ( { after && (
<> <>
<ListItemMobileGrid.Label>After</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>After</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ after }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>{ after }</ListItemMobileGrid.Value>
</> </>
) } ) }
{ change && ( { change && (
<> <>
<ListItemMobileGrid.Label>Change</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Change</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>{ change }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value>{ change }</ListItemMobileGrid.Value>
</> </>
) } ) }
{ tokenId && ( { tokenId && (
<> <>
<ListItemMobileGrid.Label>Token ID</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Token ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="0">{ tokenId }</ListItemMobileGrid.Value> <ListItemMobileGrid.Value py="0">{ tokenId }</ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -13,11 +13,12 @@ import TxStateTableItem from 'ui/tx/state/TxStateTableItem'; ...@@ -13,11 +13,12 @@ import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
interface Props { interface Props {
data: TxStateChanges; data: TxStateChanges;
isLoading?: boolean;
} }
const TxStateTable = ({ data }: Props) => { const TxStateTable = ({ data, isLoading }: Props) => {
return ( return (
<Table variant="simple" minWidth="1000px" size="sm" w="auto" mt={ 6 }> <Table variant="simple" minWidth="1000px" size="sm" w="auto">
<Thead top={ 0 }> <Thead top={ 0 }>
<Tr> <Tr>
<Th width="140px">Type</Th> <Th width="140px">Type</Th>
...@@ -29,7 +30,7 @@ const TxStateTable = ({ data }: Props) => { ...@@ -29,7 +30,7 @@ const TxStateTable = ({ data }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item, index) => <TxStateTableItem data={ item } key={ index }/>) } { data.map((item, index) => <TxStateTableItem data={ item } key={ index } isLoading={ isLoading }/>) }
</Tbody> </Tbody>
</Table> </Table>
); );
......
import { Tr, Td } from '@chakra-ui/react'; import { Tr, Td, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TxStateChange } from 'types/api/txStateChanges'; import type { TxStateChange } from 'types/api/txStateChanges';
...@@ -11,26 +11,37 @@ import { getStateElements } from './utils'; ...@@ -11,26 +11,37 @@ import { getStateElements } from './utils';
interface Props { interface Props {
data: TxStateChange; data: TxStateChange;
isLoading?: boolean;
} }
const TxStateTableItem = ({ data }: Props) => { const TxStateTableItem = ({ data, isLoading }: Props) => {
const { before, after, change, tag, tokenId } = getStateElements(data); const { before, after, change, tag, tokenId } = getStateElements(data, isLoading);
return ( return (
<Tr> <Tr>
<Td lineHeight="30px"> <Td>
{ tag } <Box py="3px">
{ tag }
</Box>
</Td> </Td>
<Td> <Td>
<Address height="30px"> <Address py="3px">
<AddressIcon address={ data.address }/> <AddressIcon address={ data.address } isLoading={ isLoading }/>
<AddressLink type="address" hash={ data.address.hash } alias={ data.address.name } fontWeight="500" truncation="constant" ml={ 2 }/> <AddressLink
type="address"
hash={ data.address.hash }
alias={ data.address.name }
fontWeight="500"
truncation="constant"
ml={ 2 }
isLoading={ isLoading }
/>
</Address> </Address>
</Td> </Td>
<Td isNumeric lineHeight="30px">{ before }</Td> <Td isNumeric><Box py="7px">{ before }</Box></Td>
<Td isNumeric lineHeight="30px">{ after }</Td> <Td isNumeric><Box py="7px">{ after }</Box></Td>
<Td isNumeric lineHeight="30px"> { change } </Td> <Td isNumeric><Box py="7px">{ change }</Box></Td>
<Td lineHeight="30px">{ tokenId }</Td> <Td>{ tokenId }</Td>
</Tr> </Tr>
); );
}; };
......
...@@ -8,9 +8,10 @@ import type { TxStateChangeNftItemFlatten } from './utils'; ...@@ -8,9 +8,10 @@ import type { TxStateChangeNftItemFlatten } from './utils';
interface Props { interface Props {
items: Array<TxStateChangeNftItemFlatten>; items: Array<TxStateChangeNftItemFlatten>;
tokenAddress: string; tokenAddress: string;
isLoading?: boolean;
} }
const TxStateTokenIdList = ({ items, tokenAddress }: Props) => { const TxStateTokenIdList = ({ items, tokenAddress, isLoading }: Props) => {
const [ isCut, setIsCut ] = useBoolean(true); const [ isCut, setIsCut ] = useBoolean(true);
return ( return (
...@@ -22,6 +23,7 @@ const TxStateTokenIdList = ({ items, tokenAddress }: Props) => { ...@@ -22,6 +23,7 @@ const TxStateTokenIdList = ({ items, tokenAddress }: Props) => {
id={ item.total.token_id } id={ item.total.token_id }
w="auto" w="auto"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
)) } )) }
{ items.length > 3 && ( { items.length > 3 && (
......
import { Box, Flex, Tag, Tooltip } from '@chakra-ui/react'; import { Flex, Skeleton, Tooltip } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -7,19 +7,22 @@ import type { ArrayElement } from 'types/utils'; ...@@ -7,19 +7,22 @@ import type { ArrayElement } from 'types/utils';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { ZERO_ADDRESS } from 'lib/consts'; import { ZERO_ADDRESS } from 'lib/consts';
import { nbsp } from 'lib/html-entities'; import { nbsp, space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import trimTokenSymbol from 'lib/token/trimTokenSymbol'; import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Tag from 'ui/shared/chakra/Tag';
import TxStateTokenIdList from './TxStateTokenIdList'; import TxStateTokenIdList from './TxStateTokenIdList';
export function getStateElements(data: TxStateChange) { export function getStateElements(data: TxStateChange, isLoading?: boolean) {
const tag = (() => { const tag = (() => {
if (data.is_miner) { if (data.is_miner) {
return ( return (
<Tooltip label="A block producer who successfully included the block into the blockchain"> <Tooltip label="A block producer who successfully included the block into the blockchain">
<Tag textTransform="capitalize" colorScheme="yellow">{ getNetworkValidatorTitle() }</Tag> <Tag textTransform="capitalize" colorScheme="yellow" isLoading={ isLoading }>
{ getNetworkValidatorTitle() }
</Tag>
</Tooltip> </Tooltip>
); );
} }
...@@ -37,7 +40,7 @@ export function getStateElements(data: TxStateChange) { ...@@ -37,7 +40,7 @@ export function getStateElements(data: TxStateChange) {
const text = changeDirection === 'from' ? 'Mint' : 'Burn'; const text = changeDirection === 'from' ? 'Mint' : 'Burn';
return ( return (
<Tooltip label="Address used in tokens mintings and burnings"> <Tooltip label="Address used in tokens mintings and burnings">
<Tag textTransform="capitalize" colorScheme="yellow">{ text } address</Tag> <Tag textTransform="capitalize" colorScheme="yellow" isLoading={ isLoading }>{ text } address</Tag>
</Tooltip> </Tooltip>
); );
} }
...@@ -55,14 +58,25 @@ export function getStateElements(data: TxStateChange) { ...@@ -55,14 +58,25 @@ export function getStateElements(data: TxStateChange) {
const changeSign = beforeBn.lte(afterBn) ? '+' : '-'; const changeSign = beforeBn.lte(afterBn) ? '+' : '-';
return { return {
before: <Box>{ beforeBn.toFormat() } { appConfig.network.currency.symbol }</Box>, before: <Skeleton isLoaded={ !isLoading } display="inline-block">{ beforeBn.toFormat() } { appConfig.network.currency.symbol }</Skeleton>,
after: <Box>{ afterBn.toFormat() } { appConfig.network.currency.symbol }</Box>, after: <Skeleton isLoaded={ !isLoading } display="inline-block">{ afterBn.toFormat() } { appConfig.network.currency.symbol }</Skeleton>,
change: <Box color={ changeColor }>{ changeSign }{ nbsp }{ differenceBn.abs().toFormat() }</Box>, change: (
<Skeleton isLoaded={ !isLoading } display="inline-block" color={ changeColor }>
<span>{ changeSign }{ nbsp }{ differenceBn.abs().toFormat() }</span>
</Skeleton>
),
tag, tag,
}; };
} }
case 'token': { case 'token': {
const tokenLink = <AddressLink type="token" hash={ data.token.address } alias={ trimTokenSymbol(data.token?.symbol || data.token.address) }/>; const tokenLink = (
<AddressLink
type="token"
hash={ data.token.address }
alias={ trimTokenSymbol(data.token?.symbol || data.token.address) }
isLoading={ isLoading }
/>
);
const before = Number(data.balance_before); const before = Number(data.balance_before);
const after = Number(data.balance_after); const after = Number(data.balance_after);
const change = (() => { const change = (() => {
...@@ -75,7 +89,11 @@ export function getStateElements(data: TxStateChange) { ...@@ -75,7 +89,11 @@ export function getStateElements(data: TxStateChange) {
const changeColor = difference >= 0 ? 'green.500' : 'red.500'; const changeColor = difference >= 0 ? 'green.500' : 'red.500';
const changeSign = difference >= 0 ? '+' : '-'; const changeSign = difference >= 0 ? '+' : '-';
return <Box color={ changeColor }>{ changeSign }{ nbsp }{ Math.abs(difference).toLocaleString() }</Box>; return (
<Skeleton isLoaded={ !isLoading } display="inline-block" color={ changeColor }>
<span>{ changeSign }{ nbsp }{ Math.abs(difference).toLocaleString() }</span>
</Skeleton>
);
})(); })();
const tokenId = (() => { const tokenId = (() => {
...@@ -84,19 +102,21 @@ export function getStateElements(data: TxStateChange) { ...@@ -84,19 +102,21 @@ export function getStateElements(data: TxStateChange) {
} }
const items = (data.change as Array<TxStateChangeNftItem>).reduce(flattenTotal, []); const items = (data.change as Array<TxStateChangeNftItem>).reduce(flattenTotal, []);
return <TxStateTokenIdList items={ items } tokenAddress={ data.token.address }/>; return <TxStateTokenIdList items={ items } tokenAddress={ data.token.address } isLoading={ isLoading }/>;
})(); })();
return { return {
before: data.balance_before ? ( before: data.balance_before ? (
<Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}> <Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}>
<span>{ before.toLocaleString() } </span> <Skeleton isLoaded={ !isLoading }>{ before.toLocaleString() }</Skeleton>
<span>{ space }</span>
{ tokenLink } { tokenLink }
</Flex> </Flex>
) : null, ) : null,
after: data.balance_after ? ( after: data.balance_after ? (
<Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}> <Flex whiteSpace="pre-wrap" justifyContent={{ base: 'flex-start', lg: 'flex-end' }}>
<span>{ after.toLocaleString() } </span> <Skeleton isLoaded={ !isLoading }>{ after.toLocaleString() }</Skeleton>
<span>{ space }</span>
{ tokenLink } { tokenLink }
</Flex> </Flex>
) : null, ) : null,
......
...@@ -12,6 +12,7 @@ import delay from 'lib/delay'; ...@@ -12,6 +12,7 @@ import delay from 'lib/delay';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
interface Params { interface Params {
onTxStatusUpdate?: () => void; onTxStatusUpdate?: () => void;
...@@ -33,6 +34,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params ...@@ -33,6 +34,7 @@ export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params
queryOptions: { queryOptions: {
enabled: Boolean(hash), enabled: Boolean(hash),
refetchOnMount: false, refetchOnMount: false,
placeholderData: TX,
}, },
}); });
const { data, isError, isLoading } = queryResult; const { data, isError, isLoading } = queryResult;
......
...@@ -27,9 +27,10 @@ type Props = ...@@ -27,9 +27,10 @@ type Props =
tx: Transaction; tx: Transaction;
}) & { }) & {
isMobile?: boolean; isMobile?: boolean;
isLoading?: boolean;
} }
const TxAdditionalInfo = ({ hash, tx, isMobile }: Props) => { const TxAdditionalInfo = ({ hash, tx, isMobile, isLoading }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const content = hash !== undefined ? <TxAdditionalInfoContainer hash={ hash }/> : <TxAdditionalInfoContent tx={ tx }/>; const content = hash !== undefined ? <TxAdditionalInfoContainer hash={ hash }/> : <TxAdditionalInfoContent tx={ tx }/>;
...@@ -37,7 +38,7 @@ const TxAdditionalInfo = ({ hash, tx, isMobile }: Props) => { ...@@ -37,7 +38,7 @@ const TxAdditionalInfo = ({ hash, tx, isMobile }: Props) => {
if (isMobile) { if (isMobile) {
return ( return (
<> <>
<AdditionalInfoButton onClick={ onOpen }/> <AdditionalInfoButton onClick={ onOpen } isLoading={ isLoading }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full"> <Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent paddingTop={ 4 }> <ModalContent paddingTop={ 4 }>
<ModalCloseButton/> <ModalCloseButton/>
...@@ -52,7 +53,7 @@ const TxAdditionalInfo = ({ hash, tx, isMobile }: Props) => { ...@@ -52,7 +53,7 @@ const TxAdditionalInfo = ({ hash, tx, isMobile }: Props) => {
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<AdditionalInfoButton isOpen={ isOpen }/> <AdditionalInfoButton isOpen={ isOpen } isLoading={ isLoading }/>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent border="1px solid" borderColor="divider"> <PopoverContent border="1px solid" borderColor="divider">
<PopoverBody> <PopoverBody>
......
import { Tag } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TransactionType } from 'types/api/transaction'; import type { TransactionType } from 'types/api/transaction';
import Tag from 'ui/shared/chakra/Tag';
export interface Props { export interface Props {
types: Array<TransactionType>; types: Array<TransactionType>;
isLoading?: boolean;
} }
const TYPES_ORDER = [ 'token_creation', 'contract_creation', 'token_transfer', 'contract_call', 'coin_transfer' ]; const TYPES_ORDER = [ 'token_creation', 'contract_creation', 'token_transfer', 'contract_call', 'coin_transfer' ];
const TxType = ({ types }: Props) => { const TxType = ({ types, isLoading }: Props) => {
const typeToShow = types.sort((t1, t2) => TYPES_ORDER.indexOf(t1) - TYPES_ORDER.indexOf(t2))[0]; const typeToShow = types.sort((t1, t2) => TYPES_ORDER.indexOf(t1) - TYPES_ORDER.indexOf(t2))[0];
let label; let label;
...@@ -43,7 +45,7 @@ const TxType = ({ types }: Props) => { ...@@ -43,7 +45,7 @@ const TxType = ({ types }: Props) => {
} }
return ( return (
<Tag colorScheme={ colorScheme }> <Tag colorScheme={ colorScheme } isLoading={ isLoading }>
{ label } { label }
</Tag> </Tag>
); );
......
...@@ -8,7 +8,7 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -8,7 +8,7 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import AddressCsvExportLink from 'ui/address/AddressCsvExportLink'; import AddressCsvExportLink from 'ui/address/AddressCsvExportLink';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsHeaderMobile from './TxsHeaderMobile'; import TxsHeaderMobile from './TxsHeaderMobile';
import TxsListItem from './TxsListItem'; import TxsListItem from './TxsListItem';
...@@ -45,7 +45,7 @@ const TxsContent = ({ ...@@ -45,7 +45,7 @@ const TxsContent = ({
hasLongSkeleton, hasLongSkeleton,
top, top,
}: Props) => { }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isPlaceholderData, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const content = data?.items ? ( const content = data?.items ? (
...@@ -53,22 +53,21 @@ const TxsContent = ({ ...@@ -53,22 +53,21 @@ const TxsContent = ({
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<Box> <Box>
{ showSocketInfo && ( { showSocketInfo && (
<SocketNewItemsNotice <SocketNewItemsNotice.Mobile
url={ window.location.href } url={ window.location.href }
num={ socketInfoNum } num={ socketInfoNum }
alert={ socketInfoAlert } alert={ socketInfoAlert }
borderBottomRadius={ 0 } isLoading={ isPlaceholderData }
> />
{ ({ content }) => <Box>{ content }</Box> }
</SocketNewItemsNotice>
) } ) }
{ data.items.map(tx => ( { data.items.map((tx, index) => (
<TxsListItem <TxsListItem
key={ tx.hash + (isPlaceholderData ? index : '') }
tx={ tx } tx={ tx }
key={ tx.hash }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isPlaceholderData }
/> />
)) } )) }
</Box> </Box>
...@@ -85,6 +84,7 @@ const TxsContent = ({ ...@@ -85,6 +84,7 @@ const TxsContent = ({
top={ top || query.isPaginationVisible ? 80 : 0 } top={ top || query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isPlaceholderData }
/> />
</Hide> </Hide>
</> </>
...@@ -97,15 +97,18 @@ const TxsContent = ({ ...@@ -97,15 +97,18 @@ const TxsContent = ({
setSorting={ setSortByValue } setSorting={ setSortByValue }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ query.isPaginationVisible } showPagination={ query.isPaginationVisible }
isLoading={ query.pagination.isLoading }
filterComponent={ filter } filterComponent={ filter }
linkSlot={ currentAddress ? <AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 }/> : null } linkSlot={ currentAddress ?
<AddressCsvExportLink address={ currentAddress } type="transactions" ml={ 2 } isLoading={ query.pagination.isLoading }/> : null
}
/> />
) : null; ) : null;
return ( return (
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
isLoading={ isLoading } isLoading={ false }
items={ data?.items } items={ data?.items }
skeletonProps={{ skeletonProps={{
isLongSkeleton: hasLongSkeleton, isLongSkeleton: hasLongSkeleton,
......
...@@ -28,9 +28,10 @@ type Props = { ...@@ -28,9 +28,10 @@ type Props = {
showPagination?: boolean; showPagination?: boolean;
filterComponent?: React.ReactNode; filterComponent?: React.ReactNode;
linkSlot?: React.ReactNode; linkSlot?: React.ReactNode;
isLoading?: boolean;
} }
const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true, linkSlot }: Props) => { const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true, linkSlot, isLoading }: Props) => {
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
...@@ -39,6 +40,7 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps ...@@ -39,6 +40,7 @@ const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps
options={ SORT_OPTIONS } options={ SORT_OPTIONS }
setSort={ setSorting } setSort={ setSorting }
sort={ sorting } sort={ sorting }
isLoading={ isLoading }
/> />
{ /* api is not implemented */ } { /* api is not implemented */ }
{ /* <FilterInput { /* <FilterInput
......
...@@ -2,8 +2,7 @@ import { ...@@ -2,8 +2,7 @@ import {
HStack, HStack,
Box, Box,
Flex, Flex,
Icon, Skeleton,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -18,6 +17,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -18,6 +17,7 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
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 Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
...@@ -31,12 +31,13 @@ type Props = { ...@@ -31,12 +31,13 @@ type Props = {
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TAG_WIDTH = 48; const TAG_WIDTH = 48;
const ARROW_WIDTH = 24; const ARROW_WIDTH = 24;
const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => { const TxsListItem = ({ tx, isLoading, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
...@@ -48,53 +49,60 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -48,53 +49,60 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
<ListItemMobile display="block" width="100%" isAnimated key={ tx.hash }> <ListItemMobile display="block" width="100%" isAnimated key={ tx.hash }>
<Flex justifyContent="space-between" mt={ 4 }> <Flex justifyContent="space-between" mt={ 4 }>
<HStack> <HStack>
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</HStack> </HStack>
<TxAdditionalInfo tx={ tx } isMobile/> <TxAdditionalInfo tx={ tx } isMobile isLoading={ isLoading }/>
</Flex> </Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 } alignItems="center"> <Flex justifyContent="space-between" lineHeight="24px" mt={ 3 } alignItems="center">
<Flex> <Flex>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
boxSize="30px" boxSize="30px"
mr={ 2 }
color="link" color="link"
isLoading={ isLoading }
/> />
<Address width="100%"> <Address width="100%" ml={ 2 }>
<AddressLink <AddressLink
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
truncation="constant" truncation="constant"
isLoading={ isLoading }
/> />
</Address> </Address>
</Flex> </Flex>
{ tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> } { tx.timestamp && (
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontWeight="400" fontSize="sm">
<span>{ timeAgo }</span>
</Skeleton>
) }
</Flex> </Flex>
{ tx.method && ( { tx.method && (
<Flex mt={ 3 }> <Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text> <Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre">Method </Skeleton>
<Text <Skeleton
as="span" isLoaded={ !isLoading }
variant="secondary" color="text_secondary"
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow="ellipsis" textOverflow="ellipsis"
> >
{ tx.method } <span>{ tx.method }</span>
</Text> </Skeleton>
</Flex> </Flex>
) } ) }
{ showBlockInfo && tx.block !== null && ( { showBlockInfo && tx.block !== null && (
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Block </Text> <Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre">Block </Skeleton>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal> <Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal>
</Skeleton>
</Box> </Box>
) } ) }
<Flex alignItems="center" height={ 6 } mt={ 6 }> <Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address maxWidth={ `calc((100% - ${ currentAddress ? TAG_WIDTH + 16 : ARROW_WIDTH + 8 }px)/2)` }> <Address maxWidth={ `calc((100% - ${ currentAddress ? TAG_WIDTH + 16 : ARROW_WIDTH + 8 }px)/2)` }>
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -102,21 +110,24 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -102,21 +110,24 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
isDisabled={ isOut } isDisabled={ isOut }
isLoading={ isLoading }
/> />
{ !isOut && <CopyToClipboard text={ tx.from.hash }/> } { !isOut && <CopyToClipboard text={ tx.from.hash } isLoading={ isLoading }/> }
</Address> </Address>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mx={ 2 }/> : ( <InOutTag isIn={ isIn } isOut={ isOut } width="48px" mx={ 2 } isLoading={ isLoading }/> : (
<Icon <Box mx={ 2 }>
as={ rightArrowIcon } <Icon
boxSize={ 6 } as={ rightArrowIcon }
mx={ 2 } boxSize={ 6 }
color="gray.500" color="gray.500"
/> isLoading={ isLoading }
/>
</Box>
) } ) }
{ dataTo ? ( { dataTo ? (
<Address maxWidth={ `calc((100% - ${ currentAddress ? TAG_WIDTH + 16 : ARROW_WIDTH + 8 }px)/2)` }> <Address maxWidth={ `calc((100% - ${ currentAddress ? TAG_WIDTH + 16 : ARROW_WIDTH + 8 }px)/2)` }>
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink <AddressLink
type="address" type="address"
hash={ dataTo.hash } hash={ dataTo.hash }
...@@ -124,18 +135,19 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: ...@@ -124,18 +135,19 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
fontWeight="500" fontWeight="500"
ml={ 2 } ml={ 2 }
isDisabled={ isIn } isDisabled={ isIn }
isLoading={ isLoading }
/> />
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> } { !isIn && <CopyToClipboard text={ dataTo.hash } isLoading={ isLoading }/> }
</Address> </Address>
) : '-' } ) : '-' }
</Flex> </Flex>
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Value { appConfig.network.currency.symbol } </Text> <Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre">Value { appConfig.network.currency.symbol } </Skeleton>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.value).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" variant="text_secondary">{ getValueWithUnit(tx.value).toFormat() }</Skeleton>
</Box> </Box>
<Box mt={ 2 } mb={ 3 }> <Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { appConfig.network.currency.symbol } </Text> <Skeleton isLoaded={ !isLoading } display="inline-block" whiteSpace="pre">Fee { appConfig.network.currency.symbol } </Skeleton>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" variant="text_secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Skeleton>
</Box> </Box>
</ListItemMobile> </ListItemMobile>
); );
......
import { Link, Table, Tbody, Tr, Th, Td, Icon } from '@chakra-ui/react'; import { Link, Table, Tbody, Tr, Th, Icon } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import React from 'react'; import React from 'react';
...@@ -7,7 +7,7 @@ import type { Sort } from 'types/client/txs-sort'; ...@@ -7,7 +7,7 @@ import type { Sort } from 'types/client/txs-sort';
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 SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice'; import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TheadSticky from 'ui/shared/TheadSticky'; import TheadSticky from 'ui/shared/TheadSticky';
import TxsTableItem from './TxsTableItem'; import TxsTableItem from './TxsTableItem';
...@@ -23,6 +23,7 @@ type Props = { ...@@ -23,6 +23,7 @@ type Props = {
socketInfoNum?: number; socketInfoNum?: number;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TxsTable = ({ const TxsTable = ({
...@@ -36,6 +37,7 @@ const TxsTable = ({ ...@@ -36,6 +37,7 @@ const TxsTable = ({
socketInfoNum, socketInfoNum,
currentAddress, currentAddress,
enableTimeIncrement, enableTimeIncrement,
isLoading,
}: Props) => { }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
...@@ -67,18 +69,22 @@ const TxsTable = ({ ...@@ -67,18 +69,22 @@ const TxsTable = ({
</TheadSticky> </TheadSticky>
<Tbody> <Tbody>
{ showSocketInfo && ( { showSocketInfo && (
<SocketNewItemsNotice borderRadius={ 0 } url={ window.location.href } alert={ socketInfoAlert } num={ socketInfoNum }> <SocketNewItemsNotice.Desktop
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> } url={ window.location.href }
</SocketNewItemsNotice> alert={ socketInfoAlert }
num={ socketInfoNum }
isLoading={ isLoading }
/>
) } ) }
<AnimatePresence initial={ false }> <AnimatePresence initial={ false }>
{ txs.map((item) => ( { txs.map((item, index) => (
<TxsTableItem <TxsTableItem
key={ item.hash } key={ item.hash + (isLoading ? index : '') }
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
isLoading={ isLoading }
/> />
)) } )) }
</AnimatePresence> </AnimatePresence>
......
import { import {
Tr, Tr,
Td, Td,
Tag,
Icon,
VStack, VStack,
Text,
Show, Show,
Hide, Hide,
Flex, Flex,
Skeleton,
Box,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
...@@ -20,11 +19,12 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; ...@@ -20,11 +19,12 @@ import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
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 Icon from 'ui/shared/chakra/Icon';
import Tag from 'ui/shared/chakra/Tag';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
...@@ -35,9 +35,10 @@ type Props = { ...@@ -35,9 +35,10 @@ type Props = {
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean; enableTimeIncrement?: boolean;
isLoading?: boolean;
} }
const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => { const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, isLoading }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === dataTo?.hash); const isIn = Boolean(currentAddress && currentAddress === dataTo?.hash);
...@@ -46,17 +47,34 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -46,17 +47,34 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const addressFrom = ( const addressFrom = (
<Address w="100%"> <Address w="100%">
<AddressIcon address={ tx.from }/> <AddressIcon address={ tx.from } isLoading={ isLoading }/>
<AddressLink type="address" hash={ tx.from.hash } alias={ tx.from.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isOut }/> <AddressLink
{ !isOut && <CopyToClipboard text={ tx.from.hash }/> } type="address"
hash={ tx.from.hash }
alias={ tx.from.name }
fontWeight="500" ml={ 2 }
truncation="constant"
isDisabled={ isOut }
isLoading={ isLoading }
/>
{ !isOut && <CopyToClipboard text={ tx.from.hash } isLoading={ isLoading }/> }
</Address> </Address>
); );
const addressTo = dataTo ? ( const addressTo = dataTo ? (
<Address w="100%"> <Address w="100%">
<AddressIcon address={ dataTo }/> <AddressIcon address={ dataTo } isLoading={ isLoading }/>
<AddressLink type="address" hash={ dataTo.hash } alias={ dataTo.name } fontWeight="500" ml={ 2 } truncation="constant" isDisabled={ isIn }/> <AddressLink
{ !isIn && <CopyToClipboard text={ dataTo.hash }/> } type="address"
hash={ dataTo.hash }
alias={ dataTo.name }
fontWeight="500"
ml={ 2 }
truncation="constant"
isDisabled={ isIn }
isLoading={ isLoading }
/>
{ !isIn && <CopyToClipboard text={ dataTo.hash } isLoading={ isLoading }/> }
</Address> </Address>
) : '-'; ) : '-';
...@@ -70,7 +88,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -70,7 +88,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
key={ tx.hash } key={ tx.hash }
> >
<Td pl={ 4 }> <Td pl={ 4 }>
<TxAdditionalInfo tx={ tx }/> <TxAdditionalInfo tx={ tx } isLoading={ isLoading }/>
</Td> </Td>
<Td pr={ 4 }> <Td pr={ 4 }>
<VStack alignItems="start" lineHeight="24px"> <VStack alignItems="start" lineHeight="24px">
...@@ -79,29 +97,34 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -79,29 +97,34 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
hash={ tx.hash } hash={ tx.hash }
type="transaction" type="transaction"
fontWeight="700" fontWeight="700"
isLoading={ isLoading }
/> />
</Address> </Address>
{ tx.timestamp && <Text color="gray.500" fontWeight="400">{ timeAgo }</Text> } { tx.timestamp && <Skeleton color="text_secondary" fontWeight="400" isLoaded={ !isLoading }><span>{ timeAgo }</span></Skeleton> }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<VStack alignItems="start"> <VStack alignItems="start">
<TxType types={ tx.tx_types }/> <TxType types={ tx.tx_types } isLoading={ isLoading }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/> <TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined } isLoading={ isLoading }/>
</VStack> </VStack>
</Td> </Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
{ tx.method && ( { tx.method && (
<TruncatedTextTooltip label={ tx.method }> <Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' } isLoading={ isLoading } isTruncated>
<Tag colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }> { tx.method }
{ tx.method } </Tag>
</Tag>
</TruncatedTextTooltip>
) } ) }
</Td> </Td>
{ showBlockInfo && ( { showBlockInfo && (
<Td> <Td>
{ tx.block && <LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>{ tx.block }</LinkInternal> } { tx.block && (
<Skeleton isLoaded={ !isLoading } display="inline-block">
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: tx.block.toString() } }) }>
{ tx.block }
</LinkInternal>
</Skeleton>
) }
</Td> </Td>
) } ) }
<Show above="xl" ssr={ false }> <Show above="xl" ssr={ false }>
...@@ -110,9 +133,11 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -110,9 +133,11 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 }>
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 }/> : <InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 } isLoading={ isLoading }/> : (
<Icon as={ rightArrowIcon } boxSize={ 6 } mx="6px" color="gray.500"/> <Box mx="6px">
} <Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500" isLoading={ isLoading }/>
</Box>
) }
</Td> </Td>
<Td> <Td>
{ addressTo } { addressTo }
...@@ -122,16 +147,15 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -122,16 +147,15 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
<Td colSpan={ 3 }> <Td colSpan={ 3 }>
<Flex alignItems="center"> <Flex alignItems="center">
{ (isIn || isOut) ? { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px"/> : <InOutTag isIn={ isIn } isOut={ isOut } width="48px" isLoading={ isLoading }/> : (
(
<Icon <Icon
as={ rightArrowIcon } as={ rightArrowIcon }
boxSize={ 6 } boxSize={ 6 }
color="gray.500" color="gray.500"
transform="rotate(90deg)" transform="rotate(90deg)"
isLoading={ isLoading }
/> />
) ) }
}
<VStack alignItems="start" overflow="hidden" ml={ 1 }> <VStack alignItems="start" overflow="hidden" ml={ 1 }>
{ addressFrom } { addressFrom }
{ addressTo } { addressTo }
...@@ -140,10 +164,10 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement } ...@@ -140,10 +164,10 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Td> </Td>
</Hide> </Hide>
<Td isNumeric> <Td isNumeric>
<CurrencyValue value={ tx.value } accuracy={ 8 }/> <CurrencyValue value={ tx.value } accuracy={ 8 } isLoading={ isLoading }/>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
<CurrencyValue value={ tx.fee.value } accuracy={ 8 }/> <CurrencyValue value={ tx.fee.value } accuracy={ 8 } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -20,6 +20,10 @@ export default function useTxsSort( ...@@ -20,6 +20,10 @@ export default function useTxsSort(
const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort); const [ sorting, setSorting ] = React.useState<Sort>(cookies.get(cookies.NAMES.TXS_SORT) as Sort);
const setSortByField = React.useCallback((field: 'val' | 'fee') => () => { const setSortByField = React.useCallback((field: 'val' | 'fee') => () => {
if (queryResult.isPlaceholderData) {
return;
}
setSorting((prevVal) => { setSorting((prevVal) => {
let newVal: Sort = ''; let newVal: Sort = '';
if (field === 'val') { if (field === 'val') {
...@@ -43,7 +47,7 @@ export default function useTxsSort( ...@@ -43,7 +47,7 @@ export default function useTxsSort(
cookies.set(cookies.NAMES.TXS_SORT, newVal); cookies.set(cookies.NAMES.TXS_SORT, newVal);
return newVal; return newVal;
}); });
}, [ ]); }, [ queryResult.isPlaceholderData ]);
const setSortByValue = React.useCallback((value: Sort | undefined) => { const setSortByValue = React.useCallback((value: Sort | undefined) => {
setSorting((prevVal: Sort) => { setSorting((prevVal: Sort) => {
......
import { HStack, VStack, chakra, Icon, Flex, Skeleton } from '@chakra-ui/react'; import { HStack, VStack, chakra, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
...@@ -8,6 +8,7 @@ import TokensIcon from 'icons/tokens.svg'; ...@@ -8,6 +8,7 @@ import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg'; // import WalletIcon from 'icons/wallet.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Icon from 'ui/shared/chakra/Icon';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
...@@ -45,10 +46,8 @@ const WatchListAddressItem = ({ item, isLoading }: { item: TWatchlistItem; isLoa ...@@ -45,10 +46,8 @@ const WatchListAddressItem = ({ item, isLoading }: { item: TWatchlistItem; isLoa
</Skeleton> </Skeleton>
</Flex> </Flex>
{ item.tokens_count && ( { item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" pl={ infoItemsPaddingLeft }> <HStack spacing={ 2 } fontSize="sm" pl={ infoItemsPaddingLeft }>
<Skeleton isLoaded={ !isLoading } boxSize={ 5 } mr={ 2 } borderRadius="sm"> <Icon as={ TokensIcon } boxSize={ 5 } isLoading={ isLoading } borderRadius="sm"/>
<Icon as={ TokensIcon } boxSize={ 5 }/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-flex"> <Skeleton isLoaded={ !isLoading } display="inline-flex">
<span>{ `Tokens:${ nbsp }` + item.tokens_count }</span> <span>{ `Tokens:${ nbsp }` + item.tokens_count }</span>
{ /* api does not provide token prices */ } { /* api does not provide token prices */ }
......
import { Icon } from '@chakra-ui/react'; import { Flex, Icon, Skeleton } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -16,7 +16,7 @@ import CurrencyValue from 'ui/shared/CurrencyValue'; ...@@ -16,7 +16,7 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = { type Props = ({
item: WithdrawalsItem; item: WithdrawalsItem;
view: 'list'; view: 'list';
} | { } | {
...@@ -25,44 +25,52 @@ type Props = { ...@@ -25,44 +25,52 @@ type Props = {
} | { } | {
item: BlockWithdrawalsItem; item: BlockWithdrawalsItem;
view: 'block'; view: 'block';
}; }) & { isLoading?: boolean };
const WithdrawalsListItem = ({ item, view }: Props) => { const WithdrawalsListItem = ({ item, isLoading, view }: Props) => {
return ( return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto"> <ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label>Index</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ item.index } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.index }</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Validator index</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Validator index</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ item.validator_index } <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.validator_index }</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ view !== 'block' && ( { view !== 'block' && (
<> <>
<ListItemMobileGrid.Label>Block</ListItemMobileGrid.Label><ListItemMobileGrid.Value> <ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<LinkInternal <ListItemMobileGrid.Value>
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) } { isLoading ? (
display="flex" <Flex columnGap={ 1 } alignItems="center">
width="fit-content" <Skeleton boxSize={ 6 }/>
alignItems="center" <Skeleton display="inline-block">{ item.block_number }</Skeleton>
> </Flex>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> ) : (
{ item.block_number } <LinkInternal
</LinkInternal> href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
display="flex"
width="fit-content"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.block_number }
</LinkInternal>
) }
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
{ view !== 'address' && ( { view !== 'address' && (
<> <>
<ListItemMobileGrid.Label>To</ListItemMobileGrid.Label><ListItemMobileGrid.Value> <ListItemMobileGrid.Label isLoading={ isLoading }>To</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
<Address> <Address>
<AddressIcon address={ item.receiver }/> <AddressIcon address={ item.receiver } isLoading={ isLoading }/>
<AddressLink type="address" hash={ item.receiver.hash } truncation="dynamic" ml={ 2 }/> <AddressLink type="address" hash={ item.receiver.hash } truncation="dynamic" ml={ 2 } isLoading={ isLoading }/>
</Address> </Address>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
...@@ -70,14 +78,14 @@ const WithdrawalsListItem = ({ item, view }: Props) => { ...@@ -70,14 +78,14 @@ const WithdrawalsListItem = ({ item, view }: Props) => {
{ view !== 'block' && ( { view !== 'block' && (
<> <>
<ListItemMobileGrid.Label>Age</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ dayjs(item.timestamp).fromNow() } <Skeleton isLoaded={ !isLoading } display="inline-block">{ dayjs(item.timestamp).fromNow() }</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label>Value</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<CurrencyValue value={ item.amount } currency={ appConfig.network.currency.symbol }/> <CurrencyValue value={ item.amount } currency={ appConfig.network.currency.symbol } isLoading={ isLoading }/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -12,6 +12,7 @@ import WithdrawalsTableItem from './WithdrawalsTableItem'; ...@@ -12,6 +12,7 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
type Props = { type Props = {
top: number; top: number;
isLoading?: boolean;
} & ({ } & ({
items: Array<WithdrawalsItem>; items: Array<WithdrawalsItem>;
view: 'list'; view: 'list';
...@@ -23,28 +24,28 @@ import WithdrawalsTableItem from './WithdrawalsTableItem'; ...@@ -23,28 +24,28 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
view: 'block'; view: 'block';
}); });
const WithdrawalsTable = ({ items, top, view = 'list' }: Props) => { const WithdrawalsTable = ({ items, isLoading, top, view = 'list' }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Index</Th> <Th minW="140px">Index</Th>
<Th>Validator index</Th> <Th minW="200px">Validator index</Th>
{ view !== 'block' && <Th>Block</Th> } { view !== 'block' && <Th w="25%">Block</Th> }
{ view !== 'address' && <Th>To</Th> } { view !== 'address' && <Th w="25%">To</Th> }
{ view !== 'block' && <Th>Age</Th> } { view !== 'block' && <Th w="25%">Age</Th> }
<Th>{ `Value ${ appConfig.network.currency.symbol }` }</Th> <Th w="25%">{ `Value ${ appConfig.network.currency.symbol }` }</Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ view === 'list' && (items as Array<WithdrawalsItem>).map((item) => ( { view === 'list' && (items as Array<WithdrawalsItem>).map((item, index) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="list"/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="list" isLoading={ isLoading }/>
)) } )) }
{ view === 'address' && (items as Array<AddressWithdrawalsItem>).map((item) => ( { view === 'address' && (items as Array<AddressWithdrawalsItem>).map((item, index) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="address"/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="address" isLoading={ isLoading }/>
)) } )) }
{ view === 'block' && (items as Array<BlockWithdrawalsItem>).map((item) => ( { view === 'block' && (items as Array<BlockWithdrawalsItem>).map((item, index) => (
<WithdrawalsTableItem key={ item.index } item={ item } view="block"/> <WithdrawalsTableItem key={ item.index + (isLoading ? String(index) : '') } item={ item } view="block" isLoading={ isLoading }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Td, Tr, Text, Icon } from '@chakra-ui/react'; import { Td, Tr, Icon, Skeleton, Flex } from '@chakra-ui/react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -14,7 +14,7 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -14,7 +14,7 @@ import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = ({
item: WithdrawalsItem; item: WithdrawalsItem;
view: 'list'; view: 'list';
} | { } | {
...@@ -23,45 +23,54 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -23,45 +23,54 @@ import LinkInternal from 'ui/shared/LinkInternal';
} | { } | {
item: BlockWithdrawalsItem; item: BlockWithdrawalsItem;
view: 'block'; view: 'block';
}; }) & { isLoading?: boolean };
const WithdrawalsTableItem = ({ item, view }: Props) => { const WithdrawalsTableItem = ({ item, view, isLoading }: Props) => {
return ( return (
<Tr> <Tr>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Text>{ item.index }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.index }</Skeleton>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Text>{ item.validator_index }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block">{ item.validator_index }</Skeleton>
</Td> </Td>
{ view !== 'block' && ( { view !== 'block' && (
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<LinkInternal { isLoading ? (
href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) } <Flex columnGap={ 1 } alignItems="center">
display="flex" <Skeleton boxSize={ 6 }/>
width="fit-content" <Skeleton display="inline-block">{ item.block_number }</Skeleton>
alignItems="center" </Flex>
> ) : (
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/> <LinkInternal
{ item.block_number } href={ route({ pathname: '/block/[height]', query: { height: item.block_number.toString() } }) }
</LinkInternal> display="flex"
width="fit-content"
alignItems="center"
>
<Icon as={ blockIcon } boxSize={ 6 } mr={ 1 }/>
{ item.block_number }
</LinkInternal>
) }
</Td> </Td>
) } ) }
{ view !== 'address' && ( { view !== 'address' && (
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Address> <Address>
<AddressIcon address={ item.receiver }/> <AddressIcon address={ item.receiver } isLoading={ isLoading }/>
<AddressLink type="address" hash={ item.receiver.hash } truncation="constant" ml={ 2 }/> <AddressLink type="address" hash={ item.receiver.hash } truncation="constant" ml={ 2 } isLoading={ isLoading }/>
</Address> </Address>
</Td> </Td>
) } ) }
{ view !== 'block' && ( { view !== 'block' && (
<Td verticalAlign="middle" pr={ 12 }> <Td verticalAlign="middle" pr={ 12 }>
<Text variant="secondary">{ dayjs(item.timestamp).fromNow() }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" color="text_secondary">
<span>{ dayjs(item.timestamp).fromNow() }</span>
</Skeleton>
</Td> </Td>
) } ) }
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<CurrencyValue value={ item.amount }/> <CurrencyValue value={ item.amount } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
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