mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
token transfer socket
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ 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
|
||||
|
||||
@@ -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'>;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 |
|
||||
@@ -16,6 +17,7 @@ SocketMessage.AddressTokenBalance |
|
||||
SocketMessage.AddressCoinBalance |
|
||||
SocketMessage.AddressTxs |
|
||||
SocketMessage.AddressTxsPending |
|
||||
SocketMessage.AddressTokenTransfer |
|
||||
SocketMessage.Unknown;
|
||||
|
||||
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
|
||||
@@ -39,5 +41,6 @@ export namespace SocketMessage {
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,219 @@
|
||||
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 { 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 TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
|
||||
|
||||
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 click here to load new token transfers.');
|
||||
}, []);
|
||||
|
||||
const handleSocketError = React.useCallback(() => {
|
||||
setSocketAlert('An error has occurred while fetching new token transfers. Please click here to 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
|
||||
socketInfoAlert={ socketAlert }
|
||||
socketInfoNum={ newItemsCount }
|
||||
/>
|
||||
</Hide>
|
||||
<Show below="lg">
|
||||
<TxsNewItemNotice url={ window.location.href } num={ newItemsCount } alert={ socketAlert }/>
|
||||
<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,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) => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<TokenTransferListItem
|
||||
key={ 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,10 +1,11 @@
|
||||
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 { default as Thead } from 'ui/shared/TheadSticky';
|
||||
import TokenTransferTableItem from 'ui/shared/TokenTransfer/TokenTransferTableItem';
|
||||
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
|
||||
|
||||
interface Props {
|
||||
data: Array<TokenTransfer>;
|
||||
@@ -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,16 @@ 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 }>
|
||||
<TxsNewItemNotice borderRadius={ 0 } pl="10px" url={ window.location.href } alert={ socketInfoAlert } num={ socketInfoNum }/>
|
||||
</Td>
|
||||
</Tr>
|
||||
) }
|
||||
{ data.map((item) => (
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<TokenTransferTableItem { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 78 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
+93
-14
@@ -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 }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user