gitscope
← marzneshin / v0.2.1 / app/dashboard/src/pages/hosts/dialog/index.tsx
https://dev.kronoz.win/r/marzneshin.git
app/dashboard/src/pages/hosts/dialog/index.tsx · TypeScript
Blame Raw
  1import {
  2  Button,
  3  Select as ChakraSelect,
  4  FormControl,
  5  FormLabel,
  6  HStack,
  7  IconButton,
  8  Modal,
  9  ModalBody,
 10  ModalCloseButton,
 11  ModalContent,
 12  ModalOverlay,
 13  Tooltip,
 14  VStack,
 15  chakra,
 16  useToast,
 17  Spacer,
 18  Popover,
 19  PopoverTrigger,
 20  Portal,
 21  PopoverContent,
 22  PopoverArrow,
 23  PopoverCloseButton,
 24  Text,
 25  Badge,
 26  Alert,
 27  AlertIcon,
 28  InputRightElement,
 29  InputGroup,
 30} from '@chakra-ui/react';
 31import { zodResolver } from '@hookform/resolvers/zod';
 32import { FC, useEffect, useState } from 'react';
 33import { useInbounds, HostType, HostSchema, hostSchema } from 'stores';
 34import { Trans, useTranslation } from 'react-i18next';
 35import { getDefaultValues } from './default-values';
 36import { useForm } from 'react-hook-form';
 37import 'slick-carousel/slick/slick-theme.css';
 38import 'slick-carousel/slick/slick.css';
 39import { DialogModalHeader, AddIcon, DeleteIcon } from 'components/dialog';
 40import { Input as CustomInput } from 'components/input';
 41import { EditIcon } from 'components/table';
 42import { InfoIcon } from './icons';
 43import {
 44  proxyALPN,
 45  proxyFingerprint,
 46  proxyHostSecurity,
 47} from 'constants/Proxies';
 48import { RemarkFieldPopover } from './remark-field-popover';
 49
 50export const Select = chakra(ChakraSelect, {
 51  baseStyle: {
 52    bg: 'white',
 53    _dark: {
 54      bg: 'gray.700',
 55    },
 56  },
 57});
 58
 59export const Input = chakra(CustomInput, {
 60  baseStyle: {
 61    bg: 'white',
 62    _dark: {
 63      bg: 'gray.700',
 64    },
 65  },
 66});
 67
 68/* TODO: Impelement specific errors and input verifications
 69 * */
 70export const HostsDialog: FC = () => {
 71  const {
 72    selectedHost, isEditingHost, isCreatingHost, selectedInbound,
 73    onEditingHost, editHost, createHost, onCreatingHost, onDeletingHost
 74  } = useInbounds();
 75  const [loading, setLoading] = useState(false);
 76  const [error, setError] = useState<string | null>('');
 77  const { t } = useTranslation();
 78  const toast = useToast();
 79
 80  const defaultValueHost: HostSchema = (isEditingHost === true && selectedHost !== null) ? selectedHost : getDefaultValues();
 81
 82  const form = useForm<HostSchema>({
 83    resolver: zodResolver(hostSchema),
 84    defaultValues: defaultValueHost,
 85  });
 86
 87  const isOpen = isEditingHost || isCreatingHost;
 88  const onClose = () => {
 89    onCreatingHost(false);
 90    onEditingHost(false, null);
 91  };
 92
 93  useEffect(() => {
 94    (isOpen) && form.reset(defaultValueHost);
 95  }, [isOpen]);
 96
 97  const submit = async (values: HostSchema | HostType) => {
 98    setLoading(true);
 99    const methods = { edited: editHost, created: createHost };
100    const method = isEditingHost ? 'edited' : 'created';
101    const refId = (isEditingHost ? selectedHost?.id : null) ?? selectedInbound?.id;
102    setError(null);
103    await methods[method](refId, values)
104      .then(() => {
105        toast({
106          title: t(
107            isEditingHost ? 'hostsDialog.editHost' : 'hostsDialog.createHost',
108            { hoshtName: values.host }
109          ),
110          status: 'success',
111          isClosable: true,
112          position: 'top',
113          duration: 3000,
114        });
115        onClose();
116      })
117      .catch((err) => {
118        if (err?.response?.status === 409 || err?.response?.status === 400)
119          setError(err?.response?._data?.detail);
120        if (err?.response?.status === 422) {
121          Object.keys(err.response._data.detail).forEach((key) => {
122            setError(err?.response._data.detail[key] as string);
123            form.setError(
124              key as 'address' | 'port' | 'host' | 'path' | 'remark' | 'sni' | 'security' | 'alpn' | 'fingerprint',
125              {
126                type: 'custom',
127                message: err.response._data.detail[key],
128              }
129            );
130          });
131        }
132      })
133      .finally(() => {
134        setLoading(false);
135      });
136  };
137
138  return (
139    <Modal isOpen={isOpen} onClose={onClose}>
140      <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
141      <ModalContent mx="3" w="lg" >
142        <DialogModalHeader HeaderIcon={isEditingHost ? EditIcon : AddIcon} title={isEditingHost ? t('hostsDialog.editHost') : t('hostsDialog.createHost')} />
143        <ModalCloseButton mt={3} />
144        <ModalBody pb={4} pt={3}>
145          {loading && 'loading...'}
146          <form onSubmit={form.handleSubmit((v) => submit(v))}>
147            <VStack>
148              <HStack w="100%" alignItems="flex-start">
149                <FormControl
150                  position="relative"
151                  zIndex={10}
152                >
153                  <InputGroup>
154                    <Input
155                      {...form.register('remark')}
156                      size="sm"
157                      borderRadius="4px"
158                      placeholder="Remark"
159                    />
160                    <InputRightElement>
161                      <RemarkFieldPopover />
162                    </InputRightElement>
163                  </InputGroup>
164                </FormControl>
165              </HStack>
166              <FormControl>
167                <InputGroup>
168                  <Input
169                    size="sm"
170                    borderRadius="4px"
171                    placeholder="Address (e.g. example.com)"
172                    {...form.register('address')}
173                  />
174                  <InputRightElement>
175                    <RemarkFieldPopover />
176                  </InputRightElement>
177                </InputGroup>
178              </FormControl>
179              <FormControl>
180                <FormLabel
181                  display="flex"
182                  pb={1}
183                  alignItems="center"
184                  justifyContent="space-between"
185                  gap={1}
186                  m="0"
187                >
188                  <span>{t('hostsDialog.port')}</span>
189                  <Popover isLazy placement="right">
190                    <PopoverTrigger>
191                      <InfoIcon />
192                    </PopoverTrigger>
193                    <Portal>
194                      <PopoverContent p={2}>
195                        <PopoverArrow />
196                        <PopoverCloseButton />
197                        <Text fontSize="xs" pr={5}>
198                          {t('hostsDialog.port.info')}
199                        </Text>
200                      </PopoverContent>
201                    </Portal>
202                  </Popover>
203                </FormLabel>
204                <Input
205                  size="sm"
206                  borderRadius="4px"
207                  placeholder={String(selectedInbound?.port || '8080')}
208                  type="number"
209                  {...form.register('port')}
210                />
211              </FormControl>
212              <FormControl>
213                <FormLabel
214                  display="flex"
215                  pb={1}
216                  alignItems="center"
217                  gap={1}
218                  justifyContent="space-between"
219                  m="0"
220                >
221                  <span>{t('hostsDialog.sni')}</span>
222                  <Popover isLazy placement="right">
223                    <PopoverTrigger>
224                      <InfoIcon />
225                    </PopoverTrigger>
226                    <Portal>
227                      <PopoverContent p={2}>
228                        <PopoverArrow />
229                        <PopoverCloseButton />
230                        <Text fontSize="xs" pr={5}>
231                          {t('hostsDialog.sni.info')}
232                        </Text>
233                        <Text fontSize="xs" mt="2">
234                          <Trans
235                            i18nKey="hostsDialog.host.wildcard"
236                            components={{
237                              badge: <Badge />,
238                            }}
239                          />
240                        </Text>
241                        <Text fontSize="xs">
242                          <Trans
243                            i18nKey="hostsDialog.host.multiHost"
244                            components={{
245                              badge: <Badge />,
246                            }}
247                          />
248                        </Text>
249                      </PopoverContent>
250                    </Portal>
251                  </Popover>
252                </FormLabel>
253                <Input
254                  size="sm"
255                  borderRadius="4px"
256                  placeholder="SNI (e.g. example.com)"
257                  {...form.register('sni')}
258                />
259              </FormControl>
260              <FormControl>
261                <FormLabel
262                  display="flex"
263                  pb={1}
264                  alignItems="center"
265                  gap={1}
266                  justifyContent="space-between"
267                  m="0"
268                >
269                  <span>{t('hostsDialog.host')}</span>
270
271                  <Popover isLazy placement="right">
272                    <PopoverTrigger>
273                      <InfoIcon />
274                    </PopoverTrigger>
275                    <Portal>
276                      <PopoverContent p={2}>
277                        <PopoverArrow />
278                        <PopoverCloseButton />
279                        <Text fontSize="xs" pr={5}>
280                          {t('hostsDialog.host.info')}
281                        </Text>
282                        <Text fontSize="xs" mt="2">
283                          <Trans
284                            i18nKey="hostsDialog.host.wildcard"
285                            components={{
286                              badge: <Badge />,
287                            }}
288                          />
289                        </Text>
290                        <Text fontSize="xs">
291                          <Trans
292                            i18nKey="hostsDialog.host.multiHost"
293                            components={{
294                              badge: <Badge />,
295                            }}
296                          />
297                        </Text>
298                      </PopoverContent>
299                    </Portal>
300                  </Popover>
301                </FormLabel>
302                <Input
303                  size="sm"
304                  borderRadius="4px"
305                  placeholder="Host (e.g. example.com)"
306                  {...form.register('host')}
307                />
308              </FormControl>
309              <FormControl>
310                <FormLabel
311                  display="flex"
312                  pb={1}
313                  alignItems="center"
314                  gap={1}
315                  justifyContent="space-between"
316                  m="0"
317                >
318                  <span>{t('hostsDialog.path')}</span>
319                  <Popover isLazy placement="right">
320                    <PopoverTrigger>
321                      <InfoIcon />
322                    </PopoverTrigger>
323                    <Portal>
324                      <PopoverContent p={2}>
325                        <PopoverArrow />
326                        <PopoverCloseButton />
327                        <Text fontSize="xs" pr={5}>
328                          {t('hostsDialog.path.info')}
329                        </Text>
330                      </PopoverContent>
331                    </Portal>
332                  </Popover>
333                </FormLabel>
334                <Input
335                  size="sm"
336                  borderRadius="4px"
337                  placeholder="path (e.g. /vless)"
338                  {...form.register('path')}
339                />
340              </FormControl>
341              <FormControl height="66px">
342                <FormLabel
343                  display="flex"
344                  pb={1}
345                  alignItems="center"
346                  gap={1}
347                  justifyContent="space-between"
348                  m="0"
349                >
350                  <span>{t('hostsDialog.security')}</span>
351                  <Popover isLazy placement="right">
352                    <PopoverTrigger>
353                      <InfoIcon />
354                    </PopoverTrigger>
355                    <Portal>
356                      <PopoverContent p={2}>
357                        <PopoverArrow />
358                        <PopoverCloseButton />
359                        <Text fontSize="xs" pr={5}>
360                          {t('hostsDialog.security.info')}
361                        </Text>
362                      </PopoverContent>
363                    </Portal>
364                  </Popover>
365                </FormLabel>
366                <Select size="sm" {...form.register('security')} >
367                  {proxyHostSecurity.map((s) => {
368                    return (
369                      <option key={s.value} value={s.value}>
370                        {s.title}
371                      </option>
372                    );
373                  })}
374                </Select>
375              </FormControl>
376              {/* TODO: Add ALPN and fingerprint fields smooth entrance and
377                  * pushing down animation when security field is TLS
378                  * - Insertion and pushing down the below element 
379                  * - Slowly increasing the opacity 
380                  * Related issue: #95
381                  */}
382              <FormControl height="66px">
383                <FormLabel
384                  display="flex"
385                  pb={1}
386                  alignItems="center"
387                  gap={1}
388                  justifyContent="space-between"
389                  m="0"
390                >
391                  <span>{t('hostsDialog.alpn')}</span>
392                </FormLabel>
393                <Select
394                  size="sm"
395                  {...form.register('alpn')}
396                >
397                  {proxyALPN.map((s) => {
398                    return (
399                      <option key={s.value} value={s.value}>
400                        {s.title}
401                      </option>
402                    );
403                  })}
404                </Select>
405              </FormControl>
406              <FormControl height="66px">
407                <FormLabel
408                  display="flex"
409                  pb={1}
410                  alignItems="center"
411                  gap={1}
412                  justifyContent="space-between"
413                  m="0"
414                >
415                  <span>{t('hostsDialog.fingerprint')}</span>
416                </FormLabel>
417                <Select
418                  size="sm"
419                  {...form.register('fingerprint')}
420                >
421                  {proxyFingerprint.map((s) => {
422                    return (
423                      <option key={s.value} value={s.value}>
424                        {s.title}
425                      </option>
426                    );
427                  })}
428                </Select>
429              </FormControl>
430              <Spacer m={2}></Spacer>
431              <HStack w="full">
432                <Tooltip label={t('delete')} placement="top">
433                  <IconButton
434                    colorScheme="red"
435                    variant="ghost"
436                    size="sm"
437                    aria-label="delete host"
438                    onClick={() => {
439                      if (selectedHost !== null) {
440                        onDeletingHost(true, selectedHost);
441                        onClose();
442                      }
443                    }}
444                  >
445                    <DeleteIcon />
446                  </IconButton>
447                </Tooltip>
448                <Button
449                  flexGrow={1}
450                  type="submit"
451                  colorScheme="primary"
452                  size="sm"
453                  px={5}
454                  w="full"
455                  isLoading={loading}
456                >
457                  {isEditingHost ? t('hostsDialog.editSubmit') : t('hostsDialog.createSubmit')}
458                </Button>
459              </HStack>
460              {error && (
461                <Alert
462                  status="error"
463                  display={{ base: 'none', md: 'flex' }}
464                >
465                  <AlertIcon />
466                  {error}
467                </Alert>
468              )}
469            </VStack>
470          </form>
471        </ModalBody>
472      </ModalContent>
473    </Modal>
474  );
475};