Commit 5df8f5a1 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into navbar-margins

parents b493a0e8 713ca5d9
...@@ -57,7 +57,7 @@ jobs: ...@@ -57,7 +57,7 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
cache-from: type=registry,ref=ghcr.io/blockscout/frontend::buildcache cache-from: type=gha
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_REF_NAME_SLUG }} tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_REF_NAME_SLUG }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
......
...@@ -44,8 +44,8 @@ jobs: ...@@ -44,8 +44,8 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
cache-from: type=registry,ref=ghcr.io/blockscout/frontend::buildcache cache-from: type=gha
cache-to: type=registry,ref=ghcr.io/blockscout/frontend::buildcache,mode=max cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
......
...@@ -22,8 +22,12 @@ blockscout: ...@@ -22,8 +22,12 @@ blockscout:
# enable ingress # enable ingress
ingress: ingress:
enabled: true enabled: true
annotations: {} annotations:
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2' # - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
- 'nginx.ingress.kubernetes.io/cors-allow-origin: "https://*.blockscout-main.test.aws-k8s.blockscout.com, https://*.test.aws-k8s.blockscout.com, http://localhost:3000"'
- 'nginx.ingress.kubernetes.io/cors-allow-credentials: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH'
- 'nginx.ingress.kubernetes.io/enable-cors: "true"'
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
......
...@@ -24,7 +24,7 @@ const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiRespo ...@@ -24,7 +24,7 @@ const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiRespo
} }
const data = await Promise.all(watchlistData.map(async item => { const data = await Promise.all(watchlistData.map(async item => {
const tokens = await fetch(`?module=account&action=tokenlist&address=${ item.address_hash }`); const tokens = await fetch(`/api/?module=account&action=tokenlist&address=${ item.address_hash }`);
const tokensData = await tokens.json() as Tokenlist; const tokensData = await tokens.json() as Tokenlist;
return ({ ...item, tokens_count: Array.isArray(tokensData.result) ? tokensData.result.length : 0 }); return ({ ...item, tokens_count: Array.isArray(tokensData.result) ? tokensData.result.length : 0 });
......
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/internal-transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer'; import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
...@@ -86,3 +87,12 @@ export interface AddressBlocksValidatedResponse { ...@@ -86,3 +87,12 @@ export interface AddressBlocksValidatedResponse {
items_count: number; items_count: number;
}; };
} }
export interface AddressInternalTxsResponse {
items: Array<InternalTransaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_index: number;
} | null;
}
...@@ -5,6 +5,7 @@ import type { ...@@ -5,6 +5,7 @@ import type {
AddressTokenTransferFilters, AddressTokenTransferFilters,
AddressCoinBalanceHistoryResponse, AddressCoinBalanceHistoryResponse,
AddressBlocksValidatedResponse, AddressBlocksValidatedResponse,
AddressInternalTxsResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
...@@ -18,6 +19,7 @@ import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull'; ...@@ -18,6 +19,7 @@ import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys = export type PaginatedQueryKeys =
QueryKeys.addressTxs | QueryKeys.addressTxs |
QueryKeys.addressTokenTransfers | QueryKeys.addressTokenTransfers |
QueryKeys.addressInternalTxs |
QueryKeys.blocks | QueryKeys.blocks |
QueryKeys.blocksReorgs | QueryKeys.blocksReorgs |
QueryKeys.blocksUncles | QueryKeys.blocksUncles |
...@@ -31,6 +33,7 @@ export type PaginatedQueryKeys = ...@@ -31,6 +33,7 @@ export type PaginatedQueryKeys =
QueryKeys.addressBlocksValidated; QueryKeys.addressBlocksValidated;
export type PaginatedResponse<Q extends PaginatedQueryKeys> = export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.addressInternalTxs ? AddressInternalTxsResponse :
Q extends QueryKeys.addressTxs ? AddressTransactionsResponse : Q extends QueryKeys.addressTxs ? AddressTransactionsResponse :
Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferResponse : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferResponse :
Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse : Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse :
...@@ -45,7 +48,7 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> = ...@@ -45,7 +48,7 @@ export type PaginatedResponse<Q extends PaginatedQueryKeys> =
never never
export type PaginationFilters<Q extends PaginatedQueryKeys> = export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.addressTxs ? AddressTxsFilters : Q extends (QueryKeys.addressTxs | QueryKeys.addressInternalTxs) ? AddressTxsFilters :
Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferFilters : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferFilters :
Q extends QueryKeys.blocks ? BlockFilters : Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters : Q extends QueryKeys.txsValidate ? TTxsFilters :
...@@ -61,6 +64,7 @@ type PaginationFields = { ...@@ -61,6 +64,7 @@ type PaginationFields = {
export const PAGINATION_FIELDS: PaginationFields = { export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.addressTxs]: [ 'block_number', 'items_count', 'index' ], [QueryKeys.addressTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.addressInternalTxs]: [ 'block_number', 'items_count', 'index', 'transaction_index' ],
[QueryKeys.addressTokenTransfers]: [ 'block_number', 'items_count', 'index', 'transaction_hash' ], [QueryKeys.addressTokenTransfers]: [ 'block_number', 'items_count', 'index', 'transaction_hash' ],
[QueryKeys.blocks]: [ 'block_number', 'items_count' ], [QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ], [QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ],
...@@ -81,6 +85,7 @@ type PaginationFiltersFields = { ...@@ -81,6 +85,7 @@ type PaginationFiltersFields = {
export const PAGINATION_FILTERS_FIELDS: PaginationFiltersFields = { export const PAGINATION_FILTERS_FIELDS: PaginationFiltersFields = {
[QueryKeys.addressTxs]: [ 'filter' ], [QueryKeys.addressTxs]: [ 'filter' ],
[QueryKeys.addressInternalTxs]: [ 'filter' ],
[QueryKeys.addressTokenTransfers]: [ 'filter', 'type' ], [QueryKeys.addressTokenTransfers]: [ 'filter', 'type' ],
[QueryKeys.addressCoinBalanceHistory]: [], [QueryKeys.addressCoinBalanceHistory]: [],
[QueryKeys.addressBlocksValidated]: [], [QueryKeys.addressBlocksValidated]: [],
......
...@@ -20,12 +20,20 @@ export type GasPrices = { ...@@ -20,12 +20,20 @@ export type GasPrices = {
} }
export type Stats = { export type Stats = {
totalBlocksAllTime: string; counters: {
averageBlockTime: string;
completedTransactions: string;
totalAccounts: string;
totalBlocksAllTime: string;
totalTransactions: string;
};
} }
export type Charts = { export type Charts = {
'chart': Array<{ chart: Array<ChartsItem>;
date: string; }
value: string;
}>; export type ChartsItem ={
date: string;
value: string;
} }
...@@ -29,4 +29,5 @@ export enum QueryKeys { ...@@ -29,4 +29,5 @@ export enum QueryKeys {
addressTxs='addressTxs', addressTxs='addressTxs',
addressTokenTransfers='addressTokenTransfers', addressTokenTransfers='addressTokenTransfers',
addressBlocksValidated='address-blocks-validated', addressBlocksValidated='address-blocks-validated',
addressInternalTxs='address-internal-txs',
} }
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp';
import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = `/node-api/addresses/${ ADDRESS_HASH }/internal-transactions`;
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressInternalTxs/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
import { Text, Show, Hide } from '@chakra-ui/react';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import AddressIntTxsSkeletonDesktop from 'ui/address/internals/AddressIntTxsSkeletonDesktop';
import AddressIntTxsSkeletonMobile from 'ui/address/internals/AddressIntTxsSkeletonMobile';
import AddressIntTxsTable from 'ui/address/internals/AddressIntTxsTable';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList';
const SCROLL_ELEM = 'address-internas-txs';
const SCROLL_OFFSET = -100;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressInternalTxs = () => {
const router = useRouter();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const queryId = router.query.id;
const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`,
queryName: QueryKeys.addressInternalTxs,
queryIds: queryIdArray,
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
onFilterChange({ filter: newVal });
}, [ onFilterChange ]);
if (isLoading) {
return (
<>
<Show below="lg"><AddressIntTxsSkeletonMobile/></Show>
<Hide below="lg"><AddressIntTxsSkeletonDesktop/></Hide>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0 && !filterValue) {
return <Text as="span">There are no internal transactions for this address.</Text>;
}
let content;
if (data.items.length === 0) {
content = <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
} else {
content = (
<>
<Show below="lg" ssr={ false }>
<AddressIntTxsList data={ data.items } currentAddress={ queryIdStr }/>
</Show>
<Hide below="lg" ssr={ false }>
<AddressIntTxsTable data={ data.items } currentAddress={ queryIdStr }/>
</Hide>
</>
);
}
return (
<Element name={ SCROLL_ELEM }>
<ActionBar mt={ -6 }>
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
{ !isPaginatorHidden && <Pagination ml="auto" { ...pagination }/> }
</ActionBar>
{ content }
</Element>
);
};
export default AddressInternalTxs;
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -13,6 +14,7 @@ const AddressTokenTransfers = () => { ...@@ -13,6 +14,7 @@ const AddressTokenTransfers = () => {
<TokenTransfer <TokenTransfer
path={ `/node-api/addresses/${ hash }/token-transfers` } path={ `/node-api/addresses/${ hash }/token-transfers` }
queryName={ QueryKeys.addressTokenTransfers } queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) }
baseAddress={ typeof hash === 'string' ? hash : undefined } baseAddress={ typeof hash === 'string' ? hash : undefined }
/> />
); );
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll'; import { Element } from 'react-scroll';
...@@ -30,6 +31,7 @@ const AddressTxs = () => { ...@@ -30,6 +31,7 @@ const AddressTxs = () => {
const addressTxsQuery = useQueryWithPages({ const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`, apiPath: `/node-api/addresses/${ router.query.id }/transactions`,
queryName: QueryKeys.addressTxs, queryName: QueryKeys.addressTxs,
queryIds: castArray(router.query.id),
filters: { filter: filterValue }, filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import AddressIntTxsListItem from 'ui/address/internals/AddressIntTxsListItem';
type Props = {
data: Array<InternalTransaction>;
currentAddress: string;
}
const AddressIntTxsList = ({ data, currentAddress }: Props) => {
return (
<Box>
{ data.map((item) => <AddressIntTxsListItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>) }
</Box>
);
};
export default AddressIntTxsList;
import { Flex, Tag, Icon, Box, HStack, Text, Link } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string };
const TxInternalsListItem = ({
type,
from,
to,
value,
success,
error,
created_contract: createdContract,
transaction_hash: txnHash,
block,
timestamp,
currentAddress,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
<Flex justifyContent="space-between" width="100%">
<AddressLink fontWeight="700" hash={ txnHash } truncation="constant" type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text>
</Flex>
<HStack spacing={ 1 }>
<Text fontSize="sm" fontWeight={ 500 }>Block</Text>
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
</HStack>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
}
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ toData.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ toData.hash }/>
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency.symbol }</Text>
<Text fontSize="sm" variant="secondary">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text>
</HStack>
</AccountListItemMobile>
);
};
export default TxInternalsListItem;
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<SkeletonTable columns={ [ '15%', '15%', '10%', '20%', '20%', '20%' ] }/>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="100%" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default TxInternalsSkeletonMobile;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressIntTxsTableItem from './AddressIntTxsTableItem';
interface Props {
data: Array<InternalTransaction>;
currentAddress: string;
}
const AddressIntTxsTable = ({ data, currentAddress }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="15%">Parent txn hash</Th>
<Th width="15%">Type</Th>
<Th width="10%">Block</Th>
<Th width="20%">From</Th>
<Th width="48px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="20%" isNumeric>
Value { appConfig.network.currency.symbol }
</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<AddressIntTxsTableItem key={ item.transaction_hash } { ...item } currentAddress={ currentAddress }/>
)) }
</Tbody>
</Table>
);
};
export default AddressIntTxsTable;
import { Tr, Td, Tag, Icon, Box, Flex, Text, Link } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = InternalTransaction & { currentAddress: string }
const AddressIntTxsTableItem = ({
type,
from,
to,
value,
success,
error,
created_contract: createdContract,
transaction_hash: txnHash,
block,
timestamp,
currentAddress,
}: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const toData = to ? to : createdContract;
const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return (
<Tr alignItems="top">
<Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex rowGap={ 2 } flexWrap="wrap">
{ typeTitle && (
<Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Link href={ link('block', { id: block.toString() }) }>{ block }</Link>
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address>
</Td>
<Td px={ 0 } verticalAlign="middle">
{ (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
}
</Td>
<Td verticalAlign="middle">
<Address display="inline-flex" maxW="100%">
<AddressIcon hash={ toData.hash }/>
<AddressLink hash={ toData.hash } alias={ toData.name } fontWeight="500" ml={ 2 }/>
</Address>
</Td>
<Td isNumeric verticalAlign="middle">
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Td>
</Tr>
);
};
export default React.memo(AddressIntTxsTableItem);
...@@ -15,7 +15,7 @@ interface Props { ...@@ -15,7 +15,7 @@ interface Props {
caption?: string; caption?: string;
} }
const CHART_MARGIN = { bottom: 0, left: 10, right: 10, top: 0 }; const CHART_MARGIN = { bottom: 5, left: 10, right: 10, top: 0 };
const ChainIndicatorChart = ({ data }: Props) => { const ChainIndicatorChart = ({ data }: Props) => {
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
......
...@@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -39,7 +40,7 @@ const AddressPageContent = () => { ...@@ -39,7 +40,7 @@ const AddressPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: null }, { id: 'internal_txn', title: 'Internal txn', component: <AddressInternalTxs/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> },
// temporary show this tab in all address // temporary show this tab in all address
// later api will return info about available tabs // later api will return info about available tabs
......
...@@ -2,7 +2,7 @@ import { Box, Icon, Link } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box, Icon, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url'; import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import config from 'configs/app/config'; import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
......
...@@ -2,7 +2,7 @@ import { Box, Center, useColorMode } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/json-rpc-url'; import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
......
...@@ -14,7 +14,7 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> { ...@@ -14,7 +14,7 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
const ChartAxis = ({ type, scale, ticks, tickFormat, disableAnimation, anchorEl, ...props }: Props) => { const ChartAxis = ({ type, scale, ticks, tickFormat, disableAnimation, anchorEl, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null); const ref = React.useRef<SVGGElement>(null);
const textColorToken = useColorModeValue('blackAlpha.500', 'whiteAlpha.500'); const textColorToken = useColorModeValue('blackAlpha.600', 'whiteAlpha.500');
const textColor = useToken('colors', textColorToken); const textColor = useToken('colors', textColorToken);
React.useEffect(() => { React.useEffect(() => {
......
...@@ -13,7 +13,7 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> { ...@@ -13,7 +13,7 @@ interface Props extends Omit<React.SVGProps<SVGGElement>, 'scale'> {
const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }: Props) => { const ChartGridLine = ({ type, scale, ticks, size, disableAnimation, ...props }: Props) => {
const ref = React.useRef<SVGGElement>(null); const ref = React.useRef<SVGGElement>(null);
const strokeColorToken = useColorModeValue('blackAlpha.300', 'whiteAlpha.300'); const strokeColorToken = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const strokeColor = useToken('colors', strokeColorToken); const strokeColor = useToken('colors', strokeColorToken);
React.useEffect(() => { React.useEffect(() => {
......
import { Box, Grid, Heading, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, useColorModeValue, VisuallyHidden } from '@chakra-ui/react'; import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { Charts } from 'types/api/stats'; import type { TimeChartItem } from './types';
import { QueryKeys } from 'types/client/queries';
import type { StatsIntervalIds } from 'types/client/stats';
import repeatArrow from 'icons/repeat_arrow.svg'; import repeatArrow from 'icons/repeat_arrow.svg';
import dotsIcon from 'icons/vertical_dots.svg'; import dotsIcon from 'icons/vertical_dots.svg';
import useFetch from 'lib/hooks/useFetch';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
import ChartWidgetSkeleton from './ChartWidgetSkeleton'; import ChartWidgetSkeleton from './ChartWidgetSkeleton';
import { STATS_INTERVALS } from './constants';
import FullscreenChartModal from './FullscreenChartModal'; import FullscreenChartModal from './FullscreenChartModal';
type Props = { type Props = {
id: string; items?: Array<TimeChartItem>;
title: string; title: string;
description: string; description: string;
interval: StatsIntervalIds; isLoading: boolean;
} }
function formatDate(date: Date) { const ChartWidget = ({ items, title, description, isLoading }: Props) => {
return date.toISOString().substring(0, 10);
}
const ChartWidget = ({ id, title, description, interval }: Props) => {
const fetch = useFetch();
const selectedInterval = STATS_INTERVALS[interval];
const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const menuButtonColor = useColorModeValue('black', 'white');
const borderColor = useColorModeValue('gray.200', 'gray.600'); const borderColor = useColorModeValue('gray.200', 'gray.600');
const url = `/node-api/stats/charts?name=${ id }${ startDate ? `&from=${ startDate }&to=${ endDate }` : '' }`;
const { data, isLoading } = useQuery<unknown, unknown, Charts>(
[ QueryKeys.charts, id, startDate ],
async() => await fetch(url),
);
const handleZoom = useCallback(() => { const handleZoom = useCallback(() => {
setIsZoomResetInitial(false); setIsZoomResetInitial(false);
}, []); }, []);
...@@ -75,16 +51,11 @@ const ChartWidget = ({ id, title, description, interval }: Props) => { ...@@ -75,16 +51,11 @@ const ChartWidget = ({ id, title, description, interval }: Props) => {
return <ChartWidgetSkeleton/>; return <ChartWidgetSkeleton/>;
} }
if (data) { if (items) {
const items = data.chart
.map((item) => {
return { date: new Date(item.date), value: Number(item.value) };
});
return ( return (
<> <>
<Box <Box
padding={{ base: 3, md: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
border="1px" border="1px"
borderColor={ borderColor } borderColor={ borderColor }
...@@ -93,12 +64,15 @@ const ChartWidget = ({ id, title, description, interval }: Props) => { ...@@ -93,12 +64,15 @@ const ChartWidget = ({ id, title, description, interval }: Props) => {
gridTemplateColumns="auto auto 36px" gridTemplateColumns="auto auto 36px"
gridColumnGap={ 2 } gridColumnGap={ 2 }
> >
<Heading <Text
mb={ 1 } fontWeight={ 600 }
size={{ base: 'xs', md: 'sm' }} fontSize="md"
lineHeight={ 6 }
as="p"
size={{ base: 'xs', lg: 'sm' }}
> >
{ title } { title }
</Heading> </Text>
<Text <Text
mb={ 1 } mb={ 1 }
...@@ -110,22 +84,23 @@ const ChartWidget = ({ id, title, description, interval }: Props) => { ...@@ -110,22 +84,23 @@ const ChartWidget = ({ id, title, description, interval }: Props) => {
{ description } { description }
</Text> </Text>
<IconButton <Tooltip label="Reset zoom">
hidden={ isZoomResetInitial } <IconButton
aria-label="Reset zoom" hidden={ isZoomResetInitial }
title="Reset zoom" aria-label="Reset zoom"
colorScheme="blue" colorScheme="blue"
w={ 9 } w={ 9 }
h={ 8 } h={ 8 }
gridColumn={ 2 } gridColumn={ 2 }
justifySelf="end" justifySelf="end"
alignSelf="top" alignSelf="top"
gridRow="1/3" gridRow="1/3"
size="sm" size="sm"
variant="ghost" variant="outline"
onClick={ handleZoomResetClick } onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrow } w={ 4 } h={ 4 } color="blue.700"/> } icon={ <Icon as={ repeatArrow } w={ 4 } h={ 4 }/> }
/> />
</Tooltip>
<Menu> <Menu>
<MenuButton <MenuButton
...@@ -134,8 +109,9 @@ const ChartWidget = ({ id, title, description, interval }: Props) => { ...@@ -134,8 +109,9 @@ const ChartWidget = ({ id, title, description, interval }: Props) => {
justifySelf="end" justifySelf="end"
w="36px" w="36px"
h="32px" h="32px"
icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 } color={ menuButtonColor }/> } icon={ <Icon as={ dotsIcon } w={ 4 } h={ 4 }/> }
colorScheme="transparent" colorScheme="gray"
variant="ghost"
as={ IconButton } as={ IconButton }
> >
<VisuallyHidden> <VisuallyHidden>
...@@ -160,6 +136,7 @@ const ChartWidget = ({ id, title, description, interval }: Props) => { ...@@ -160,6 +136,7 @@ const ChartWidget = ({ id, title, description, interval }: Props) => {
isOpen={ isFullscreen } isOpen={ isFullscreen }
items={ items } items={ items }
title={ title } title={ title }
description={ description }
onClose={ clearFullscreenChart } onClose={ clearFullscreenChart }
/> />
</> </>
......
...@@ -15,20 +15,21 @@ import useChartSize from 'ui/shared/chart/useChartSize'; ...@@ -15,20 +15,21 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController'; import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props { interface Props {
isEnlarged?: boolean;
title: string; title: string;
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onZoom: () => void; onZoom: () => void;
isZoomResetInitial: boolean; isZoomResetInitial: boolean;
} }
const CHART_MARGIN = { bottom: 20, left: 52, right: 30, top: 10 }; const CHART_MARGIN = { bottom: 20, left: 30, right: 20, top: 10 };
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) => { const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const ref = React.useRef<SVGSVGElement>(null); const ref = React.useRef<SVGSVGElement>(null);
const color = useToken('colors', 'blue.200'); const color = useToken('colors', 'blue.200');
const overlayRef = React.useRef<SVGRectElement>(null); const overlayRef = React.useRef<SVGRectElement>(null);
const chartId = useMemo(() => `chart-${ crypto.randomUUID() }`, []); const chartId = useMemo(() => `chart-${ title.split(' ').join('') }`, [ title ]);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]); const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN); const { innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
...@@ -60,7 +61,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) = ...@@ -60,7 +61,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) =
<ChartGridLine <ChartGridLine
type="horizontal" type="horizontal"
scale={ yScale } scale={ yScale }
ticks={ 3 } ticks={ isEnlarged ? 6 : 3 }
size={ innerWidth } size={ innerWidth }
disableAnimation disableAnimation
/> />
...@@ -78,28 +79,28 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) = ...@@ -78,28 +79,28 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) =
xScale={ xScale } xScale={ xScale }
yScale={ yScale } yScale={ yScale }
stroke={ color } stroke={ color }
animation="left" animation="none"
strokeWidth={ 3 } strokeWidth={ isMobile ? 1 : 2 }
/> />
<ChartAxis <ChartAxis
type="left" type="left"
scale={ yScale } scale={ yScale }
ticks={ 5 } ticks={ isEnlarged ? 6 : 3 }
tickFormat={ yTickFormat } tickFormat={ yTickFormat }
disableAnimation disableAnimation
/> />
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }> <ChartAxis
<ChartAxis type="bottom"
type="bottom" scale={ xScale }
scale={ xScale } transform={ `translate(0, ${ innerHeight })` }
transform={ `translate(0, ${ innerHeight })` } ticks={ isMobile ? 1 : 4 }
ticks={ isMobile ? 1 : 3 } anchorEl={ overlayRef.current }
anchorEl={ overlayRef.current } disableAnimation
disableAnimation />
/>
<ChartOverlay ref={ overlayRef } width={ innerWidth } height={ innerHeight }>
<ChartTooltip <ChartTooltip
chartId={ chartId } chartId={ chartId }
anchorEl={ overlayRef.current } anchorEl={ overlayRef.current }
......
...@@ -5,7 +5,7 @@ const ChartWidgetSkeleton = () => { ...@@ -5,7 +5,7 @@ const ChartWidgetSkeleton = () => {
return ( return (
<Box <Box
height="235px" height="235px"
paddingY={{ base: 3, md: 4 }} paddingY={{ base: 3, lg: 4 }}
> >
<Skeleton w="75%" h="24px" mb={ 1 }/> <Skeleton w="75%" h="24px" mb={ 1 }/>
<Skeleton w="50%" h="18px" mb={ 5 }/> <Skeleton w="50%" h="18px" mb={ 5 }/>
......
import { Button, Flex, Heading, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react'; import { Box, Button, Grid, Heading, Icon, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay, Text } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TimeChartItem } from '../shared/chart/types'; import type { TimeChartItem } from './types';
import repeatArrow from 'icons/repeat_arrow.svg';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
title: string; title: string;
description: string;
items: Array<TimeChartItem>; items: Array<TimeChartItem>;
onClose: () => void; onClose: () => void;
} }
...@@ -15,6 +18,7 @@ type Props = { ...@@ -15,6 +18,7 @@ type Props = {
const FullscreenChartModal = ({ const FullscreenChartModal = ({
isOpen, isOpen,
title, title,
description,
items, items,
onClose, onClose,
}: Props) => { }: Props) => {
...@@ -39,44 +43,53 @@ const FullscreenChartModal = ({ ...@@ -39,44 +43,53 @@ const FullscreenChartModal = ({
<ModalContent> <ModalContent>
<ModalHeader> <Box
<Flex mb={ 1 }
alignItems="center" >
<Grid
gridColumnGap={ 2 }
> >
<Heading <Heading
as="h2" mb={ 1 }
gridColumn={ 2 } size={{ base: 'xs', sm: 'md' }}
fontSize={{ base: '2xl', sm: '3xl' }}
fontWeight="medium"
lineHeight={ 1 }
color="blue.600"
> >
{ title } { title }
</Heading> </Heading>
<Text
gridColumn={ 1 }
as="p"
variant="secondary"
fontSize="xs"
>
{ description }
</Text>
{ !isZoomResetInitial && ( { !isZoomResetInitial && (
<Button <Button
ml="auto" leftIcon={ <Icon as={ repeatArrow } w={ 4 } h={ 4 }/> }
colorScheme="blue"
gridColumn={ 2 } gridColumn={ 2 }
justifySelf="end" justifySelf="end"
alignSelf="top" alignSelf="top"
gridRow="1/3" gridRow="1/3"
size="md" size="sm"
variant="outline" variant="outline"
onClick={ handleZoomResetClick } onClick={ handleZoomResetClick }
> >
Reset zoom Reset zoom
</Button> </Button>
) } ) }
</Flex> </Grid>
</ModalHeader> </Box>
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody <ModalBody
h="75%" h="100%"
> >
<ChartWidgetGraph <ChartWidgetGraph
isEnlarged
items={ items } items={ items }
onZoom={ handleZoom } onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial } isZoomResetInitial={ isZoomResetInitial }
......
...@@ -37,7 +37,8 @@ export default function useTimeChartController({ data, width, height }: Props) { ...@@ -37,7 +37,8 @@ export default function useTimeChartController({ data, width, height }: Props) {
); );
const yScale = useMemo(() => { const yScale = useMemo(() => {
const indention = (yMax - yMin) * 0.3; const indention = (yMax - yMin) * 0.15;
return d3.scaleLinear() return d3.scaleLinear()
.domain([ yMin >= 0 && yMin - indention <= 0 ? 0 : yMin - indention, yMax + indention ]) .domain([ yMin >= 0 && yMin - indention <= 0 ? 0 : yMin - indention, yMax + indention ])
.range([ height, 0 ]); .range([ height, 0 ]);
......
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Charts } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import type { StatsIntervalIds } from 'types/client/stats';
import useFetch from 'lib/hooks/useFetch';
import ChartWidget from '../shared/chart/ChartWidget';
import { STATS_INTERVALS } from './constants';
type Props = {
id: string;
title: string;
description: string;
interval: StatsIntervalIds;
}
function formatDate(date: Date) {
return date.toISOString().substring(0, 10);
}
const ChartWidgetContainer = ({ id, title, description, interval }: Props) => {
const fetch = useFetch();
const selectedInterval = STATS_INTERVALS[interval];
const endDate = selectedInterval.start ? formatDate(new Date()) : undefined;
const startDate = selectedInterval.start ? formatDate(selectedInterval.start) : undefined;
const url = `/node-api/stats/charts?name=${ id }${ startDate ? `&from=${ startDate }&to=${ endDate }` : '' }`;
const { data, isLoading } = useQuery<unknown, unknown, Charts>(
[ QueryKeys.charts, id, startDate ],
async() => await fetch(url),
);
const items = data?.chart
.map((item) => {
return { date: new Date(item.date), value: Number(item.value) };
});
return (
<ChartWidget
items={ items }
title={ title }
description={ description }
isLoading={ isLoading }
/>
);
};
export default ChartWidgetContainer;
...@@ -6,7 +6,7 @@ import type { StatsIntervalIds, StatsSection } from 'types/client/stats'; ...@@ -6,7 +6,7 @@ import type { StatsIntervalIds, StatsSection } from 'types/client/stats';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from '../apps/EmptySearchResult'; import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidget from './ChartWidget'; import ChartWidgetContainer from './ChartWidgetContainer';
type Props = { type Props = {
charts: Array<StatsSection>; charts: Array<StatsSection>;
...@@ -46,7 +46,7 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => { ...@@ -46,7 +46,7 @@ const ChartsWidgetsList = ({ charts, interval }: Props) => {
<GridItem <GridItem
key={ chart.id } key={ chart.id }
> >
<ChartWidget <ChartWidgetContainer
id={ chart.id } id={ chart.id }
title={ chart.title } title={ chart.title }
description={ chart.description } description={ chart.description }
......
...@@ -10,7 +10,8 @@ const NumberWidget = ({ label, value }: Props) => { ...@@ -10,7 +10,8 @@ const NumberWidget = ({ label, value }: Props) => {
return ( return (
<Box <Box
bg={ useColorModeValue('blue.50', 'blue.800') } bg={ useColorModeValue('blue.50', 'blue.800') }
p={ 3 } px={ 3 }
py={{ base: 2, lg: 3 }}
borderRadius={ 12 } borderRadius={ 12 }
> >
<Text <Text
......
...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -10,7 +10,7 @@ import useFetch from 'lib/hooks/useFetch';
import NumberWidget from './NumberWidget'; import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton'; import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 4; const skeletonsCount = 8;
const NumberWidgetsList = () => { const NumberWidgetsList = () => {
const fetch = useFetch(); const fetch = useFetch();
...@@ -28,10 +28,28 @@ const NumberWidgetsList = () => { ...@@ -28,10 +28,28 @@ const NumberWidgetsList = () => {
{ isLoading ? [ ...Array(skeletonsCount) ] { isLoading ? [ ...Array(skeletonsCount) ]
.map((e, i) => <NumberWidgetSkeleton key={ i }/>) : .map((e, i) => <NumberWidgetSkeleton key={ i }/>) :
( (
<NumberWidget <>
label="Total blocks all time" <NumberWidget
value={ Number(data?.totalBlocksAllTime).toLocaleString() } label="Total blocks"
/> value={ Number(data?.counters.totalBlocksAllTime).toLocaleString() }
/>
<NumberWidget
label="Average block time"
value={ Number(data?.counters.averageBlockTime).toLocaleString() }
/>
<NumberWidget
label="Completed transactions"
value={ Number(data?.counters.completedTransactions).toLocaleString() }
/>
<NumberWidget
label="Total transactions"
value={ Number(data?.counters.totalTransactions).toLocaleString() }
/>
<NumberWidget
label="Total accounts"
value={ Number(data?.counters.totalAccounts).toLocaleString() }
/>
</>
) } ) }
</Grid> </Grid>
); );
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import TxsTable from './TxsTable';
test('base view +@dark-mode +@desktop-xl', async({ mount }) => {
const component = await mount(
<TestApp>
{ /* eslint-disable-next-line react/jsx-no-bind */ }
<TxsTable txs={ [ txMock.base, txMock.base ] } sort={ () => () => {} } top={ 0 } showBlockInfo showSocketInfo={ false }/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
...@@ -27,8 +27,8 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr ...@@ -27,8 +27,8 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="54px"></Th> <Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th> <Th width="18%">Txn hash</Th>
<Th width="20%">Type</Th>
<Th width="15%">Method</Th> <Th width="15%">Method</Th>
{ showBlockInfo && <Th width="11%">Block</Th> } { showBlockInfo && <Th width="11%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
......
...@@ -84,13 +84,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -84,13 +84,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
) } ) }
</Popover> </Popover>
</Td> </Td>
<Td> <Td pr={ 4 }>
<VStack alignItems="start">
<TxType types={ tx.tx_types }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td>
<VStack alignItems="start" lineHeight="24px"> <VStack alignItems="start" lineHeight="24px">
<Address width="100%"> <Address width="100%">
<AddressLink <AddressLink
...@@ -102,6 +96,12 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -102,6 +96,12 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack> </VStack>
</Td> </Td>
<Td>
<VStack alignItems="start">
<TxType types={ tx.tx_types }/>
<TxStatus status={ tx.status } errorText={ tx.status === 'error' ? tx.result : undefined }/>
</VStack>
</Td>
<Td whiteSpace="nowrap"> <Td whiteSpace="nowrap">
{ tx.method ? ( { tx.method ? (
<TruncatedTextTooltip label={ tx.method }> <TruncatedTextTooltip label={ tx.method }>
......
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