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