1
2import {
3 Alert,
4 AlertIcon,
5 Flex,
6 Button,
7 Tooltip,
8 HStack,
9 Spinner,
10 ModalBody,
11 ModalCloseButton,
12 ModalContent,
13 Modal,
14 VStack,
15 useToast,
16 ModalOverlay,
17 IconButton,
18} from '@chakra-ui/react';
19import { zodResolver } from '@hookform/resolvers/zod';
20import { FC, useEffect, useState } from 'react';
21import { FormProvider, useForm } from 'react-hook-form';
22import { useTranslation } from 'react-i18next';
23import {
24 AddIcon,
25 DialogModalHeader,
26 DialogModalFooter,
27 DeleteIcon,
28 EditIcon
29} from 'components/dialog';
30import { getDefaultValues } from './default-values';
31import { schema, FormType } from './form-schema';
32import { InboundsField } from './inbounds-field';
33import { NameField } from './name-field';
34import { InboundType, Service, ServiceCreate } from 'types';
35import { useInbounds, useServices } from 'stores';
36
37const formatService = (service: Service): FormType => {
38 const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => {
39 return (typeof inbound !== 'number') ? inbound.id : inbound;
40 });
41 return {
42 ...service,
43 inbounds,
44 };
45};
46
47export type ServiceDialogProps = {};
48
49export const ServiceDialog: FC<ServiceDialogProps> = () => {
50 const {
51 editingService,
52 isCreatingNewService,
53 onCreateService,
54 editService,
55 onEditingService,
56 createService,
57 onDeletingService,
58 } = useServices();
59 const {
60 inbounds,
61 refetchInbounds,
62 } = useInbounds();
63 const isEditing = !!editingService;
64 const isOpen = isCreatingNewService || isEditing;
65 const [loading, setLoading] = useState(false);
66 const [error, setError] = useState<string | null>('');
67 const toast = useToast();
68 const { t } = useTranslation();
69
70 const form = useForm<FormType>({
71 defaultValues: getDefaultValues(),
72 resolver: zodResolver(schema),
73 });
74
75 useEffect(() => {
76 if (isOpen) {
77 refetchInbounds();
78 form.reset();
79 }
80 }, [isOpen]);
81
82
83 useEffect(() => {
84 if (editingService) {
85 form.reset(formatService(editingService));
86 }
87 }, [editingService]);
88
89 const submit = async (values: FormType) => {
90 setLoading(true);
91 const methods = { edited: editService, created: createService };
92 const method = isEditing ? 'edited' : 'created';
93 setError(null);
94 const { inbounds, name } = values;
95
96 let body: ServiceCreate = { name, inbounds, id: editingService?.id };
97
98 await methods[method](body)
99 .then(() => {
100 toast({
101 title: t(
102 isEditing ? 'serviceDialog.serviceEdited' : 'serviceDialog.serviceCreated',
103 { name: values.name }
104 ),
105 status: 'success',
106 isClosable: true,
107 position: 'top',
108 duration: 3000,
109 });
110 onClose();
111 })
112 .catch((err) => {
113 if (err?.response?.status === 409 || err?.response?.status === 400)
114 setError(err?.response?._data?.detail);
115 if (err?.response?.status === 422) {
116 Object.keys(err.response._data.detail).forEach((key) => {
117 setError(err?.response._data.detail[key] as string);
118 form.setError(key as 'name' | 'inbounds', { type: 'custom', message: err.response._data.detail[key], });
119 });
120 }
121 })
122 .finally(() => {
123 setLoading(false);
124 });
125 };
126
127 const onClose = () => {
128 form.reset(getDefaultValues());
129 onCreateService(false);
130 onEditingService(null);
131 setError(null);
132 };
133
134 const disabled = loading;
135
136 return (
137 <Modal isOpen={isOpen} onClose={onClose} size="lg">
138 <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
139 <FormProvider {...form} formState={form.formState}>
140 <ModalContent mx="3">
141 <form onSubmit={form.handleSubmit(submit)}>
142 <DialogModalHeader HeaderIcon={isEditing ? EditIcon : AddIcon} title={isEditing ? t('serviceDialog.editServiceTitle') : t('serviceDialog.createNewService')} />
143 <ModalCloseButton mt={3} disabled={disabled} />
144 <ModalBody>
145 <VStack justifyContent="space-between">
146 <Flex
147 flexDirection="column"
148 gridAutoRows="min-content"
149 w="full"
150 >
151 <NameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
152 <InboundsField t={t} inbounds={inbounds} form={form} />
153 </Flex>
154 {error && (
155 <Alert
156 status="error"
157 display={{ base: 'none', md: 'flex' }}
158 >
159 <AlertIcon />
160 {error}
161 </Alert>
162 )}
163 </VStack>
164 {error && (
165 <Alert
166 mt="3"
167 status="error"
168 display={{ base: 'flex', md: 'none' }}
169 >
170 <AlertIcon />
171 {error}
172 </Alert>
173 )}
174 </ModalBody>
175 <DialogModalFooter>
176 <HStack
177 justifyContent="flex-start"
178 w={{
179 base: 'full',
180 sm: 'unset',
181 }}
182 >
183 {isEditing && editingService !== null && (
184 <>
185 <Tooltip label={t('delete')} placement="top">
186 <IconButton
187 aria-label="Delete"
188 size="sm"
189 onClick={() => {
190 onDeletingService(editingService);
191 onClose();
192 }}
193 >
194 <DeleteIcon />
195 </IconButton>
196 </Tooltip>
197 </>
198 )}
199 </HStack>
200 <HStack
201 w="full"
202 maxW={{ md: '50%', base: 'full' }}
203 justify="end"
204 >
205 <Button
206 type="submit"
207 size="sm"
208 px="8"
209 colorScheme="primary"
210 leftIcon={loading ? <Spinner size="xs" /> : undefined}
211 disabled={false}
212 >
213 {isEditing ? t('serviceDialog.editService') : t('createService')}
214 </Button>
215 </HStack>
216 </DialogModalFooter>
217 </form>
218 </ModalContent>
219 </FormProvider>
220 </Modal >
221 );
222};