1import {
2 Box,
3 Button,
4 chakra,
5 Checkbox,
6 FormControl,
7 FormLabel,
8 HStack,
9 Modal,
10 ModalBody,
11 ModalCloseButton,
12 ModalContent,
13 ModalOverlay,
14 Switch,
15 Tooltip,
16 useToast,
17 VStack, IconButton, Spacer
18} from '@chakra-ui/react';
19import { zodResolver } from '@hookform/resolvers/zod';
20import {
21 getNodeCreateDefaultValues,
22 NodeCreate,
23 NodeCreateSchema,
24 useNodes,
25} from 'stores';
26import { FC, useEffect, useState } from 'react';
27import { Controller, useForm } from 'react-hook-form';
28import { useTranslation } from 'react-i18next';
29import 'slick-carousel/slick/slick-theme.css';
30import 'slick-carousel/slick/slick.css';
31import { DialogModalHeader, DeleteIcon, AddIcon, EditIcon } from 'components/dialog';
32import { Input } from 'components/input';
33
34const CustomInput = chakra(Input, {
35 baseStyle: {
36 bg: 'white',
37 _dark: {
38 bg: 'gray.700',
39 },
40 },
41});
42
43export const NodesDialog: FC = () => {
44 const { editingNode, isEditingNode, onEditingNode, isAddingNode, refetchNodes, updateNode, addNode, onAddingNode, onDeletingNode } = useNodes();
45 const [loading, setLoading] = useState(false);
46 const [, setError] = useState<string | null>('');
47
48 const { t } = useTranslation();
49 const toast = useToast();
50 const defaultValueNode: NodeCreate = (isEditingNode === true && editingNode !== null && editingNode !== undefined) ? editingNode : getNodeCreateDefaultValues();
51 const form = useForm<NodeCreate>({
52 resolver: zodResolver(NodeCreateSchema),
53 defaultValues: defaultValueNode,
54 });
55 const isOpen = isEditingNode || isAddingNode;
56 const onClose = () => {
57 onAddingNode(false);
58 onEditingNode(undefined);
59 };
60 useEffect(() => {
61 if (isOpen) {
62 refetchNodes();
63 form.reset(defaultValueNode);
64 }
65 }, [isOpen]);
66
67 const submit = async (values: NodeCreate) => {
68 setLoading(true);
69 const methods = { edited: updateNode, created: addNode };
70 const method = isEditingNode ? 'edited' : 'created';
71 const { name, port, id, address, usage_coefficient } = values;
72 setError(null);
73 let body: NodeCreate = {
74 id,
75 name,
76 address,
77 port,
78 status: values.status,
79 usage_coefficient,
80 add_as_new_host: values.add_as_new_host,
81 };
82
83 await methods[method](body)
84 .then(() => {
85 toast({
86 title: t(
87 isEditingNode ? 'nodeDialog.editNode' : 'nodeDialog.createNewNode',
88 { name: values.name }
89 ),
90 status: 'success',
91 isClosable: true,
92 position: 'top',
93 duration: 3000,
94 });
95 refetchNodes();
96 onClose();
97 })
98 .catch((err) => {
99 if (err?.response?.status === 409 || err?.response?.status === 400)
100 setError(err?.response?._data?.detail);
101 if (err?.response?.status === 422) {
102 Object.keys(err.response._data.detail).forEach((key) => {
103 setError(err?.response._data.detail[key] as string);
104 form.setError(
105 key as 'address' | 'port' | 'name' | 'status',
106 {
107 type: 'custom',
108 message: err.response._data.detail[key],
109 }
110 );
111 });
112 }
113 })
114 .finally(() => {
115 setLoading(false);
116 });
117 };
118
119 return (
120 <>
121 <Modal isOpen={isOpen} onClose={onClose}>
122 <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
123 <ModalContent mx="3" w="fit-content" maxW="3xl">
124 <DialogModalHeader HeaderIcon={isEditingNode ? EditIcon : AddIcon} title={isEditingNode ? t('nodeDialog.editNode') : t('nodeDialog.addNode')} />
125 <ModalCloseButton mt={3} />
126 <ModalBody w="440px" pb={6} pt={3}>
127 <VStack>
128 {loading && 'loading...'}
129
130 <form onSubmit={form.handleSubmit((v) => submit(v))}>
131 <VStack>
132 <HStack w="full">
133 <FormControl>
134 <CustomInput
135 label={t('nodeDialog.nodeName')}
136 size="sm"
137 placeholder="Marzn-Node-2"
138 {...form.register('name')}
139 error={form.formState?.errors?.name?.message}
140 />
141 </FormControl>
142 <HStack px={1}>
143 <Controller
144 name="status"
145 control={form.control}
146 render={({ field }) => {
147 return (
148 <Tooltip
149 key={field.value}
150 placement="top"
151 label={
152 `${t('usersTable.status')}: ` +
153 (field.value !== 'disabled' ? t('active') : t('disabled'))
154 }
155 textTransform="capitalize"
156 >
157 <Box mt="6">
158 <Switch
159 colorScheme="primary"
160 isChecked={field.value !== 'disabled'}
161 onChange={(e) => {
162 if (e.target.checked) {
163 field.onChange('none');
164 } else {
165 field.onChange('disabled');
166 }
167 }}
168 />
169 </Box>
170 </Tooltip>
171 );
172 }}
173 />
174 </HStack>
175 </HStack>
176 <HStack alignItems="flex-start">
177 <Box w="65%">
178 <CustomInput
179 label={t('nodeDialog.nodeAddress')}
180 size="sm"
181 placeholder="51.20.12.13"
182 {...form.register('address')}
183 error={form.formState?.errors?.address?.message}
184 />
185 </Box>
186 <Box w="35%">
187 <CustomInput
188 label={t('nodeDialog.nodePort')}
189 size="sm"
190 placeholder="62050"
191 {...form.register('port')}
192 error={form.formState?.errors?.port?.message}
193 />
194 </Box>
195 </HStack>
196 {!editingNode && (
197 <FormControl py={1}>
198 <Checkbox {...form.register('add_as_new_host')}>
199 <FormLabel m={0}>{t('nodeDialog.addHostForEveryInbound')}</FormLabel>
200 </Checkbox>
201 </FormControl>
202 )}
203 <Spacer m={2}></Spacer>
204 <HStack w="full">
205 <Tooltip label={t('delete')} placement="top">
206 <IconButton
207 colorScheme="red"
208 variant="ghost"
209 size="sm"
210 aria-label="delete node"
211 onClick={() => {
212 if (editingNode !== null) {
213 onDeletingNode(editingNode);
214 onClose();
215 }
216 }}
217 >
218 <DeleteIcon />
219 </IconButton>
220 </Tooltip>
221 <Button
222 flexGrow={1}
223 type="submit"
224 colorScheme="primary"
225 size="sm"
226 px={5}
227 w="full"
228 isLoading={loading}
229 >
230 {isEditingNode ? t('nodeDialog.updateSubmit') : t('nodeDialog.addSubmit')}
231 </Button>
232 </HStack>
233 </VStack>
234 </form>
235 </VStack>
236 </ModalBody>
237 </ModalContent>
238 </Modal>
239 </>
240 );
241}