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