import { Button, Select as ChakraSelect, FormControl, FormLabel, HStack, IconButton, Modal, ModalBody, ModalCloseButton, ModalContent, ModalOverlay, Tooltip, VStack, chakra, useToast, Spacer, Popover, PopoverTrigger, Portal, PopoverContent, PopoverArrow, PopoverCloseButton, Text, Badge, Alert, AlertIcon, InputRightElement, InputGroup, } from '@chakra-ui/react'; import { zodResolver } from '@hookform/resolvers/zod'; import { FC, useEffect, useState } from 'react'; import { useInbounds, HostType, HostSchema, hostSchema } from 'stores'; import { Trans, useTranslation } from 'react-i18next'; import { getDefaultValues } from './default-values'; import { useForm } from 'react-hook-form'; import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick.css'; import { DialogModalHeader, AddIcon, DeleteIcon } from 'components/dialog'; import { Input as CustomInput } from 'components/input'; import { EditIcon } from 'components/table'; import { InfoIcon } from './icons'; import { proxyALPN, proxyFingerprint, proxyHostSecurity, } from 'constants/Proxies'; import { RemarkFieldPopover } from './remark-field-popover'; export const Select = chakra(ChakraSelect, { baseStyle: { bg: 'white', _dark: { bg: 'gray.700', }, }, }); export const Input = chakra(CustomInput, { baseStyle: { bg: 'white', _dark: { bg: 'gray.700', }, }, }); /* TODO: Impelement specific errors and input verifications * */ export const HostsDialog: FC = () => { const { selectedHost, isEditingHost, isCreatingHost, selectedInbound, onEditingHost, editHost, createHost, onCreatingHost, onDeletingHost } = useInbounds(); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const { t } = useTranslation(); const toast = useToast(); const defaultValueHost: HostSchema = (isEditingHost === true && selectedHost !== null) ? selectedHost : getDefaultValues(); const form = useForm({ resolver: zodResolver(hostSchema), defaultValues: defaultValueHost, }); const isOpen = isEditingHost || isCreatingHost; const onClose = () => { onCreatingHost(false); onEditingHost(false, null); }; useEffect(() => { (isOpen) && form.reset(defaultValueHost); }, [isOpen]); const submit = async (values: HostSchema | HostType) => { setLoading(true); const methods = { edited: editHost, created: createHost }; const method = isEditingHost ? 'edited' : 'created'; const refId = (isEditingHost ? selectedHost?.id : null) ?? selectedInbound?.id; setError(null); await methods[method](refId, values) .then(() => { toast({ title: t( isEditingHost ? 'hostsDialog.editHost' : 'hostsDialog.createHost', { hoshtName: values.host } ), status: 'success', isClosable: true, position: 'top', duration: 3000, }); onClose(); }) .catch((err) => { if (err?.response?.status === 409 || err?.response?.status === 400) setError(err?.response?._data?.detail); if (err?.response?.status === 422) { Object.keys(err.response._data.detail).forEach((key) => { setError(err?.response._data.detail[key] as string); form.setError( key as 'address' | 'port' | 'host' | 'path' | 'remark' | 'sni' | 'security' | 'alpn' | 'fingerprint', { type: 'custom', message: err.response._data.detail[key], } ); }); } }) .finally(() => { setLoading(false); }); }; return ( {loading && 'loading...'}
submit(v))}> {t('hostsDialog.port')} {t('hostsDialog.port.info')} {t('hostsDialog.sni')} {t('hostsDialog.sni.info')} , }} /> , }} /> {t('hostsDialog.host')} {t('hostsDialog.host.info')} , }} /> , }} /> {t('hostsDialog.path')} {t('hostsDialog.path.info')} {t('hostsDialog.security')} {t('hostsDialog.security.info')} {/* TODO: Add ALPN and fingerprint fields smooth entrance and * pushing down animation when security field is TLS * - Insertion and pushing down the below element * - Slowly increasing the opacity * Related issue: #95 */} {t('hostsDialog.alpn')} {t('hostsDialog.fingerprint')} { if (selectedHost !== null) { onDeletingHost(true, selectedHost); onClose(); } }} > {error && ( {error} )}
); };