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