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