1import {
2 Alert,
3 Button,
4 Tooltip,
5 IconButton,
6 Spinner,
7 AlertIcon,
8 Box,
9 Collapse,
10 Flex,
11 Grid,
12 GridItem,
13 ModalBody,
14 ModalCloseButton,
15 ModalContent,
16 Modal,
17 VStack,
18 useColorMode,
19 useToast,
20 ModalOverlay,
21 HStack,
22} from '@chakra-ui/react';
23import { zodResolver } from '@hookform/resolvers/zod';
24import dayjs from 'dayjs';
25import { FC, useEffect, useState } from 'react';
26import ReactApexChart from 'react-apexcharts';
27import { FormProvider, useForm, useWatch } from 'react-hook-form';
28import { useTranslation } from 'react-i18next';
29import {
30 User,
31 FilterUsageType,
32 UserCreate,
33 Service
34} from 'types';
35import { UsageFilter, createUsageConfig } from 'components/usage-filter';
36import {
37 AddIcon,
38 EditIcon,
39 DialogModalHeader,
40 DialogModalFooter,
41 DeleteIcon
42} from 'components/dialog';
43import { UserUsageIcon } from './icons';
44import { getDefaultValues } from './default-values';
45import { schema, FormType } from './form-schema';
46import { ServicesField } from './services-field';
47import { UsernameField } from './username-field';
48import { DataLimitField } from './data-limit-field';
49import { PeriodicUsageReset } from './periodic-usage-reset';
50import { ExpireDateField } from './expire-date-field';
51import { NoteField } from './note-field';
52import { useUsers, useServices, fetchServices } from 'stores';
53import { queryIds } from 'constants/query-ids';
54import { useQuery } from '@tanstack/react-query';
55
56const formatUser = (user: User): FormType => {
57 const services: number[] = user.services.map((service: number | Service): number => {
58 return (typeof service !== 'number') ? service.id : service;
59 });
60 return {
61 ...user,
62 services,
63 data_limit: user.data_limit
64 ? Number((user.data_limit / 1073741824).toFixed(5))
65 : user.data_limit,
66 };
67};
68
69export type UserDialogProps = {};
70
71export const UserDialog: FC<UserDialogProps> = () => {
72 const {
73 editingUser,
74 isCreatingNewUser,
75 onCreateUser,
76 editUser,
77 fetchUserUsage,
78 onEditingUser,
79 createUser,
80 onDeletingUser,
81 } = useUsers();
82 const { refetchServices, servicesFilters: filters } = useServices();
83 const { data: services } = useQuery({
84 queryKey: [queryIds.services],
85 initialData: [],
86 queryFn: () => {
87 return fetchServices(filters);
88 }
89 });
90 const isEditing = !!editingUser;
91 const isOpen = isCreatingNewUser || isEditing;
92 const [loading, setLoading] = useState(false);
93 const [error, setError] = useState<string | null>('');
94 const toast = useToast();
95 const { t, i18n } = useTranslation();
96
97 const { colorMode } = useColorMode();
98
99 const [usageVisible, setUsageVisible] = useState(false);
100 const handleUsageToggle = () => {
101 setUsageVisible((current) => !current);
102 };
103
104 const form = useForm<FormType>({
105 defaultValues: getDefaultValues(),
106 resolver: zodResolver(schema),
107 });
108
109 const [dataLimit] = useWatch({
110 control: form.control,
111 name: ['data_limit'],
112 });
113
114 const usageTitle = t('userDialog.total');
115 const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
116 const [usageFilter, setUsageFilter] = useState('1m');
117 const fetchUsageWithFilter = (query: FilterUsageType) => {
118 fetchUserUsage(editingUser!, query).then((data: any) => {
119 const labels = [];
120 const series = [];
121 for (const key in data.usages) {
122 series.push(data.usages[key].used_traffic);
123 labels.push(data.usages[key].node_name);
124 }
125 setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
126 });
127 };
128
129 useEffect(() => {
130 if (isOpen) {
131 refetchServices();
132 }
133 }, [isOpen]);
134
135
136 useEffect(() => {
137 if (editingUser) {
138 form.reset(formatUser(editingUser));
139
140 fetchUsageWithFilter({
141 start: dayjs().utc().subtract(30, 'day').format('YYYY-MM-DDTHH:00:00'),
142 });
143 }
144 }, [editingUser]);
145
146 const submit = async (values: FormType) => {
147 setLoading(true);
148 const methods = { edited: editUser, created: createUser };
149 const method = isEditing ? 'edited' : 'created';
150 setError(null);
151 const { services, username, ...rest } = values;
152
153 let body: UserCreate = {
154 ...rest,
155 username,
156 services,
157 data_limit: values.data_limit,
158 data_limit_reset_strategy:
159 values.data_limit && values.data_limit > 0
160 ? values.data_limit_reset_strategy
161 : 'no_reset',
162 status:
163 values.status === 'active' || values.status === 'disabled'
164 ? values.status
165 : 'active',
166 };
167
168 await methods[method](body)
169 .then(() => {
170 toast({
171 title: t(
172 isEditing ? 'userDialog.userEdited' : 'userDialog.userCreated',
173 { username: values.username }
174 ),
175 status: 'success',
176 isClosable: true,
177 position: 'top',
178 duration: 3000,
179 });
180 onClose();
181 })
182 .catch((err) => {
183 if (err?.response?.status === 409 || err?.response?.status === 400)
184 setError(err?.response?._data?.detail);
185 if (err?.response?.status === 422) {
186 Object.keys(err.response._data.detail).forEach((key) => {
187 setError(err?.response._data.detail[key] as string);
188 form.setError(
189 key as 'services' | 'username' | 'data_limit' | 'expire',
190 {
191 type: 'custom',
192 message: err.response._data.detail[key],
193 }
194 );
195 });
196 }
197 })
198 .finally(() => {
199 setLoading(false);
200 });
201 };
202
203 const onClose = () => {
204 form.reset(getDefaultValues());
205 onCreateUser(false);
206 onEditingUser(null);
207 setError(null);
208 setUsageVisible(false);
209 setUsageFilter('1m');
210 };
211
212 const handleResetUsage = () => {
213 useUsers.setState({ resetUsageUser: editingUser });
214 };
215
216 const handleRevokeSubscription = () => {
217 useUsers.setState({ revokeSubscriptionUser: editingUser });
218 };
219
220 const disabled = loading;
221
222 return (
223 <Modal isOpen={isOpen} onClose={onClose} size="2xl">
224 <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
225 <FormProvider {...form} formState={form.formState}>
226 <ModalContent mx="3">
227 <form onSubmit={form.handleSubmit(submit)}>
228 <DialogModalHeader HeaderIcon={isEditing ? EditIcon : AddIcon} title={isEditing ? t('userDialog.editUserTitle') : t('createNewUser')} />
229 <ModalCloseButton mt={3} disabled={disabled} />
230 <ModalBody>
231 <Grid
232 templateColumns={{
233 base: 'repeat(1, 1fr)',
234 md: 'repeat(2, 1fr)',
235 }}
236 gap={3}
237 >
238 <GridItem>
239 <VStack justifyContent="space-between">
240 <Flex
241 flexDirection="column"
242 gridAutoRows="min-content"
243 w="full"
244 >
245 <UsernameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
246 <DataLimitField form={form} disabled={disabled} t={t} />
247
248 <Collapse
249 in={!!(dataLimit && dataLimit > 0)}
250 animateOpacity
251 style={{ width: '100%' }}
252 >
253 <PeriodicUsageReset form={form} t={t} />
254 </Collapse>
255 <ExpireDateField form={form} t={t} i18n={i18n} disabled={disabled} />
256 <NoteField form={form} t={t} />
257 </Flex>
258 {error && (
259 <Alert
260 status="error"
261 display={{ base: 'none', md: 'flex' }}
262 >
263 <AlertIcon />
264 {error}
265 </Alert>
266 )}
267 </VStack>
268 </GridItem>
269
270 <GridItem>
271 <ServicesField t={t} services={services || []} form={form} />
272 </GridItem>
273
274 {isEditing && usageVisible && (
275 <GridItem pt={6} colSpan={{ base: 1, md: 2 }}>
276 <VStack gap={4}>
277 <UsageFilter
278 defaultValue={usageFilter}
279 onChange={(filter, query) => {
280 setUsageFilter(filter);
281 fetchUsageWithFilter(query);
282 }}
283 />
284 <Box
285 width={{ base: '100%', md: '70%' }}
286 justifySelf="center"
287 >
288 <ReactApexChart
289 options={usage.options}
290 series={usage.series}
291 type="donut"
292 />
293 </Box>
294 </VStack>
295 </GridItem>
296 )}
297 </Grid>
298
299 {error && (
300 <Alert
301 mt="3"
302 status="error"
303 display={{ base: 'flex', md: 'none' }}
304 >
305 <AlertIcon />
306 {error}
307 </Alert>
308 )}
309 </ModalBody>
310 <DialogModalFooter>
311 <HStack
312 justifyContent="flex-start"
313 w={{
314 base: 'full',
315 sm: 'unset',
316 }}
317 >
318 {isEditing && editingUser !== null && (
319 <>
320 <Tooltip label={t('delete')} placement="top">
321 <IconButton
322 aria-label="Delete"
323 size="sm"
324 onClick={() => {
325 onDeletingUser(editingUser);
326 onClose();
327 }}
328 >
329 <DeleteIcon />
330 </IconButton>
331 </Tooltip>
332 <Tooltip label={t('userDialog.usage')} placement="top">
333 <IconButton
334 aria-label="usage"
335 size="sm"
336 onClick={handleUsageToggle}
337 >
338 <UserUsageIcon />
339 </IconButton>
340 </Tooltip>
341 <Button onClick={handleResetUsage} size="sm">
342 {t('userDialog.resetUsage')}
343 </Button>
344 <Button onClick={handleRevokeSubscription} size="sm">
345 {t('userDialog.revokeSubscription')}
346 </Button>
347 </>
348 )}
349 </HStack>
350 <HStack
351 w="full"
352 maxW={{ md: '50%', base: 'full' }}
353 justify="end"
354 >
355 <Button
356 type="submit"
357 size="sm"
358 px="8"
359 colorScheme="primary"
360 leftIcon={loading ? <Spinner size="xs" /> : undefined}
361 disabled={false}
362 >
363 {isEditing ? t('userDialog.editUser') : t('createUser')}
364 </Button>
365 </HStack>
366 </DialogModalFooter>
367 </form>
368 </ModalContent>
369 </FormProvider>
370 </Modal >
371 );
372};