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 { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
......@@ -56,3 +56,8 @@ export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: TWatchlistItem = {
},
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';
interface Props {
item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
}
const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const ApiKeyListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
......@@ -24,8 +25,8 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<ListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile>
);
};
......
......@@ -12,13 +12,14 @@ import type { ApiKeys, ApiKey } from 'types/api/account';
import ApiKeyTableItem from './ApiKeyTableItem';
interface Props {
data: ApiKeys;
data?: ApiKeys;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
limit: number;
}
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => {
return (
<Table variant="simple" minWidth="600px">
<Thead>
......@@ -28,10 +29,11 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
{ data?.map((item, index) => (
<ApiKeyTableItem
key={ item.api_key + (isLoading ? index : '') }
item={ item }
key={ item.api_key }
isLoading={ isLoading }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
......
......@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
}
const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const ApiKeyTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
......@@ -28,10 +29,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return (
<Tr alignItems="top" key={ item.api_key }>
<Td>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
</Td>
<Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td>
</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 type { ApiKey } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { space } from 'lib/html-entities';
import { API_KEY } from 'stubs/account';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
......@@ -14,21 +14,22 @@ import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
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 ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
useRedirectForInvalidAuthToken();
const [ apiKeyModalData, setApiKeyModalData ] = 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) => {
setApiKeyModalData(data);
......@@ -58,22 +59,6 @@ const ApiKeysPage: React.FC = () => {
);
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 (error.status === 403) {
throw new Error('Unverified email error', { cause: error });
......@@ -81,50 +66,59 @@ const ApiKeysPage: React.FC = () => {
return <DataFetchAlert/>;
}
const list = isMobile ? (
<Box>
{ data.map((item) => (
<ApiKeyListItem
item={ item }
key={ item.api_key }
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
<ApiKeyListItem
key={ item.api_key + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<ApiKeyTable
data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
limit={ DATA_LIMIT }
/>
)) }
</Box>
) : (
<ApiKeyTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
limit={ DATA_LIMIT }
/>
</Box>
</>
);
const canAdd = data.length < DATA_LIMIT;
const canAdd = !isPlaceholderData ? (data?.length || 0) < DATA_LIMIT : true;
return (
<>
{ description }
{ Boolean(data.length) && list }
<Stack
{ Boolean(data?.length) && list }
<Skeleton
marginTop={ 8 }
spacing={ 5 }
direction={{ base: 'column', lg: 'row' }}
align={{ base: 'start', lg: 'center' }}
flexDir={{ base: 'column', lg: 'row' }}
alignItems={{ base: 'start', lg: 'center' }}
isLoaded={ !isPlaceholderData }
display="inline-flex"
columnGap={ 5 }
rowGap={ 5 }
>
<Button
size="lg"
onClick={ apiKeyModalProps.onOpen }
isDisabled={ !canAdd }
>
Add API key
Add API key
</Button>
{ !canAdd && (
<Text fontSize="sm" variant="secondary">
{ `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` }
</Text>
) }
</Stack>
</Skeleton>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
{ 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 keyIcon from 'icons/key.svg';
......@@ -7,18 +7,27 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
apiKey: string;
name: string;
isLoading?: boolean;
}
const ApiKeySnippet = ({ apiKey, name }: Props) => {
const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return (
<HStack spacing={ 2 } alignItems="start">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
<Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Text fontSize="md" lineHeight={ 6 } fontWeight={ 600 } mr={ 1 }>{ apiKey }</Text>
<CopyToClipboard text={ apiKey }/>
<Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }>
<span>{ apiKey }</span>
</Skeleton>
<CopyToClipboard text={ apiKey } isLoading={ isLoading }/>
</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>
</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