Commit 70921643 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #46 from blockscout/api-keys

api keys page
parents 97577c6b f2b9e4d0
export const apiKey = [
{
token: '6fd12fe0-841c-4abf-ac2a-8c1b08dadf8e',
name: 'zapper.fi',
},
{
token: '057085a1-d2eb-4d8d-8b89-1dd9fba32071',
name: 'TenderlyBlaBlaName',
},
{
token: '057085a1-d2eb-4d8d-8b89-1dd9fba32071',
name: 'Application name',
},
];
export type TApiKey = Array<TApiKeyItem>
export type TApiKeyItem = {
token: string;
name: string;
}
import React from 'react';
import type { NextPage } from 'next';
import Head from 'next/head'
import ApiKeys from '../ui/pages/ApiKeys';
const ApiKeysPage: NextPage = () => {
return (
<>
<Head><title>API keys</title></Head>
<ApiKeys/>
</>
);
}
export default ApiKeysPage
...@@ -32,7 +32,9 @@ const variantOutline: PartsStyleFunction<typeof parts> = (props) => { ...@@ -32,7 +32,9 @@ const variantOutline: PartsStyleFunction<typeof parts> = (props) => {
userSelect: 'all', userSelect: 'all',
}, },
_disabled: { _disabled: {
opacity: 0.4, opacity: 1,
background: mode('gray.200', 'whiteAlpha.400')(props),
border: 'none',
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
_invalid: { _invalid: {
......
...@@ -39,6 +39,9 @@ const baseStyleCloseButton: SystemStyleFunction = (props) => { ...@@ -39,6 +39,9 @@ const baseStyleCloseButton: SystemStyleFunction = (props) => {
_active: { bg: 'none' }, _active: { bg: 'none' },
} }
} }
const baseStyleOverlay = {
bg: 'blackAlpha.800',
}
const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({ const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({
dialog: baseStyleDialog(props), dialog: baseStyleDialog(props),
...@@ -46,6 +49,7 @@ const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({ ...@@ -46,6 +49,7 @@ const baseStyle: PartsStyleFunction<typeof parts> = (props) => ({
body: baseStyleBody, body: baseStyleBody,
footer: baseStyleFooter, footer: baseStyleFooter,
closeButton: baseStyleCloseButton(props), closeButton: baseStyleCloseButton(props),
overlay: baseStyleOverlay,
}) })
const sizes = { const sizes = {
...@@ -62,4 +66,6 @@ const Modal: ComponentMultiStyleConfig = { ...@@ -62,4 +66,6 @@ const Modal: ComponentMultiStyleConfig = {
baseStyle, baseStyle,
} }
Modal.defaultProps = { isCentered: true };
export default Modal; export default Modal;
...@@ -30,6 +30,7 @@ const Table: ComponentMultiStyleConfig = { ...@@ -30,6 +30,7 @@ const Table: ComponentMultiStyleConfig = {
fontWeight: 'normal', fontWeight: 'normal',
overflow: 'hidden', overflow: 'hidden',
color: 'gray.500', color: 'gray.500',
letterSpacing: 'none',
}, },
td: { td: {
fontSize: 'md', fontSize: 'md',
......
import React, { useCallback, useEffect } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import {
Box,
Button,
FormControl,
FormLabel,
Input,
} from '@chakra-ui/react';
import type { TApiKeyItem } from '../../../data/apiKey';
type Props = {
data?: TApiKeyItem;
}
type Inputs = {
token: string;
name: string;
}
// idk, maybe there is no limit
const NAME_MAX_LENGTH = 100;
const ApiKeyForm: React.FC<Props> = ({ data }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => {
setValue('token', data?.token || '');
setValue('name', data?.name || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const renderTokenInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'token'>}) => {
return (
<FormControl variant="floating" id="address" isRequired>
<Input
{ ...field }
placeholder=" "
disabled={ true }
/>
<FormLabel>Auto-generated API key token</FormLabel>
</FormControl>
)
}, []);
const renderNameInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'name'>}) => {
return (
<FormControl variant="floating" id="name" isRequired>
<Input
{ ...field }
placeholder=" "
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>Application name for API key (e.g Web3 project)</FormLabel>
</FormControl>
)
}, [ errors ]);
return (
<>
{ data && (
<Box marginBottom={ 5 }>
<Controller
name="token"
control={ control }
render={ renderTokenInput }
/>
</Box>
) }
<Box marginBottom={ 8 }>
<Controller
name="name"
control={ control }
rules={{
maxLength: NAME_MAX_LENGTH,
}}
render={ renderNameInput }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
>
{ data ? 'Save' : 'Generate API key' }
</Button>
</Box>
</>
)
}
export default ApiKeyForm;
import React, { useCallback } from 'react';
import type { TApiKeyItem } from '../../../data/apiKey';
import ApiKeyForm from './ApiKeyForm';
import FormModal from '../../shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TApiKeyItem;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit API key' : 'New API key';
const text = 'Add an application name to identify your API key. Click the button below to auto-generate the associated key.'
const renderForm = useCallback(() => {
return <ApiKeyForm data={ data }/>
}, [ data ]);
return (
<FormModal<TApiKeyItem>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
text={ text }
data={ data }
renderForm={ renderForm }
/>
)
}
export default AddressModal;
import React from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react'
import type { TApiKey, TApiKeyItem } from '../../../data/apiKey';
import ApiKeyTableItem from './ApiKeyTableItem';
interface Props {
data: TApiKey;
onEditClick: (data: TApiKeyItem) => void;
onDeleteClick: (data: TApiKeyItem) => void;
limit: number;
}
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th>{ `API key token (limit ${ limit } keys)` }</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<ApiKeyTableItem
item={ item }
key={ item.token }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default ApiKeyTable;
import React, { useCallback } from 'react';
import {
Tr,
Td,
HStack,
Text,
useColorModeValue,
} from '@chakra-ui/react'
import EditButton from '../../shared/EditButton';
import DeleteButton from '../../shared/DeleteButton';
import type { TApiKeyItem } from '../../../data/apiKey';
import CopyToClipboard from '../../shared/CopyToClipboard';
interface Props {
item: TApiKeyItem;
onEditClick: (data: TApiKeyItem) => void;
onDeleteClick: (data: TApiKeyItem) => void;
}
const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
return (
<Tr alignItems="top" key={ item.token }>
<Td>
<HStack>
<Text fontSize="md" fontWeight={ 600 }>{ item.token }</Text>
<CopyToClipboard text={ item.token }/>
</HStack>
<Text fontSize="sm" marginTop={ 0.5 } color={ secondaryColor }>{ item.name }</Text>
</Td>
<Td>
<HStack spacing={ 6 }>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td>
</Tr>
)
};
export default WatchlistTableItem;
import React, { useCallback } from 'react';
import { Text } from '@chakra-ui/react';
import DeleteModal from '../shared/DeleteModal'
type Props = {
isOpen: boolean;
onClose: () => void;
name?: string;
}
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, name }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', name);
}, [ name ]);
const renderText = useCallback(() => {
return (
<Text display="flex">API key for<Text fontWeight="600" whiteSpace="pre">{ ` "${ name || 'name' }" ` }</Text>will be deleted</Text>
)
}, [ name ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Remove API key"
renderText={ renderText }
/>
)
}
export default DeleteAddressModal;
.identicon { .identicon {
max-width: 40px; max-width: 48px;
max-height: 40px; max-height: 48px;
} }
\ No newline at end of file
...@@ -22,12 +22,12 @@ const Header = () => { ...@@ -22,12 +22,12 @@ const Header = () => {
<InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }> <InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }>
<SearchIcon w={ 5 } h={ 5 } color="gray.500"/> <SearchIcon w={ 5 } h={ 5 } color="gray.500"/>
</InputLeftElement> </InputLeftElement>
<Input paddingInlineStart="50px" placeholder="Search by addresses / transactions /block/ token ... "/> <Input paddingInlineStart="50px" placeholder="Search by addresses / transactions / block / token... "/>
</InputGroup> </InputGroup>
<ColorModeToggler/> <ColorModeToggler/>
<Center minWidth="50px" width="50px" height="50px" bg={ useColorModeValue('blackAlpha.100', 'white') } borderRadius="50%" overflow="hidden"> <Center minWidth="50px" width="50px" height="50px" bg={ useColorModeValue('blackAlpha.100', 'white') } borderRadius="50%" overflow="hidden">
{ /* the displayed size is 40px, but we need to generate x2 for retina displays */ } { /* the displayed size is 48px, but we need to generate x2 for retina displays */ }
<Identicon className={ styles.identicon } string="randomness" size={ 80 }/> <Identicon className={ styles.identicon } string="randomness" size={ 96 }/>
</Center> </Center>
</HStack> </HStack>
) )
......
...@@ -26,7 +26,7 @@ const AccountNavLink = ({ text, pathname, icon }: Props) => { ...@@ -26,7 +26,7 @@ const AccountNavLink = ({ text, pathname, icon }: Props) => {
py={ 2.5 } py={ 2.5 }
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default } bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: colors.text.hover }} _hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base" borderRadius="base"
> >
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
......
...@@ -27,7 +27,7 @@ const MainNavLink = ({ text, pathname, icon }: Props) => { ...@@ -27,7 +27,7 @@ const MainNavLink = ({ text, pathname, icon }: Props) => {
py={ 2.5 } py={ 2.5 }
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default } bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: colors.text.hover }} _hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base" borderRadius="base"
> >
<HStack justifyContent="space-between"> <HStack justifyContent="space-between">
......
import React, { useCallback, useState } from 'react';
import { Box, Button, HStack, Link, Text, useColorModeValue, useDisclosure } from '@chakra-ui/react';
import Page from '../Page/Page';
import ApiKeyTable from '../apiKey/ApiKeyTable/ApiKeyTable';
import ApiKeyModal from '../apiKey/ApiKeyModal/ApiKeyModal';
import DeleteApiKeyModal from '../apiKey/DeleteApiKeyModal';
import type { TApiKeyItem } from '../../data/apiKey';
import { apiKey } from '../../data/apiKey';
import { space } from '../../lib/html-entities';
const DATA_LIMIT = 3;
const ApiKeys: React.FC = () => {
const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ apiKeyModalData, setApiKeyModalData ] = useState<TApiKeyItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const onEditClick = useCallback((data: TApiKeyItem) => {
setApiKeyModalData(data);
apiKeyModalProps.onOpen();
}, [ apiKeyModalProps ])
const onApiKeyModalClose = useCallback(() => {
setApiKeyModalData(undefined);
apiKeyModalProps.onClose();
}, [ apiKeyModalProps ]);
const onDeleteClick = useCallback((data: TApiKeyItem) => {
setDeleteModalData(data.name);
deleteModalProps.onOpen();
}, [ deleteModalProps ])
const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const captionColor = useColorModeValue('gray.500', 'gray.400');
const canAdd = apiKey.length < DATA_LIMIT
return (
<Page>
<Box h="100%">
<Box as="h1" textStyle="h2" marginBottom={ 8 }>API keys</Box>
<Text marginBottom={ 12 }>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="#">“How to use a Blockscout API key”</Link>.
</Text>
{ Boolean(apiKey.length) && (
<ApiKeyTable
data={ apiKey }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
limit={ DATA_LIMIT }
/>
) }
<HStack marginTop={ 8 } spacing={ 5 }>
<Button
variant="primary"
size="lg"
onClick={ apiKeyModalProps.onOpen }
disabled={ !canAdd }
>
Add API key
</Button>
{ !canAdd && (
<Text fontSize="sm" color={ captionColor }>
{ `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` }
</Text>
) }
</HStack>
</Box>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
<DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } name={ deleteModalData }/>
</Page>
);
};
export default ApiKeys
...@@ -24,7 +24,7 @@ type Inputs = { ...@@ -24,7 +24,7 @@ type Inputs = {
tag: string; tag: string;
} }
const AddressModal: React.FC<Props> = ({ data }) => { const AddressForm: React.FC<Props> = ({ data }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => { useEffect(() => {
...@@ -80,4 +80,4 @@ const AddressModal: React.FC<Props> = ({ data }) => { ...@@ -80,4 +80,4 @@ const AddressModal: React.FC<Props> = ({ data }) => {
) )
} }
export default AddressModal; export default AddressForm;
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Text } from '@chakra-ui/react';
import DeleteModal from '../shared/DeleteModal' import DeleteModal from '../shared/DeleteModal'
type Props = { type Props = {
...@@ -13,13 +13,20 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => { ...@@ -13,13 +13,20 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('delete', tag); console.log('delete', tag);
}, [ tag ]); }, [ tag ]);
const renderText = useCallback(() => {
return (
<Text display="flex">Tag<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag || 'address' }" ` }</Text>will be deleted</Text>
)
}, [ tag ]);
return ( return (
<DeleteModal <DeleteModal
isOpen={ isOpen } isOpen={ isOpen }
onClose={ onClose } onClose={ onClose }
onDelete={ onDelete } onDelete={ onDelete }
title="Removal of private tag" title="Removal of private tag"
text={ `Tag "${ tag || 'address' }" will be removed` } renderText={ renderText }
/> />
) )
} }
......
...@@ -25,7 +25,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -25,7 +25,7 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th width="75%">Address</Th> <Th width="75%">Transaction</Th>
<Th width="25%">Private tag</Th> <Th width="25%">Private tag</Th>
<Th width="108px"></Th> <Th width="108px"></Th>
</Tr> </Tr>
......
...@@ -16,10 +16,10 @@ type Props = { ...@@ -16,10 +16,10 @@ type Props = {
onClose: () => void; onClose: () => void;
onDelete: () => void; onDelete: () => void;
title: string; title: string;
text: string; renderText: () => JSX.Element;
} }
const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, text }) => { const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, renderText }) => {
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
onDelete(); onDelete();
...@@ -33,7 +33,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, text } ...@@ -33,7 +33,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, text }
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody> <ModalBody>
{ text } { renderText() }
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="primary" size="lg" onClick={ onDeleteClick }> <Button variant="primary" size="lg" onClick={ onDeleteClick }>
......
...@@ -30,7 +30,7 @@ type Inputs = { ...@@ -30,7 +30,7 @@ type Inputs = {
notification: boolean; notification: boolean;
} }
const AddressModal: React.FC<Props> = ({ data }) => { const AddressForm: React.FC<Props> = ({ data }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => { useEffect(() => {
...@@ -122,4 +122,4 @@ const AddressModal: React.FC<Props> = ({ data }) => { ...@@ -122,4 +122,4 @@ const AddressModal: React.FC<Props> = ({ data }) => {
) )
} }
export default AddressModal; export default AddressForm;
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Text } from '@chakra-ui/react';
import DeleteModal from '../shared/DeleteModal' import DeleteModal from '../shared/DeleteModal'
type Props = { type Props = {
...@@ -13,13 +13,20 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => { ...@@ -13,13 +13,20 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('delete', address); console.log('delete', address);
}, [ address ]); }, [ address ]);
const renderText = useCallback(() => {
return (
<Text display="flex">Address <Text fontWeight="600" whiteSpace="pre"> { address || 'address' } </Text> will be deleted</Text>
)
}, [ address ]);
return ( return (
<DeleteModal <DeleteModal
isOpen={ isOpen } isOpen={ isOpen }
onClose={ onClose } onClose={ onClose }
onDelete={ onDelete } onDelete={ onDelete }
title="Remove address from watch list" title="Remove address from watch list"
text={ `Address ${ address || 'address' } will be deleted` } renderText={ renderText }
/> />
) )
} }
......
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