Commit 8e5e0c6d authored by tom's avatar tom

skeletons for api keys page

parent 5ff6ec76
import type { PublicTag, AddressTag, TransactionTag } from 'types/api/account'; import type { PublicTag, AddressTag, TransactionTag, ApiKey } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
...@@ -56,3 +56,8 @@ export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: TWatchlistItem = { ...@@ -56,3 +56,8 @@ export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: TWatchlistItem = {
}, },
tokens_count: 42, tokens_count: 42,
}; };
export const API_KEY: ApiKey = {
api_key: '9c3ecf44-a1ca-4ff1-b28e-329e8b65f652',
name: 'placeholder',
};
...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: ApiKey; item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
} }
const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const ApiKeyListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -24,8 +25,8 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -24,8 +25,8 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/> <ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,13 +12,14 @@ import type { ApiKeys, ApiKey } from 'types/api/account'; ...@@ -12,13 +12,14 @@ import type { ApiKeys, ApiKey } from 'types/api/account';
import ApiKeyTableItem from './ApiKeyTableItem'; import ApiKeyTableItem from './ApiKeyTableItem';
interface Props { interface Props {
data: ApiKeys; data?: ApiKeys;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
limit: number; limit: number;
} }
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => { const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -28,10 +29,11 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => { ...@@ -28,10 +29,11 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<ApiKeyTableItem <ApiKeyTableItem
key={ item.api_key + (isLoading ? index : '') }
item={ item } item={ item }
key={ item.api_key } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: ApiKey; item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
} }
const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const ApiKeyTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -28,10 +29,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -28,10 +29,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.api_key }> <Tr alignItems="top" key={ item.api_key }>
<Td> <Td>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/> <ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Button, Stack, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ApiKey } from 'types/api/account'; import type { ApiKey } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import { API_KEY } from 'stubs/account';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem'; import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
...@@ -14,21 +14,22 @@ import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; ...@@ -14,21 +14,22 @@ import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const DATA_LIMIT = 3; const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => { const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError, error } = useApiQuery('api_keys'); const { data, isPlaceholderData, isError, error } = useApiQuery('api_keys', {
queryOptions: {
placeholderData: Array(3).fill(API_KEY),
},
});
const onEditClick = useCallback((data: ApiKey) => { const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data); setApiKeyModalData(data);
...@@ -58,22 +59,6 @@ const ApiKeysPage: React.FC = () => { ...@@ -58,22 +59,6 @@ const ApiKeysPage: React.FC = () => {
); );
const content = (() => { const content = (() => {
if (isLoading && !data) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="48px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
if (error.status === 403) { if (error.status === 403) {
throw new Error('Unverified email error', { cause: error }); throw new Error('Unverified email error', { cause: error });
...@@ -81,36 +66,45 @@ const ApiKeysPage: React.FC = () => { ...@@ -81,36 +66,45 @@ const ApiKeysPage: React.FC = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ data.map((item) => ( <Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
<ApiKeyListItem <ApiKeyListItem
key={ item.api_key + (isPlaceholderData ? index : '') }
item={ item } item={ item }
key={ item.api_key } isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
)) } )) }
</Box> </Box>
) : ( <Box display={{ base: 'none', lg: 'block' }}>
<ApiKeyTable <ApiKeyTable
data={ data } data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
limit={ DATA_LIMIT } limit={ DATA_LIMIT }
/> />
</Box>
</>
); );
const canAdd = data.length < DATA_LIMIT; const canAdd = !isPlaceholderData ? (data?.length || 0) < DATA_LIMIT : true;
return ( return (
<> <>
{ description } { description }
{ Boolean(data.length) && list } { Boolean(data?.length) && list }
<Stack <Skeleton
marginTop={ 8 } marginTop={ 8 }
spacing={ 5 } flexDir={{ base: 'column', lg: 'row' }}
direction={{ base: 'column', lg: 'row' }} alignItems={{ base: 'start', lg: 'center' }}
align={{ base: 'start', lg: 'center' }} isLoaded={ !isPlaceholderData }
display="inline-flex"
columnGap={ 5 }
rowGap={ 5 }
> >
<Button <Button
size="lg" size="lg"
...@@ -124,7 +118,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -124,7 +118,7 @@ const ApiKeysPage: React.FC = () => {
{ `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` } { `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` }
</Text> </Text>
) } ) }
</Stack> </Skeleton>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/> <ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
{ deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> } { deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</> </>
......
import { Box, HStack, Icon, Flex, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, HStack, Icon, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import keyIcon from 'icons/key.svg'; import keyIcon from 'icons/key.svg';
...@@ -7,18 +7,27 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -7,18 +7,27 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
apiKey: string; apiKey: string;
name: string; name: string;
isLoading?: boolean;
} }
const ApiKeySnippet = ({ apiKey, name }: Props) => { const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return ( return (
<HStack spacing={ 2 } alignItems="start"> <HStack spacing={ 2 } alignItems="start">
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/> <Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Box> <Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Text fontSize="md" lineHeight={ 6 } fontWeight={ 600 } mr={ 1 }>{ apiKey }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }>
<CopyToClipboard text={ apiKey }/> <span>{ apiKey }</span>
</Skeleton>
<CopyToClipboard text={ apiKey } isLoading={ isLoading }/>
</Flex> </Flex>
{ name && <Text fontSize="sm" variant="secondary" mt={ 1 }>{ name }</Text> } { name && (
<Skeleton isLoaded={ !isLoading } display="inline-block" fontSize="sm" color="text_secondary" mt={ 1 }>
<span>{ name }</span>
</Skeleton>
) }
</Box> </Box>
</HStack> </HStack>
); );
......
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