Merge branch 'main' of github.com:blockscout/frontend into contract-write
@@ -28,7 +28,8 @@ blockscout:
|
||||
- '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"'
|
||||
- 'nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token,x-bs-account-csrf"'
|
||||
- 'nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token"'
|
||||
- 'nginx.ingress.kubernetes.io/cors-expose-headers: "x-bs-account-csrf"'
|
||||
host:
|
||||
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
|
||||
# enable https
|
||||
|
||||
@@ -72,14 +72,16 @@ frontend:
|
||||
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
|
||||
NEXT_PUBLIC_AUTH0_CLIENT_ID:
|
||||
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
|
||||
_default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age: []
|
||||
lastmodified: "2022-11-28T16:58:46Z"
|
||||
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
|
||||
lastmodified: "2023-01-18T20:08:13Z"
|
||||
mac: ENC[AES256_GCM,data:Qk1EyzFy/tYS5bDREC+EROmZ2mhSLNAa6ZJNByBYqMOqYTcrQhYC95I7/2qAHNnjTqNWwPSKb7LxEDSk60I3FZkXZr42gribWSoblf95KRbKsSiBhX1omE6tAK6gEpWuLuhwdLrUOdWM4fCUnwdFn7IV4y8KknhbSZ/yE0kNEnY=,iv:yc4E7yGZ8ftVU3XN2VhwhvjwtSbpXLWFgAG897TbrLk=,tag:SZrlReFY1cN9u54hsgJBKQ==,type:str]
|
||||
pgp:
|
||||
- created_at: "2022-09-14T13:42:28Z"
|
||||
enc: |
|
||||
|
||||
@@ -2,31 +2,19 @@ import { compile } from 'path-to-regexp';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
|
||||
import isNeedProxy from './isNeedProxy';
|
||||
import { RESOURCES } from './resources';
|
||||
import type { ApiResource, ResourceName } from './resources';
|
||||
|
||||
export default function buildUrl(
|
||||
_resource: ApiResource | ResourceName,
|
||||
pathParams?: Record<string, string | undefined>,
|
||||
queryParams?: Record<string, string | number | undefined>,
|
||||
queryParams?: Record<string, string | Array<string> | number | undefined>,
|
||||
) {
|
||||
// FIXME
|
||||
// 1. I was not able to figure out how to send CORS with credentials from localhost
|
||||
// unsuccessfully tried different ways, even custom local dev domain
|
||||
// so for local development we have to use next.js api as proxy server
|
||||
// 2. and there is an issue with API and csrf token
|
||||
// for some reason API will reply with error "Bad request" to any PUT / POST CORS request
|
||||
// even though valid csrf-token is passed in header
|
||||
// we also can pass token in request body but in this case API will replay with "Forbidden" error
|
||||
// @nikitosing said it will take a lot of time to debug this problem on back-end side, maybe he'll change his mind in future :)
|
||||
// To sum up, we are using next.js proxy for all instances where app host is not the same as API host (incl. localhost)
|
||||
// will need to change the condition if there are more micro services that need authentication and DB state changes
|
||||
const needProxy = appConfig.host !== appConfig.api.host;
|
||||
|
||||
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
|
||||
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
|
||||
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
|
||||
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
|
||||
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
|
||||
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
|
||||
const url = new URL(compile(path)(pathParams), baseUrl);
|
||||
|
||||
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import appConfig from 'configs/app/config';
|
||||
|
||||
// FIXME
|
||||
// I was not able to figure out how to send CORS with credentials from localhost
|
||||
// unsuccessfully tried different ways, even custom local dev domain
|
||||
// so for local development we have to use next.js api as proxy server
|
||||
export default function isNeedProxy() {
|
||||
return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host;
|
||||
}
|
||||
@@ -24,8 +24,9 @@ export default function fetchFactory(
|
||||
});
|
||||
|
||||
return nodeFetch(url, {
|
||||
headers,
|
||||
...init,
|
||||
headers,
|
||||
body: init?.body ? JSON.stringify(init.body) : undefined,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { ApiResource } from './resources';
|
||||
|
||||
export interface Params {
|
||||
pathParams?: Record<string, string | undefined>;
|
||||
queryParams?: Record<string, string | number | undefined>;
|
||||
queryParams?: Record<string, string | Array<string> | number | undefined>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ export default function useFetch() {
|
||||
headers: {
|
||||
...(hasBody ? { 'Content-type': 'application/json' } : undefined),
|
||||
...params?.headers,
|
||||
// ...(token ? { 'x-csrf-token': token } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import buildUrl from 'lib/api/buildUrl';
|
||||
import isNeedProxy from 'lib/api/isNeedProxy';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
|
||||
export default function useGetCsrfToken() {
|
||||
const nodeApiFetch = useFetch();
|
||||
|
||||
useQuery(getResourceKey('csrf'), async() => {
|
||||
if (!isNeedProxy()) {
|
||||
const url = buildUrl('csrf');
|
||||
const apiResponse = await fetch(url, { credentials: 'include' });
|
||||
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('>>> RESPONSE HEADERS <<<');
|
||||
// eslint-disable-next-line no-console
|
||||
console.table([ {
|
||||
'content-length': apiResponse.headers.get('content-length'),
|
||||
'x-bs-account-csrf': csrfFromHeader,
|
||||
} ]);
|
||||
|
||||
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
|
||||
}
|
||||
|
||||
return nodeApiFetch('/node-api/csrf');
|
||||
}, {
|
||||
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,8 @@ function getSocketParams(router: NextRouter) {
|
||||
if (
|
||||
router.pathname === ROUTES.txs.pattern &&
|
||||
(router.query.tab === 'validated' || router.query.tab === undefined) &&
|
||||
!router.query.block_number
|
||||
!router.query.block_number &&
|
||||
!router.query.page
|
||||
) {
|
||||
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
|
||||
}
|
||||
@@ -21,7 +22,12 @@ function getSocketParams(router: NextRouter) {
|
||||
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
|
||||
}
|
||||
|
||||
if (router.pathname === ROUTES.txs.pattern && router.query.tab === 'pending' && !router.query.block_number) {
|
||||
if (
|
||||
router.pathname === ROUTES.txs.pattern &&
|
||||
router.query.tab === 'pending' &&
|
||||
!router.query.block_number &&
|
||||
!router.query.page
|
||||
) {
|
||||
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { Channel } from 'phoenix';
|
||||
|
||||
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
|
||||
import type { NewBlockSocketResponse } from 'types/api/block';
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
export type SocketMessageParams = SocketMessage.NewBlock |
|
||||
SocketMessage.BlocksIndexStatus |
|
||||
@@ -13,6 +15,9 @@ SocketMessage.AddressBalance |
|
||||
SocketMessage.AddressCurrentCoinBalance |
|
||||
SocketMessage.AddressTokenBalance |
|
||||
SocketMessage.AddressCoinBalance |
|
||||
SocketMessage.AddressTxs |
|
||||
SocketMessage.AddressTxsPending |
|
||||
SocketMessage.AddressTokenTransfer |
|
||||
SocketMessage.Unknown;
|
||||
|
||||
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
|
||||
@@ -34,5 +39,8 @@ export namespace SocketMessage {
|
||||
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
|
||||
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
|
||||
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
|
||||
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
|
||||
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
|
||||
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
|
||||
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ export const erc20: TokenTransfer = {
|
||||
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
|
||||
type: 'token_transfer',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
};
|
||||
|
||||
export const erc721: TokenTransfer = {
|
||||
@@ -77,6 +79,8 @@ export const erc721: TokenTransfer = {
|
||||
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
|
||||
type: 'token_transfer',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
};
|
||||
|
||||
export const erc1155: TokenTransfer = {
|
||||
@@ -118,6 +122,8 @@ export const erc1155: TokenTransfer = {
|
||||
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
|
||||
type: 'token_minting',
|
||||
timestamp: '2022-10-10T14:34:30.000000Z',
|
||||
block_hash: '1',
|
||||
log_index: '1',
|
||||
};
|
||||
|
||||
export const erc1155multiple: TokenTransfer = {
|
||||
|
||||
@@ -37,6 +37,8 @@ interface TokenTransferBase {
|
||||
from: AddressParam;
|
||||
to: AddressParam;
|
||||
timestamp: string;
|
||||
block_hash: string;
|
||||
log_index: string;
|
||||
}
|
||||
|
||||
export type TokenTransferPagination = {
|
||||
|
||||
@@ -1,20 +1,227 @@
|
||||
import { Hide, Show, Text } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import { AddressFromToFilterValues } from 'types/api/address';
|
||||
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
|
||||
import type { TokenType } from 'types/api/tokenInfo';
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
|
||||
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
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 SkeletonList from 'ui/shared/skeletons/SkeletonList';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers';
|
||||
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
|
||||
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
|
||||
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
|
||||
|
||||
type Filters = {
|
||||
type: Array<TokenType>;
|
||||
filter: AddressFromToFilter | undefined;
|
||||
}
|
||||
|
||||
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
|
||||
|
||||
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
|
||||
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
|
||||
|
||||
const OVERLOAD_COUNT = 75;
|
||||
|
||||
const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: string) => {
|
||||
if (filters.filter) {
|
||||
if (filters.filter === 'from' && tokenTransfer.from.hash !== address) {
|
||||
return false;
|
||||
}
|
||||
if (filters.filter === 'to' && tokenTransfer.to.hash !== address) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (filters.type && filters.type.length) {
|
||||
if (!filters.type.includes(tokenTransfer.token.type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const currentAddress = router.query.id?.toString();
|
||||
|
||||
const [ socketAlert, setSocketAlert ] = React.useState('');
|
||||
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
|
||||
|
||||
const [ filters, setFilters ] = React.useState<Filters>(
|
||||
{ type: getTokenFilterValue(router.query.type) || [], filter: getAddressFilterValue(router.query.filter) },
|
||||
);
|
||||
|
||||
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
|
||||
resourceName: 'address_token_transfers',
|
||||
pathParams: { id: currentAddress },
|
||||
filters: filters,
|
||||
scrollRef,
|
||||
});
|
||||
|
||||
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
|
||||
onFilterChange({ ...filters, type: nextValue });
|
||||
setFilters((prevState) => ({ ...prevState, type: nextValue }));
|
||||
}, [ filters, onFilterChange ]);
|
||||
|
||||
const handleAddressFilterChange = React.useCallback((nextValue: string) => {
|
||||
const filterVal = getAddressFilterValue(nextValue);
|
||||
onFilterChange({ ...filters, filter: filterVal });
|
||||
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
|
||||
}, [ filters, onFilterChange ]);
|
||||
|
||||
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
|
||||
setSocketAlert('');
|
||||
|
||||
if (data?.items && data.items.length >= OVERLOAD_COUNT) {
|
||||
if (matchFilters(filters, payload.token_transfer, currentAddress)) {
|
||||
setNewItemsCount(prev => prev + 1);
|
||||
}
|
||||
} else {
|
||||
queryClient.setQueryData(
|
||||
getResourceKey('address_token_transfers', { pathParams: { id: router.query.id?.toString() }, queryParams: { ...filters } }),
|
||||
(prevData: AddressTokenTransferResponse | undefined) => {
|
||||
if (!prevData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchFilters(filters, payload.token_transfer, currentAddress)) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
items: [
|
||||
payload.token_transfer,
|
||||
...prevData.items,
|
||||
],
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSocketClose = React.useCallback(() => {
|
||||
setSocketAlert('Connection is lost. Please refresh the page to load new token transfers.');
|
||||
}, []);
|
||||
|
||||
const handleSocketError = React.useCallback(() => {
|
||||
setSocketAlert('An error has occurred while fetching new token transfers. Please refresh the page.');
|
||||
}, []);
|
||||
|
||||
const channel = useSocketChannel({
|
||||
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
|
||||
onSocketClose: handleSocketClose,
|
||||
onSocketError: handleSocketError,
|
||||
isDisabled: pagination.page !== 1,
|
||||
});
|
||||
|
||||
useSocketMessage({
|
||||
channel,
|
||||
event: 'token_transfer',
|
||||
handler: handleNewSocketMessage,
|
||||
});
|
||||
|
||||
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
|
||||
const isActionBarHidden = !numActiveFilters && !data?.items.length;
|
||||
|
||||
const content = (() => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg">
|
||||
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] }/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
<SkeletonList/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!data.items?.length && !numActiveFilters) {
|
||||
return <Text as="span">There are no token transfers</Text>;
|
||||
}
|
||||
|
||||
if (!data.items?.length) {
|
||||
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
|
||||
}
|
||||
|
||||
const items = data.items.reduce(flattenTotal, []);
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg">
|
||||
<TokenTransferTable
|
||||
data={ items }
|
||||
baseAddress={ currentAddress }
|
||||
showTxInfo
|
||||
top={ 80 }
|
||||
enableTimeIncrement
|
||||
showSocketInfo={ pagination.page === 1 }
|
||||
socketInfoAlert={ socketAlert }
|
||||
socketInfoNum={ newItemsCount }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
{ pagination.page === 1 && (
|
||||
<SocketNewItemsNotice
|
||||
url={ window.location.href }
|
||||
num={ newItemsCount }
|
||||
alert={ socketAlert }
|
||||
type="token_transfer"
|
||||
borderBottomRadius={ 0 }
|
||||
/>
|
||||
) }
|
||||
<TokenTransferList
|
||||
data={ items }
|
||||
baseAddress={ currentAddress }
|
||||
showTxInfo
|
||||
enableTimeIncrement
|
||||
/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
})();
|
||||
|
||||
const hash = router.query.id;
|
||||
return (
|
||||
<TokenTransfer
|
||||
resourceName="address_token_transfers"
|
||||
pathParams={{ id: hash?.toString() }}
|
||||
baseAddress={ typeof hash === 'string' ? hash : undefined }
|
||||
enableTimeIncrement
|
||||
scrollRef={ scrollRef }
|
||||
/>
|
||||
<>
|
||||
{ !isActionBarHidden && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<TokenTransferFilter
|
||||
defaultTypeFilters={ filters.type }
|
||||
onTypeFilterChange={ handleTypeFilterChange }
|
||||
appliedFiltersNum={ numActiveFilters }
|
||||
withAddressFilter
|
||||
onAddressFilterChange={ handleAddressFilterChange }
|
||||
defaultAddressFilter={ filters.filter }
|
||||
/>
|
||||
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
|
||||
</ActionBar>
|
||||
) }
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,23 +1,34 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import castArray from 'lodash/castArray';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressFromToFilter } from 'types/api/address';
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
|
||||
import { AddressFromToFilterValues } from 'types/api/address';
|
||||
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import useSocketChannel from 'lib/socket/useSocketChannel';
|
||||
import useSocketMessage from 'lib/socket/useSocketMessage';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import TxsContent from 'ui/txs/TxsContent';
|
||||
|
||||
import AddressTxsFilter from './AddressTxsFilter';
|
||||
|
||||
const OVERLOAD_COUNT = 75;
|
||||
|
||||
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
|
||||
|
||||
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [ socketAlert, setSocketAlert ] = React.useState('');
|
||||
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -37,6 +48,85 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
|
||||
addressTxsQuery.onFilterChange({ filter: newVal });
|
||||
}, [ addressTxsQuery ]);
|
||||
|
||||
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
|
||||
setSocketAlert('');
|
||||
|
||||
const currentAddress = router.query.id?.toString();
|
||||
|
||||
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
|
||||
if (
|
||||
!filterValue ||
|
||||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
|
||||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
|
||||
) {
|
||||
setNewItemsCount(prev => prev + 1);
|
||||
}
|
||||
}
|
||||
|
||||
queryClient.setQueryData(
|
||||
getResourceKey('address_txs', { pathParams: { id: router.query.id?.toString() }, queryParams: { filter: filterValue } }),
|
||||
(prevData: AddressTransactionsResponse | undefined) => {
|
||||
if (!prevData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash);
|
||||
|
||||
if (currIndex > -1) {
|
||||
prevData.items[currIndex] = payload.transaction;
|
||||
return prevData;
|
||||
}
|
||||
|
||||
if (prevData.items.length >= OVERLOAD_COUNT) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
if (filterValue) {
|
||||
if (
|
||||
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) ||
|
||||
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress)
|
||||
) {
|
||||
return prevData;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
items: [
|
||||
payload.transaction,
|
||||
...prevData.items,
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSocketClose = React.useCallback(() => {
|
||||
setSocketAlert('Connection is lost. Please refresh the page to load new transactions.');
|
||||
}, []);
|
||||
|
||||
const handleSocketError = React.useCallback(() => {
|
||||
setSocketAlert('An error has occurred while fetching new transactions. Please refresh the page.');
|
||||
}, []);
|
||||
|
||||
const channel = useSocketChannel({
|
||||
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
|
||||
onSocketClose: handleSocketClose,
|
||||
onSocketError: handleSocketError,
|
||||
isDisabled: addressTxsQuery.pagination.page !== 1,
|
||||
});
|
||||
|
||||
useSocketMessage({
|
||||
channel,
|
||||
event: 'transaction',
|
||||
handler: handleNewSocketMessage,
|
||||
});
|
||||
|
||||
useSocketMessage({
|
||||
channel,
|
||||
event: 'pending_transaction',
|
||||
handler: handleNewSocketMessage,
|
||||
});
|
||||
|
||||
const filter = (
|
||||
<AddressTxsFilter
|
||||
defaultFilter={ filterValue }
|
||||
@@ -56,9 +146,11 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
|
||||
<TxsContent
|
||||
filter={ filter }
|
||||
query={ addressTxsQuery }
|
||||
showSocketInfo={ false }
|
||||
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
|
||||
enableTimeIncrement
|
||||
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
|
||||
socketInfoAlert={ socketAlert }
|
||||
socketInfoNum={ newItemsCount }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB |
@@ -3,8 +3,9 @@ import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
|
||||
import link from 'lib/link/link';
|
||||
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
|
||||
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
|
||||
import LatestTxsItem from './LatestTxsItem';
|
||||
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
|
||||
@@ -14,6 +15,8 @@ const LatestTransactions = () => {
|
||||
const txsCount = isMobile ? 2 : 6;
|
||||
const { data, isLoading, isError } = useApiQuery('homepage_txs');
|
||||
|
||||
const { num, socketAlert } = useNewTxsSocket();
|
||||
|
||||
let content;
|
||||
|
||||
if (isLoading) {
|
||||
@@ -33,7 +36,7 @@ const LatestTransactions = () => {
|
||||
const txsUrl = link('txs');
|
||||
content = (
|
||||
<>
|
||||
<TxsNewItemNotice borderBottomRadius={ 0 } url={ link('txs') }/>
|
||||
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ link('txs') } num={ num } alert={ socketAlert }/>
|
||||
<Box mb={{ base: 3, lg: 4 }}>
|
||||
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
|
||||
</Box>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
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 PageTitle from 'ui/shared/Page/PageTitle';
|
||||
@@ -29,9 +30,20 @@ const Transactions = () => {
|
||||
filters: { filter },
|
||||
});
|
||||
|
||||
const { num, socketAlert } = useNewTxsSocket();
|
||||
|
||||
const isFirstPage = txsQuery.pagination.page === 1;
|
||||
|
||||
const tabs: Array<RoutedTab> = [
|
||||
{ id: 'validated', title: verifiedTitle, component: <TxsContent query={ txsQuery }/> },
|
||||
{ id: 'pending', title: 'Pending', component: <TxsContent query={ txsQuery } showBlockInfo={ false }/> },
|
||||
{
|
||||
id: 'validated',
|
||||
title: verifiedTitle,
|
||||
component: <TxsContent query={ txsQuery } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
|
||||
{
|
||||
id: 'pending',
|
||||
title: 'Pending',
|
||||
component: <TxsContent query={ txsQuery } showBlockInfo={ false } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import buildUrl from 'lib/api/buildUrl';
|
||||
import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
|
||||
import AppError from 'ui/shared/AppError/AppError';
|
||||
import ErrorBoundary from 'ui/shared/ErrorBoundary';
|
||||
import ErrorInvalidTxHash from 'ui/shared/ErrorInvalidTxHash';
|
||||
@@ -27,28 +22,8 @@ const Page = ({
|
||||
isHomePage,
|
||||
renderHeader,
|
||||
}: Props) => {
|
||||
const nodeApiFetch = useFetch();
|
||||
|
||||
useQuery(getResourceKey('csrf'), async() => {
|
||||
if (appConfig.host === appConfig.api.host) {
|
||||
const url = buildUrl('csrf');
|
||||
const apiResponse = await fetch(url, { credentials: 'include' });
|
||||
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('>>> RESPONSE HEADERS <<<');
|
||||
// eslint-disable-next-line no-console
|
||||
console.table([ {
|
||||
'content-length': apiResponse.headers.get('content-length'),
|
||||
'x-bs-account-csrf': csrfFromHeader,
|
||||
} ]);
|
||||
|
||||
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
|
||||
}
|
||||
|
||||
return nodeApiFetch('/node-api/csrf');
|
||||
}, {
|
||||
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
|
||||
});
|
||||
useGetCsrfToken();
|
||||
|
||||
const renderErrorScreen = React.useCallback((error?: Error) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import { ROUTES } from 'lib/link/routes';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import SocketNewItemsNotice from './SocketNewItemsNotice';
|
||||
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
pathname: ROUTES.txs.pattern,
|
||||
query: {},
|
||||
},
|
||||
};
|
||||
|
||||
test('2 new items in validated txs list +@dark-mode', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<SocketNewItemsNotice url="/" num={ 2 }/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('connection loss', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<SocketNewItemsNotice url="/" alert="Connection is lost. Please reload the page."/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('fetching', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<SocketNewItemsNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -2,34 +2,36 @@ import { Alert, Link, Text, chakra, useTheme, useColorModeValue } from '@chakra-
|
||||
import { transparentize } from '@chakra-ui/theme-tools';
|
||||
import React from 'react';
|
||||
|
||||
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
|
||||
|
||||
interface InjectedProps {
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
type?: 'transaction' | 'token_transfer';
|
||||
children?: (props: InjectedProps) => JSX.Element;
|
||||
className?: string;
|
||||
url: string;
|
||||
alert?: string;
|
||||
num?: number;
|
||||
}
|
||||
|
||||
const TxsNewItemNotice = ({ children, className, url }: Props) => {
|
||||
const { num, socketAlert } = useNewTxsSocket();
|
||||
const SocketNewItemsNotice = ({ children, className, url, num, alert, type = 'transaction' }: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const alertContent = (() => {
|
||||
if (socketAlert) {
|
||||
return socketAlert;
|
||||
if (alert) {
|
||||
return alert;
|
||||
}
|
||||
|
||||
const name = type === 'token_transfer' ? 'token transfer' : 'transaction';
|
||||
|
||||
if (!num) {
|
||||
return 'scanning new transactions...';
|
||||
return `scanning new ${ name }s...`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link href={ url }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
|
||||
<Link href={ url }>{ num } more { name }{ num > 1 ? 's' : '' }</Link>
|
||||
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
|
||||
</>
|
||||
);
|
||||
@@ -53,4 +55,4 @@ const TxsNewItemNotice = ({ children, className, url }: Props) => {
|
||||
return children ? children({ content }) : content;
|
||||
};
|
||||
|
||||
export default chakra(TxsNewItemNotice);
|
||||
export default chakra(SocketNewItemsNotice);
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
|
||||
import TokenTransfer from './TokenTransfer';
|
||||
|
||||
const API_URL = buildApiUrl('tx_token_transfers', { id: '1' });
|
||||
|
||||
test('without tx info +@mobile', async({ mount, page }) => {
|
||||
await page.route(API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(tokenTransferMock.mixTokens),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransfer
|
||||
resourceName="tx_token_transfers"
|
||||
pathParams={{ id: '1' }}
|
||||
showTxInfo={ false }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with tx info +@mobile', async({ mount, page }) => {
|
||||
await page.route(API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(tokenTransferMock.mixTokens),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransfer
|
||||
resourceName="tx_token_transfers"
|
||||
pathParams={{ id: '1' }}
|
||||
showTxInfo={ true }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,149 +0,0 @@
|
||||
import { Hide, Show, Text } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressFromToFilter } from 'types/api/address';
|
||||
import { AddressFromToFilterValues } from 'types/api/address';
|
||||
import type { TokenType } from 'types/api/tokenInfo';
|
||||
|
||||
import type { PaginationFilters } from 'lib/api/resources';
|
||||
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
|
||||
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
|
||||
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { apos } from 'lib/html-entities';
|
||||
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 SkeletonList from 'ui/shared/skeletons/SkeletonList';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
|
||||
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
|
||||
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
|
||||
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
|
||||
|
||||
import { TOKEN_TYPE } from './helpers';
|
||||
|
||||
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
|
||||
|
||||
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
|
||||
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
|
||||
|
||||
interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers'> {
|
||||
isLoading?: boolean;
|
||||
isDisabled?: boolean;
|
||||
resourceName: Resource;
|
||||
baseAddress?: string;
|
||||
showTxInfo?: boolean;
|
||||
txHash?: string;
|
||||
enableTimeIncrement?: boolean;
|
||||
pathParams?: UseApiQueryParams<Resource>['pathParams'];
|
||||
scrollRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
type State = {
|
||||
type: Array<TokenType> | undefined;
|
||||
filter: AddressFromToFilter;
|
||||
}
|
||||
|
||||
const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_transfers'>({
|
||||
isLoading: isLoadingProp,
|
||||
isDisabled,
|
||||
resourceName,
|
||||
baseAddress,
|
||||
showTxInfo = true,
|
||||
enableTimeIncrement,
|
||||
pathParams,
|
||||
scrollRef,
|
||||
}: Props<Resource>) => {
|
||||
const router = useRouter();
|
||||
const [ filters, setFilters ] = React.useState<State>(
|
||||
{ type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
|
||||
);
|
||||
|
||||
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
|
||||
resourceName,
|
||||
pathParams,
|
||||
options: { enabled: !isDisabled },
|
||||
filters: filters as PaginationFilters<Resource>,
|
||||
scrollRef,
|
||||
});
|
||||
|
||||
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
|
||||
onFilterChange({ ...filters, type: nextValue } as PaginationFilters<Resource>);
|
||||
setFilters((prevState) => ({ ...prevState, type: nextValue }));
|
||||
}, [ filters, onFilterChange ]);
|
||||
|
||||
const handleAddressFilterChange = React.useCallback((nextValue: string) => {
|
||||
const filterVal = getAddressFilterValue(nextValue);
|
||||
onFilterChange({ ...filters, filter: filterVal } as PaginationFilters<Resource>);
|
||||
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
|
||||
}, [ filters, onFilterChange ]);
|
||||
|
||||
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
|
||||
const isActionBarHidden = !numActiveFilters && !data?.items.length;
|
||||
|
||||
const content = (() => {
|
||||
if (isLoading || isLoadingProp) {
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg">
|
||||
<SkeletonTable columns={ showTxInfo ?
|
||||
[ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] :
|
||||
[ '185px', '25%', '25%', '25%', '25%' ] }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
<SkeletonList/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
if (!data.items?.length && !numActiveFilters) {
|
||||
return <Text as="span">There are no token transfers</Text>;
|
||||
}
|
||||
|
||||
if (!data.items?.length) {
|
||||
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
|
||||
}
|
||||
|
||||
const items = data.items.reduce(flattenTotal, []);
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg">
|
||||
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 } enableTimeIncrement={ enableTimeIncrement }/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
{ !isActionBarHidden && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<TokenTransferFilter
|
||||
defaultTypeFilters={ filters.type }
|
||||
onTypeFilterChange={ handleTypeFilterChange }
|
||||
appliedFiltersNum={ numActiveFilters }
|
||||
withAddressFilter={ Boolean(baseAddress) }
|
||||
onAddressFilterChange={ handleAddressFilterChange }
|
||||
defaultAddressFilter={ filters.filter }
|
||||
/>
|
||||
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
|
||||
</ActionBar>
|
||||
) }
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TokenTransfer);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { test, expect, devices } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import { flattenTotal } from './helpers';
|
||||
import TokenTransferList from './TokenTransferList';
|
||||
|
||||
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
|
||||
|
||||
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
|
||||
|
||||
test('without tx info', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransferList
|
||||
data={ flattenData }
|
||||
showTxInfo={ false }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with tx info', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransferList
|
||||
data={ flattenData }
|
||||
showTxInfo={ true }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -15,9 +15,9 @@ interface Props {
|
||||
const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
{ data.map((item, index) => (
|
||||
{ data.map((item) => (
|
||||
<TokenTransferListItem
|
||||
key={ index }
|
||||
key={ item.tx_hash + item.block_hash + item.log_index }
|
||||
{ ...item }
|
||||
baseAddress={ baseAddress }
|
||||
showTxInfo={ showTxInfo }
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import { flattenTotal } from './helpers';
|
||||
import TokenTransferTable from './TokenTransferTable';
|
||||
|
||||
const flattenData = tokenTransferMock.mixTokens.items.reduce(flattenTotal, []);
|
||||
|
||||
test('without tx info', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransferTable
|
||||
data={ flattenData }
|
||||
top={ 0 }
|
||||
showTxInfo={ false }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with tx info', async({ mount }) => {
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Box h={{ base: '134px', lg: 6 }}/>
|
||||
<TokenTransferTable
|
||||
data={ flattenData }
|
||||
top={ 0 }
|
||||
showTxInfo={ true }
|
||||
/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
|
||||
import { Table, Tbody, Tr, Th, Td } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenTransfer } from 'types/api/tokenTransfer';
|
||||
|
||||
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
|
||||
|
||||
@@ -12,9 +13,21 @@ interface Props {
|
||||
showTxInfo?: boolean;
|
||||
top: number;
|
||||
enableTimeIncrement?: boolean;
|
||||
showSocketInfo?: boolean;
|
||||
socketInfoAlert?: string;
|
||||
socketInfoNum?: number;
|
||||
}
|
||||
|
||||
const TokenTransferTable = ({ data, baseAddress, showTxInfo, top, enableTimeIncrement }: Props) => {
|
||||
const TokenTransferTable = ({
|
||||
data,
|
||||
baseAddress,
|
||||
showTxInfo,
|
||||
top,
|
||||
enableTimeIncrement,
|
||||
showSocketInfo,
|
||||
socketInfoAlert,
|
||||
socketInfoNum,
|
||||
}: Props) => {
|
||||
|
||||
return (
|
||||
<Table variant="simple" size="sm">
|
||||
@@ -31,8 +44,28 @@ const TokenTransferTable = ({ data, baseAddress, showTxInfo, top, enableTimeIncr
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ data.map((item, index) => (
|
||||
<TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
|
||||
{ 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>
|
||||
) }
|
||||
{ data.map((item) => (
|
||||
<TokenTransferTableItem
|
||||
key={ item.tx_hash + item.block_hash + item.log_index }
|
||||
{ ...item }
|
||||
baseAddress={ baseAddress }
|
||||
showTxInfo={ showTxInfo }
|
||||
enableTimeIncrement={ enableTimeIncrement }
|
||||
/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -1,32 +1,111 @@
|
||||
import { Hide, Show, Text } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenType } from 'types/api/tokenInfo';
|
||||
|
||||
import { SECOND } from 'lib/consts';
|
||||
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import EmptySearchResult from 'ui/apps/EmptySearchResult';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
import { TOKEN_TYPE, flattenTotal } from 'ui/shared/TokenTransfer/helpers';
|
||||
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
|
||||
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
|
||||
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
|
||||
import TxPendingAlert from 'ui/tx/TxPendingAlert';
|
||||
import TxSocketAlert from 'ui/tx/TxSocketAlert';
|
||||
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
|
||||
|
||||
const TxTokenTransfer = () => {
|
||||
const { isError, isLoading, data, socketStatus } = useFetchTxInfo({ updateDelay: 5 * SECOND });
|
||||
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
|
||||
|
||||
if (!isLoading && !isError && !data.status) {
|
||||
return socketStatus ? <TxSocketAlert status={ socketStatus }/> : <TxPendingAlert/>;
|
||||
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
|
||||
|
||||
const TxTokenTransfer = () => {
|
||||
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []);
|
||||
|
||||
const tokenTransferQuery = useQueryWithPages({
|
||||
resourceName: 'tx_token_transfers',
|
||||
pathParams: { id: txsInfo.data?.hash.toString() },
|
||||
options: { enabled: Boolean(txsInfo.data?.status && txsInfo.data?.hash) },
|
||||
filters: { type: typeFilter },
|
||||
});
|
||||
|
||||
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
|
||||
tokenTransferQuery.onFilterChange({ type: nextValue });
|
||||
setTypeFilter(nextValue);
|
||||
}, [ tokenTransferQuery ]);
|
||||
|
||||
if (!txsInfo.isLoading && !txsInfo.isError && !txsInfo.data.status) {
|
||||
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (txsInfo.isError || tokenTransferQuery.isError) {
|
||||
return <DataFetchAlert/>;
|
||||
}
|
||||
|
||||
const numActiveFilters = typeFilter.length;
|
||||
const isActionBarHidden = !numActiveFilters && !tokenTransferQuery.data?.items.length;
|
||||
|
||||
const content = (() => {
|
||||
if (txsInfo.isLoading || tokenTransferQuery.isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<SkeletonTable columns={ [ '185px', '25%', '25%', '25%', '25%' ] }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<SkeletonList/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!tokenTransferQuery.data.items?.length && !numActiveFilters) {
|
||||
return <Text as="span">There are no token transfers</Text>;
|
||||
}
|
||||
|
||||
if (!tokenTransferQuery.data.items?.length) {
|
||||
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
|
||||
}
|
||||
|
||||
const items = tokenTransferQuery.data.items.reduce(flattenTotal, []);
|
||||
return (
|
||||
<>
|
||||
<Hide below="lg">
|
||||
<TokenTransferTable data={ items } top={ 80 }/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
<TokenTransferList data={ items }/>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<TokenTransfer
|
||||
isLoading={ isLoading }
|
||||
isDisabled={ !data?.status || !data?.hash }
|
||||
resourceName="tx_token_transfers"
|
||||
pathParams={{ id: data?.hash.toString() }}
|
||||
showTxInfo={ false }
|
||||
txHash={ data?.hash || '' }
|
||||
/>
|
||||
<>
|
||||
{ !isActionBarHidden && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<TokenTransferFilter
|
||||
defaultTypeFilters={ typeFilter }
|
||||
onTypeFilterChange={ handleTypeFilterChange }
|
||||
appliedFiltersNum={ numActiveFilters }
|
||||
/>
|
||||
{ tokenTransferQuery.isPaginationVisible && <Pagination ml="auto" { ...tokenTransferQuery.pagination }/> }
|
||||
</ActionBar>
|
||||
) }
|
||||
{ content }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
|
||||
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
|
||||
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
|
||||
|
||||
import TxsHeaderMobile from './TxsHeaderMobile';
|
||||
import TxsListItem from './TxsListItem';
|
||||
import TxsNewItemNotice from './TxsNewItemNotice';
|
||||
import TxsTable from './TxsTable';
|
||||
import useTxsSort from './useTxsSort';
|
||||
|
||||
@@ -25,12 +25,23 @@ type Props = {
|
||||
query: QueryResult;
|
||||
showBlockInfo?: boolean;
|
||||
showSocketInfo?: boolean;
|
||||
socketInfoAlert?: string;
|
||||
socketInfoNum?: number;
|
||||
currentAddress?: string;
|
||||
filter?: React.ReactNode;
|
||||
enableTimeIncrement?: boolean;
|
||||
}
|
||||
|
||||
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
|
||||
const TxsContent = ({
|
||||
filter,
|
||||
query,
|
||||
showBlockInfo = true,
|
||||
showSocketInfo = true,
|
||||
socketInfoAlert,
|
||||
socketInfoNum,
|
||||
currentAddress,
|
||||
enableTimeIncrement,
|
||||
}: Props) => {
|
||||
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
@@ -64,9 +75,14 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
|
||||
<Show below="lg" ssr={ false }>
|
||||
<Box>
|
||||
{ showSocketInfo && (
|
||||
<TxsNewItemNotice url={ window.location.href }>
|
||||
<SocketNewItemsNotice
|
||||
url={ window.location.href }
|
||||
num={ socketInfoNum }
|
||||
alert={ socketInfoAlert }
|
||||
borderBottomRadius={ 0 }
|
||||
>
|
||||
{ ({ content }) => <Box>{ content }</Box> }
|
||||
</TxsNewItemNotice>
|
||||
</SocketNewItemsNotice>
|
||||
) }
|
||||
{ txs.map(tx => (
|
||||
<TxsListItem
|
||||
@@ -86,6 +102,8 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
|
||||
sorting={ sorting }
|
||||
showBlockInfo={ showBlockInfo }
|
||||
showSocketInfo={ showSocketInfo }
|
||||
socketInfoAlert={ socketInfoAlert }
|
||||
socketInfoNum={ socketInfoNum }
|
||||
top={ query.isPaginationVisible ? 80 : 0 }
|
||||
currentAddress={ currentAddress }
|
||||
enableTimeIncrement={ enableTimeIncrement }
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 ListItemMobile from 'ui/shared/ListItemMobile';
|
||||
import TxStatus from 'ui/shared/TxStatus';
|
||||
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
|
||||
import TxType from 'ui/txs/TxType';
|
||||
@@ -43,7 +44,6 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const iconColor = useColorModeValue('blue.600', 'blue.300');
|
||||
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
|
||||
const dataTo = tx.to ? tx.to : tx.created_contract;
|
||||
|
||||
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
|
||||
@@ -53,7 +53,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
|
||||
<ListItemMobile display="block" width="100%" isAnimated key={ tx.hash }>
|
||||
<Flex justifyContent="space-between" mt={ 4 }>
|
||||
<HStack>
|
||||
<TxType types={ tx.tx_types }/>
|
||||
@@ -139,7 +139,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
|
||||
<Text as="span">Fee { appConfig.network.currency.symbol } </Text>
|
||||
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</ListItemMobile>
|
||||
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
|
||||
<ModalContent paddingTop={ 4 }>
|
||||
<ModalCloseButton/>
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { test as base, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import { ROUTES } from 'lib/link/routes';
|
||||
import * as socketServer from 'playwright/fixtures/socketServer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import TxsNewItemNotice from './TxsNewItemNotice';
|
||||
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
pathname: ROUTES.txs.pattern,
|
||||
query: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const test = base.extend<socketServer.SocketServerFixture>({
|
||||
createSocket: socketServer.createSocket,
|
||||
});
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('new item in validated txs list', async({ mount, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<TxsNewItemNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
const socket = await createSocket();
|
||||
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
|
||||
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test.describe('dark mode', () => {
|
||||
test.use({ colorScheme: 'dark' });
|
||||
|
||||
test('default view', async({ mount, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<TxsNewItemNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
const socket = await createSocket();
|
||||
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
|
||||
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('2 new items in validated txs list', async({ mount, page, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<TxsNewItemNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
const socket = await createSocket();
|
||||
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
|
||||
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
|
||||
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
|
||||
|
||||
await page.waitForSelector('text=2 more');
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('connection loss', async({ mount, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<TxsNewItemNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
const socket = await createSocket();
|
||||
await socketServer.joinChannel(socket, 'transactions:new_transaction');
|
||||
socket.close();
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('fetching', async({ mount, createSocket }) => {
|
||||
const component = await mount(
|
||||
<TestApp withSocket>
|
||||
<TxsNewItemNotice url="/"/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
const socket = await createSocket();
|
||||
await socketServer.joinChannel(socket, 'transactions:new_transaction');
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, Table, Tbody, Tr, Th, Td, Icon } from '@chakra-ui/react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
@@ -6,9 +7,9 @@ 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 TheadSticky from 'ui/shared/TheadSticky';
|
||||
|
||||
import TxsNewItemNotice from './TxsNewItemNotice';
|
||||
import TxsTableItem from './TxsTableItem';
|
||||
|
||||
type Props = {
|
||||
@@ -18,11 +19,24 @@ type Props = {
|
||||
top: number;
|
||||
showBlockInfo: boolean;
|
||||
showSocketInfo: boolean;
|
||||
socketInfoAlert?: string;
|
||||
socketInfoNum?: number;
|
||||
currentAddress?: string;
|
||||
enableTimeIncrement?: boolean;
|
||||
}
|
||||
|
||||
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress, enableTimeIncrement }: Props) => {
|
||||
const TxsTable = ({
|
||||
txs,
|
||||
sort,
|
||||
sorting,
|
||||
top,
|
||||
showBlockInfo,
|
||||
showSocketInfo,
|
||||
socketInfoAlert,
|
||||
socketInfoNum,
|
||||
currentAddress,
|
||||
enableTimeIncrement,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Table variant="simple" minWidth="950px" size="xs">
|
||||
<TheadSticky top={ top }>
|
||||
@@ -53,19 +67,21 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr
|
||||
</TheadSticky>
|
||||
<Tbody>
|
||||
{ showSocketInfo && (
|
||||
<TxsNewItemNotice borderRadius={ 0 } url={ window.location.href }>
|
||||
<SocketNewItemsNotice borderRadius={ 0 } url={ window.location.href } alert={ socketInfoAlert } num={ socketInfoNum }>
|
||||
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> }
|
||||
</TxsNewItemNotice>
|
||||
</SocketNewItemsNotice>
|
||||
) }
|
||||
{ txs.map((item) => (
|
||||
<TxsTableItem
|
||||
key={ item.hash }
|
||||
tx={ item }
|
||||
showBlockInfo={ showBlockInfo }
|
||||
currentAddress={ currentAddress }
|
||||
enableTimeIncrement={ enableTimeIncrement }
|
||||
/>
|
||||
)) }
|
||||
<AnimatePresence initial={ false }>
|
||||
{ txs.map((item) => (
|
||||
<TxsTableItem
|
||||
key={ item.hash }
|
||||
tx={ item }
|
||||
showBlockInfo={ showBlockInfo }
|
||||
currentAddress={ currentAddress }
|
||||
enableTimeIncrement={ enableTimeIncrement }
|
||||
/>
|
||||
)) }
|
||||
</AnimatePresence>
|
||||
</Tbody>
|
||||
</Table>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Show,
|
||||
Hide,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
import type { Transaction } from 'types/api/transaction';
|
||||
@@ -65,7 +66,14 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
|
||||
|
||||
const infoBorderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
|
||||
return (
|
||||
<Tr>
|
||||
<Tr
|
||||
as={ motion.tr }
|
||||
initial={{ opacity: 0, scale: 0.97 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transitionDuration="normal"
|
||||
transitionTimingFunction="linear"
|
||||
key={ tx.hash }
|
||||
>
|
||||
<Td pl={ 4 }>
|
||||
<Popover placement="right-start" openDelay={ 300 } isLazy>
|
||||
{ ({ isOpen }) => (
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |