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