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