Commit a8c723ca authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

watchlist and private tags pagination (#1333)

Co-authored-by: default avatarisstuev <natix.naf@gmail.com>
parent 15cfad34
......@@ -3,13 +3,13 @@ import type {
UserInfo,
CustomAbis,
PublicTags,
AddressTags,
TransactionTags,
ApiKeys,
WatchlistAddress,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
TokenInfoApplications,
WatchlistResponse,
TransactionTagsResponse,
AddressTagsResponse,
} from 'types/api/account';
import type {
Address,
......@@ -90,20 +90,23 @@ export const RESOURCES = {
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v1/user/watchlist/:id?',
path: '/api/account/v2/user/watchlist/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
public_tags: {
path: '/api/account/v1/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v1/user/tags/address/:id?',
path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
private_tags_tx: {
path: '/api/account/v1/user/tags/transaction/:id?',
path: '/api/account/v2/user/tags/transaction/:id?',
pathParams: [ 'id' as const ],
filterFields: [ ],
},
api_keys: {
path: '/api/account/v1/user/api_keys/:id?',
......@@ -579,7 +582,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'verified_contracts' |
'l2_output_roots' | 'l2_withdrawals' | 'l2_txn_batches' | 'l2_deposits' |
'zkevm_l2_txn_batches' | 'zkevm_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals';
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -588,10 +592,10 @@ export type ResourcePayload<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'watchlist' ? WatchlistResponse :
Q extends 'verified_addresses' ? VerifiedAddressResponse :
Q extends 'token_info_applications_config' ? TokenInfoApplicationConfig :
Q extends 'token_info_applications' ? TokenInfoApplications :
......
......@@ -8,6 +8,14 @@ export interface AddressTag {
export type AddressTags = Array<AddressTag>
export type AddressTagsResponse = {
items: AddressTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface ApiKey {
api_key: string;
name: string;
......@@ -48,6 +56,14 @@ export interface TransactionTag {
export type TransactionTags = Array<TransactionTag>
export type TransactionTagsResponse = {
items: TransactionTags;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export type Transactions = Array<Transaction>
export interface UserInfo {
......@@ -78,6 +94,14 @@ export interface WatchlistAddressNew {
export type WatchlistAddresses = Array<WatchlistAddress>
export type WatchlistResponse = {
items: WatchlistAddresses;
next_page_params: {
id: number;
items_count: number;
} | null;
}
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
......
......@@ -2,24 +2,29 @@ import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { WatchlistAddress } from 'types/api/account';
import type { WatchlistAddress, WatchlistResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { WATCH_LIST_ITEM_WITH_TOKEN_INFO } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => {
const { data, isPlaceholderData, isError } = useApiQuery('watchlist', {
queryOptions: {
placeholderData: Array(3).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO),
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'watchlist',
options: {
placeholderData: { items: Array(5).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO), next_page_params: null },
},
});
const queryClient = useQueryClient();
......@@ -58,9 +63,11 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]);
const onDeleteSuccess = useCallback(async() => {
queryClient.setQueryData([ resourceKey('watchlist') ], (prevData: Array<WatchlistAddress> | undefined) => {
return prevData?.filter((item) => item.id !== deleteModalData?.id);
});
queryClient.setQueryData(getResourceKey('watchlist'), (prevData: WatchlistResponse | undefined) => {
const newItems = prevData?.items.filter((item: WatchlistAddress) => item.id !== deleteModalData?.id);
return { ...prevData, items: newItems };
},
);
}, [ deleteModalData?.id, queryClient ]);
const description = (
......@@ -69,15 +76,17 @@ const WatchList: React.FC = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const content = (() => {
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
{ data?.items.map((item, index) => (
<WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item }
......@@ -89,10 +98,11 @@ const WatchList: React.FC = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable
data={ data }
data={ data?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
......@@ -101,7 +111,13 @@ const WatchList: React.FC = () => {
return (
<>
{ description }
{ Boolean(data?.length) && list }
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { AddressTags, AddressTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
isLoading: boolean;
top: number;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading }: Props) => {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="116px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item: AddressTag, index: number) => (
<AddressTagTableItem
......
......@@ -2,10 +2,10 @@ import { Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback } from 'react';
import type { AddressTag, TransactionTag, AddressTags, TransactionTags } from 'types/api/account';
import type { AddressTag, TransactionTag, AddressTagsResponse, TransactionTagsResponse } from 'types/api/account';
import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import DeleteModal from 'ui/shared/DeleteModal';
type Props = {
......@@ -32,12 +32,15 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const onSuccess = useCallback(async() => {
if (type === 'address') {
queryClient.setQueryData([ resourceKey('private_tags_address') ], (prevData: AddressTags | undefined) => {
return prevData?.filter((item: AddressTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_address'), (prevData: AddressTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: AddressTag) => item.id !== id);
return { ...prevData, items: newItems };
});
} else {
queryClient.setQueryData([ resourceKey('private_tags_tx') ], (prevData: TransactionTags | undefined) => {
return prevData?.filter((item: TransactionTag) => item.id !== id);
queryClient.setQueryData(getResourceKey('private_tags_tx'), (prevData: TransactionTagsResponse | undefined) => {
const newItems = prevData?.items.filter((item: TransactionTag) => item.id !== id);
return { ...prevData, items: newItems };
});
}
}, [ type, id, queryClient ]);
......
......@@ -3,11 +3,13 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
......@@ -15,10 +17,11 @@ import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal';
const PrivateAddressTags = () => {
const { data: addressTagsData, isError, isPlaceholderData, refetch } = useApiQuery('private_tags_address', {
queryOptions: {
const { data: addressTagsData, isError, isPlaceholderData, refetch, pagination } = useQueryWithPages({
resourceName: 'private_tags_address',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_ADDRESS),
placeholderData: { items: Array(5).fill(PRIVATE_TAG_ADDRESS), next_page_params: null },
},
});
......@@ -52,14 +55,10 @@ const PrivateAddressTags = () => {
deleteModalProps.onClose();
}, [ deleteModalProps ]);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ addressTagsData?.map((item: AddressTag, index: number) => (
{ addressTagsData?.items.map((item: AddressTag, index: number) => (
<AddressTagListItem
item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
......@@ -72,21 +71,34 @@ const PrivateAddressTags = () => {
<Box display={{ base: 'none', lg: 'block' }}>
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
data={ addressTagsData?.items }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
<AccountPageDescription>
Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription>
{ Boolean(addressTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ addressTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
......@@ -3,10 +3,12 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import { PRIVATE_TAG_TX } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal';
......@@ -14,10 +16,11 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => {
const { data: transactionTagsData, isPlaceholderData, isError } = useApiQuery('private_tags_tx', {
queryOptions: {
const { data: transactionTagsData, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'private_tags_tx',
options: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_TX),
placeholderData: { items: Array(3).fill(PRIVATE_TAG_TX), next_page_params: null },
},
});
......@@ -54,14 +57,10 @@ const PrivateTransactionTags = () => {
</AccountPageDescription>
);
if (isError) {
return <DataFetchAlert/>;
}
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ transactionTagsData?.map((item, index) => (
{ transactionTagsData?.items.map((item, index) => (
<TransactionTagListItem
key={ item.id + (isPlaceholderData ? index : '') }
item={ item }
......@@ -73,19 +72,31 @@ const PrivateTransactionTags = () => {
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<TransactionTagTable
data={ transactionTagsData }
data={ transactionTagsData?.items }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
top={ pagination.isVisible ? 80 : 0 }
/>
</Box>
</>
);
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
{ description }
{ Boolean(transactionTagsData?.length) && list }
<DataListDisplay
isError={ isError }
items={ transactionTagsData?.items }
emptyText=""
content={ list }
actionBar={ actionBar }
/>
<Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button
size="lg"
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { TransactionTags, TransactionTag } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import TransactionTagTableItem from './TransactionTagTableItem';
interface Props {
......@@ -16,18 +17,19 @@ interface Props {
isLoading: boolean;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
top: number;
}
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item, index) => (
<TransactionTagTableItem
......
......@@ -70,7 +70,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryParams: Object.keys(queryParams).length ? queryParams : undefined,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
......@@ -9,6 +8,8 @@ import React from 'react';
import type { WatchlistAddress } from 'types/api/account';
import TheadSticky from 'ui/shared/TheadSticky';
import WatchlistTableItem from './WatchListTableItem';
interface Props {
......@@ -16,19 +17,20 @@ interface Props {
isLoading?: boolean;
onEditClick: (data: WatchlistAddress) => void;
onDeleteClick: (data: WatchlistAddress) => void;
top: number;
}
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
<TheadSticky top={ top }>
<Tr>
<Th width="70%">Address</Th>
<Th width="30%">Private tag</Th>
<Th width="160px">Email notification</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
</TheadSticky>
<Tbody>
{ data?.map((item, index) => (
<WatchlistTableItem
......
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