gitscope
← marzneshin / Diff · a61ba53 → a61ba53
Changes a61ba53..a61ba53
Patch commit
diff --git a/app/dashboard/src/constants/query-ids.ts b/app/dashboard/src/constants/query-ids.ts
new file mode 100644
index 0000000..9ded4b9
--- /dev/null
+++ b/app/dashboard/src/constants/query-ids.ts
@@ -0,0 +1,8 @@
+
+export const queryIds = {
+ hosts: 'hosts',
+ inbounds: 'inbounds',
+ users: 'users',
+ nodes: 'nodes',
+ services: 'services',
+};
diff --git a/app/dashboard/src/pages/hosts/inbounds-sidebar/index.tsx b/app/dashboard/src/pages/hosts/inbounds-sidebar/index.tsx
index 97ec2fd..cd6bb3c 100644
--- a/app/dashboard/src/pages/hosts/inbounds-sidebar/index.tsx
+++ b/app/dashboard/src/pages/hosts/inbounds-sidebar/index.tsx
@@ -2,23 +2,24 @@ import { Table, Thead, Tbody, Th, Td, Tr, HStack } from '@chakra-ui/react';
import classNames from 'classnames';
import { InboundCard } from 'components/inbounds-card';
import { handleSort } from 'components/table';
-import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-import { useInbounds } from 'stores';
+import { useQuery } from 'react-query';
+import { fetchInbounds, useInbounds } from 'stores';
export const InboundsSidebar = () => {
const {
- inbounds,
onInboundsFilterChange,
inboundsFilters: filters,
selectedInbound,
selectInbound,
- refetchInbounds
} = useInbounds();
+
+ const { data: inbounds } = useQuery('hosts', () => {
+ return fetchInbounds();
+ });
+
const { t } = useTranslation();
- useEffect(() => {
- inbounds.length === 0 && refetchInbounds();
- }, [inbounds.length])
+
return (
<Table orientation="vertical" zIndex="docked" pr="3" >
<Thead zIndex="docked" position="relative">
@@ -37,7 +38,7 @@ export const InboundsSidebar = () => {
</Tr>
</Thead>
<Tbody>
- {inbounds.map((inbound, i) => {
+ {inbounds?.map((inbound, i) => {
const active = inbounds[i] == selectedInbound
return (
<Tr key={i}
@@ -47,7 +48,6 @@ export const InboundsSidebar = () => {
onClick={() => { selectInbound(inbound) }}>
<Td>
<InboundCard
-
tag={inbound.tag}
nodeName={inbound.node.name}
protocol={inbound.protocol}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/data-limit-field.tsx b/app/dashboard/src/pages/hosts/users/dialog/data-limit-field.tsx
deleted file mode 100644
index d225b53..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/data-limit-field.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { FormControl, FormLabel } from '@chakra-ui/react';
-import { Input } from 'components/input';
-import { TFunction } from 'i18next';
-import { Controller } from 'react-hook-form';
-
-
-interface DataLimitFieldProps {
- form: any;
- disabled: boolean;
- t: TFunction<'translation', undefined, 'translation'>;
-}
-
-export const DataLimitField = ({ form, disabled, t }: DataLimitFieldProps) => {
- return (
- <FormControl mb={'10px'}>
- <FormLabel>{t('userDialog.dataLimit')}</FormLabel>
- <Controller
- control={form.control}
- name="data_limit"
- render={({ field }) => {
- return (
- <Input
- endAdornment="GB"
- type="number"
- size="sm"
- borderRadius="6px"
- onChange={field.onChange}
- disabled={disabled}
- error={
- form.formState.errors.data_limit?.message
- }
- value={field.value ? String(field.value) : ''}
- />
- );
- }}
- />
- </FormControl>
- )
-}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/default-values.ts b/app/dashboard/src/pages/hosts/users/dialog/default-values.ts
deleted file mode 100644
index 3c516d3..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/default-values.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-
-import { FormType } from './form-schema';
-
-export const getDefaultValues = (): FormType => {
-
- return {
- services: [],
- data_limit: 0,
- expire: null,
- username: '',
- data_limit_reset_strategy: 'no_reset',
- status: 'active',
- note: '',
- };
-};
diff --git a/app/dashboard/src/pages/hosts/users/dialog/expire-date-field.tsx b/app/dashboard/src/pages/hosts/users/dialog/expire-date-field.tsx
deleted file mode 100644
index 7019df8..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/expire-date-field.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import {
- FormControl,
- FormHelperText,
- FormLabel,
-} from '@chakra-ui/react';
-import dayjs from 'dayjs';
-import { i18n, TFunction } from 'i18next';
-import ReactDatePicker from 'react-datepicker';
-import { Controller } from 'react-hook-form';
-import { relativeExpiryDate } from 'utils/dateFormatter';
-import { Input } from 'components/input';
-
-interface ExpireDateFieldProps {
- form: any;
- disabled: boolean;
- t: TFunction<'translation', undefined, 'translation'>;
- i18n: i18n;
-}
-
-export const ExpireDateField = ({ form, t, i18n, disabled }: ExpireDateFieldProps) => {
- return (
- <FormControl mb={'10px'}>
- <FormLabel>{t('userDialog.expiryDate')}</FormLabel>
- <Controller
- name="expire"
- control={form.control}
- render={({ field }) => {
- function createDateAsUTC(num: number) {
- return dayjs(
- dayjs(num * 1000).utc()
- // .format("MMMM D, YYYY") // exception with: dayjs.locale(lng);
- ).toDate();
- }
- const { status, time } = relativeExpiryDate(
- field.value
- );
- return (
- <>
- <ReactDatePicker
- locale={i18n.language.toLocaleLowerCase()}
- dateFormat={t('dateFormat')}
- minDate={new Date()}
- selected={
- field.value
- ? createDateAsUTC(field.value)
- : undefined
- }
- onChange={(date: Date) => {
- field.onChange({
- target: {
- value: date
- ? dayjs(
- dayjs(date)
- .set('hour', 23)
- .set('minute', 59)
- .set('second', 59)
- )
- .utc()
- .valueOf() / 1000
- : 0,
- name: 'expire',
- },
- });
- }}
- customInput={
- <Input
- size="sm"
- type="text"
- borderRadius="6px"
- clearable
- disabled={disabled}
- error={
- form.formState.errors.expire?.message
- }
- />
- }
- />
- {field.value ? (
- <FormHelperText>
- {t(status, { time: time })}
- </FormHelperText>
- ) : (
- ''
- )}
- </>
- );
- }}
- />
- </FormControl>
- )
-}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/form-schema.ts b/app/dashboard/src/pages/hosts/users/dialog/form-schema.ts
deleted file mode 100644
index 9f452c8..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/form-schema.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-
-import { z } from 'zod';
-import { UserCreate } from 'types';
-
-export type FormType = Pick<UserCreate, keyof UserCreate>;
-export const schema = z.object({
- username: z.string().min(1, { message: 'Username is required' }),
- note: z.string().nullable(),
- data_limit: z
- .string()
- .min(0, 'The minimum number is 0')
- .or(z.number())
- .nullable()
- .transform((str) => {
- if (str) return Number((parseFloat(String(str)) * 1073741824).toFixed(5));
- return 0;
- }),
- expire: z.number().nullable(),
- data_limit_reset_strategy: z.string(),
- status: z.string(),
- services: z.array(z.number()).nonempty({ message: 'At least one service is required' }),
-});
diff --git a/app/dashboard/src/pages/hosts/users/dialog/icons.ts b/app/dashboard/src/pages/hosts/users/dialog/icons.ts
deleted file mode 100644
index 8f02730..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/icons.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-
-import { chakra } from '@chakra-ui/react';
-import {
- ChartPieIcon,
-} from '@heroicons/react/24/outline';
-export const UserUsageIcon = chakra(ChartPieIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
diff --git a/app/dashboard/src/pages/hosts/users/dialog/index.tsx b/app/dashboard/src/pages/hosts/users/dialog/index.tsx
deleted file mode 100644
index 6ca47dc..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/index.tsx
+++ /dev/null
@@ -1,363 +0,0 @@
-import {
- Alert,
- Button,
- Tooltip,
- IconButton,
- Spinner,
- AlertIcon,
- Box,
- Collapse,
- Flex,
- Grid,
- GridItem,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- Modal,
- VStack,
- useColorMode,
- useToast,
- ModalOverlay,
- HStack,
-} from '@chakra-ui/react';
-import { zodResolver } from '@hookform/resolvers/zod';
-import dayjs from 'dayjs';
-import { FC, useEffect, useState } from 'react';
-import ReactApexChart from 'react-apexcharts';
-import { FormProvider, useForm, useWatch } from 'react-hook-form';
-import { useTranslation } from 'react-i18next';
-import {
- User,
- FilterUsageType,
- UserCreate,
- Service
-} from 'types';
-import { UsageFilter, createUsageConfig } from 'components/usage-filter';
-import {
- AddIcon,
- EditIcon,
- DialogModalHeader,
- DialogModalFooter,
- DeleteIcon
-} from 'components/dialog';
-import { UserUsageIcon } from './icons';
-import { getDefaultValues } from './default-values';
-import { schema, FormType } from './form-schema';
-import { ServicesField } from './services-field';
-import { UsernameField } from './username-field';
-import { DataLimitField } from './data-limit-field';
-import { PeriodicUsageReset } from './periodic-usage-reset';
-import { ExpireDateField } from './expire-date-field';
-import { NoteField } from './note-field';
-import { useUsers, useServices } from 'stores';
-
-const formatUser = (user: User): FormType => {
- const services: number[] = user.services.map((service: number | Service): number => {
- return (typeof service !== 'number') ? service.id : service;
- });
- return {
- ...user,
- services,
- data_limit: user.data_limit
- ? Number((user.data_limit / 1073741824).toFixed(5))
- : user.data_limit,
- };
-};
-
-export type UserDialogProps = {};
-
-export const UserDialog: FC<UserDialogProps> = () => {
- const {
- editingUser,
- isCreatingNewUser,
- onCreateUser,
- editUser,
- fetchUserUsage,
- onEditingUser,
- createUser,
- onDeletingUser,
- } = useUsers();
- const { refetchServices, services } = useServices();
- const isEditing = !!editingUser;
- const isOpen = isCreatingNewUser || isEditing;
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>('');
- const toast = useToast();
- const { t, i18n } = useTranslation();
-
- const { colorMode } = useColorMode();
-
- const [usageVisible, setUsageVisible] = useState(false);
- const handleUsageToggle = () => {
- setUsageVisible((current) => !current);
- };
-
- const form = useForm<FormType>({
- defaultValues: getDefaultValues(),
- resolver: zodResolver(schema),
- });
-
- const [dataLimit] = useWatch({
- control: form.control,
- name: ['data_limit'],
- });
-
- const usageTitle = t('userDialog.total');
- const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
- const [usageFilter, setUsageFilter] = useState('1m');
- const fetchUsageWithFilter = (query: FilterUsageType) => {
- fetchUserUsage(editingUser!, query).then((data: any) => {
- const labels = [];
- const series = [];
- for (const key in data.usages) {
- series.push(data.usages[key].used_traffic);
- labels.push(data.usages[key].node_name);
- }
- setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
- });
- };
-
- useEffect(() => {
- if (isOpen) {
- refetchServices();
- }
- }, [isOpen]);
-
-
- useEffect(() => {
- if (editingUser) {
- form.reset(formatUser(editingUser));
-
- fetchUsageWithFilter({
- start: dayjs().utc().subtract(30, 'day').format('YYYY-MM-DDTHH:00:00'),
- });
- }
- }, [editingUser]);
-
- const submit = async (values: FormType) => {
- setLoading(true);
- const methods = { edited: editUser, created: createUser };
- const method = isEditing ? 'edited' : 'created';
- setError(null);
- const { services, username, ...rest } = values;
-
- let body: UserCreate = {
- ...rest,
- username,
- services,
- data_limit: values.data_limit,
- data_limit_reset_strategy:
- values.data_limit && values.data_limit > 0
- ? values.data_limit_reset_strategy
- : 'no_reset',
- status:
- values.status === 'active' || values.status === 'disabled'
- ? values.status
- : 'active',
- };
-
- await methods[method](body)
- .then(() => {
- toast({
- title: t(
- isEditing ? 'userDialog.userEdited' : 'userDialog.userCreated',
- { username: values.username }
- ),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- onClose();
- })
- .catch((err) => {
- if (err?.response?.status === 409 || err?.response?.status === 400)
- setError(err?.response?._data?.detail);
- if (err?.response?.status === 422) {
- Object.keys(err.response._data.detail).forEach((key) => {
- setError(err?.response._data.detail[key] as string);
- form.setError(
- key as 'services' | 'username' | 'data_limit' | 'expire',
- {
- type: 'custom',
- message: err.response._data.detail[key],
- }
- );
- });
- }
- })
- .finally(() => {
- setLoading(false);
- });
- };
-
- const onClose = () => {
- form.reset(getDefaultValues());
- onCreateUser(false);
- onEditingUser(null);
- setError(null);
- setUsageVisible(false);
- setUsageFilter('1m');
- };
-
- const handleResetUsage = () => {
- useUsers.setState({ resetUsageUser: editingUser });
- };
-
- const handleRevokeSubscription = () => {
- useUsers.setState({ revokeSubscriptionUser: editingUser });
- };
-
- const disabled = loading;
-
- return (
- <Modal isOpen={isOpen} onClose={onClose} size="2xl">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <FormProvider {...form} formState={form.formState}>
- <ModalContent mx="3">
- <form onSubmit={form.handleSubmit(submit)}>
- <DialogModalHeader HeaderIcon={isEditing ? EditIcon : AddIcon} title={isEditing ? t('userDialog.editUserTitle') : t('createNewUser')} />
- <ModalCloseButton mt={3} disabled={disabled} />
- <ModalBody>
- <Grid
- templateColumns={{
- base: 'repeat(1, 1fr)',
- md: 'repeat(2, 1fr)',
- }}
- gap={3}
- >
- <GridItem>
- <VStack justifyContent="space-between">
- <Flex
- flexDirection="column"
- gridAutoRows="min-content"
- w="full"
- >
- <UsernameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
- <DataLimitField form={form} disabled={disabled} t={t} />
-
- <Collapse
- in={!!(dataLimit && dataLimit > 0)}
- animateOpacity
- style={{ width: '100%' }}
- >
- <PeriodicUsageReset form={form} t={t} />
- </Collapse>
- <ExpireDateField form={form} t={t} i18n={i18n} disabled={disabled} />
- <NoteField form={form} t={t} />
- </Flex>
- {error && (
- <Alert
- status="error"
- display={{ base: 'none', md: 'flex' }}
- >
- <AlertIcon />
- {error}
- </Alert>
- )}
- </VStack>
- </GridItem>
-
- <GridItem>
- <ServicesField t={t} services={services} form={form} />
- </GridItem>
-
- {isEditing && usageVisible && (
- <GridItem pt={6} colSpan={{ base: 1, md: 2 }}>
- <VStack gap={4}>
- <UsageFilter
- defaultValue={usageFilter}
- onChange={(filter, query) => {
- setUsageFilter(filter);
- fetchUsageWithFilter(query);
- }}
- />
- <Box
- width={{ base: '100%', md: '70%' }}
- justifySelf="center"
- >
- <ReactApexChart
- options={usage.options}
- series={usage.series}
- type="donut"
- />
- </Box>
- </VStack>
- </GridItem>
- )}
- </Grid>
-
- {error && (
- <Alert
- mt="3"
- status="error"
- display={{ base: 'flex', md: 'none' }}
- >
- <AlertIcon />
- {error}
- </Alert>
- )}
- </ModalBody>
- <DialogModalFooter>
- <HStack
- justifyContent="flex-start"
- w={{
- base: 'full',
- sm: 'unset',
- }}
- >
- {isEditing && editingUser !== null && (
- <>
- <Tooltip label={t('delete')} placement="top">
- <IconButton
- aria-label="Delete"
- size="sm"
- onClick={() => {
- onDeletingUser(editingUser);
- onClose();
- }}
- >
- <DeleteIcon />
- </IconButton>
- </Tooltip>
- <Tooltip label={t('userDialog.usage')} placement="top">
- <IconButton
- aria-label="usage"
- size="sm"
- onClick={handleUsageToggle}
- >
- <UserUsageIcon />
- </IconButton>
- </Tooltip>
- <Button onClick={handleResetUsage} size="sm">
- {t('userDialog.resetUsage')}
- </Button>
- <Button onClick={handleRevokeSubscription} size="sm">
- {t('userDialog.revokeSubscription')}
- </Button>
- </>
- )}
- </HStack>
- <HStack
- w="full"
- maxW={{ md: '50%', base: 'full' }}
- justify="end"
- >
- <Button
- type="submit"
- size="sm"
- px="8"
- colorScheme="primary"
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- disabled={false}
- >
- {isEditing ? t('userDialog.editUser') : t('createUser')}
- </Button>
- </HStack>
- </DialogModalFooter>
- </form>
- </ModalContent>
- </FormProvider>
- </Modal >
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/dialog/note-field.tsx b/app/dashboard/src/pages/hosts/users/dialog/note-field.tsx
deleted file mode 100644
index 618a0f6..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/note-field.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { FormControl, FormLabel, Textarea, FormErrorMessage } from '@chakra-ui/react';
-import { TFunction } from 'i18next';
-
-interface NoteFieldProps {
- form: any;
- t: TFunction<'translation', undefined, 'translation'>;
-}
-
-export const NoteField = ({ t, form }: NoteFieldProps) => {
- return (
- <FormControl
- mb={'10px'}
- isInvalid={!!form.formState.errors.note}
- >
- <FormLabel>{t('userDialog.note')}</FormLabel>
- <Textarea {...form.register('note')} />
- <FormErrorMessage>
- {form.formState.errors?.note?.message}
- </FormErrorMessage>
- </FormControl>
- )
-}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/periodic-usage-reset.tsx b/app/dashboard/src/pages/hosts/users/dialog/periodic-usage-reset.tsx
deleted file mode 100644
index 5a7d530..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/periodic-usage-reset.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { FormControl, FormLabel, Select } from '@chakra-ui/react';
-import { Controller } from 'react-hook-form';
-import { resetStrategy } from 'constants/Settings';
-import { TFunction } from 'i18next';
-
-
-interface PreiodicUsageResetProps {
- form: any;
- t: TFunction<'translation', undefined, 'translation'>;
-}
-
-export const PeriodicUsageReset = ({ form, t }: PreiodicUsageResetProps) => {
- return (
- <FormControl height="66px">
- <FormLabel>
- {t('userDialog.periodicUsageReset')}
- </FormLabel>
- <Controller
- control={form.control}
- name="data_limit_reset_strategy"
- render={({ field }) => {
- return (
- <Select size="sm" {...field}>
- {resetStrategy.map((s) => {
- return (
- <option key={s.value} value={s.value}>
- {t(
- 'userDialog.resetStrategy' + s.title
- )}
- </option>
- );
- })}
- </Select>
- );
- }}
- />
- </FormControl>
- )
-}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/services-field.tsx b/app/dashboard/src/pages/hosts/users/dialog/services-field.tsx
deleted file mode 100644
index 7cfd724..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/services-field.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { FormControl, FormLabel, FormErrorMessage, Center, Spinner, Text, Flex, chakra } from '@chakra-ui/react';
-import { UsersIcon, ServerStackIcon } from '@heroicons/react/24/outline';
-import { IconBaseStyle } from 'constants/IconBaseStyle';
-import { TFunction } from 'i18next';
-import { Controller } from 'react-hook-form';
-import { Service } from 'types';
-import { SystemPropertyStatus } from 'components/system-property-status';
-import { ServicesSelectList } from './services-select-list';
-
-const UsersServiceIcon = chakra(UsersIcon, IconBaseStyle);
-const InboundsServiceIcon = chakra(ServerStackIcon, IconBaseStyle);
-
-interface ServicesFieldProps {
- form: any;
- services: Service[];
- t: TFunction<'translation', undefined, 'translation'>;
-}
-
-export const ServicesField = ({ form, t, services }: ServicesFieldProps) => {
- return services.length === 0 ? (
- <Center><Spinner /></Center>
- ) : (
- <FormControl
- isInvalid={
- !!form.formState.errors.services?.message
- }
- >
- <FormLabel>{t('userDialog.services')}</FormLabel>
- <Controller
- control={form.control}
- name="services"
- render={() => {
- return (
- <ServicesSelectList
- list={services}
- renderCheckboxContent={(service) => (
- <Flex alignItems="center" justifyContent="space-between" flexDir="row" w="full">
- <Text fontSize="sm" color="gray.700" _dark={{ color: 'gray.100' }} fontWeight="medium">
- {service.name}
- </Text>
- <Flex flexDirection="row" alignItems="center">
- <SystemPropertyStatus value={service.users.length} StatusIcon={UsersServiceIcon} />
- <Text color="primary.600" _dark={{ color: 'gray.100' }} mx="8px" fontSize="sm">/</Text>
- <SystemPropertyStatus value={service.inbounds.length} StatusIcon={InboundsServiceIcon} />
- </Flex>
- </Flex>
- )}
- />
- );
- }}
- />
- <FormErrorMessage>
- {t(
- form.formState.errors.services
- ?.message as string
- )}
- </FormErrorMessage>
- </FormControl>
- );
-}
diff --git a/app/dashboard/src/pages/hosts/users/dialog/services-select-list.tsx b/app/dashboard/src/pages/hosts/users/dialog/services-select-list.tsx
deleted file mode 100644
index ee3e4b4..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/services-select-list.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-
-import {
- Box,
- Flex,
- Stack,
-} from '@chakra-ui/react';
-import { ListCheckBox } from 'components/list-check-box';
-import { forwardRef, ReactNode } from 'react';
-import {
- useFormContext,
-} from 'react-hook-form';
-import { Service } from 'types';
-
-interface ServicesSelectListProps {
- list: Service[];
- renderCheckboxContent: (service: Service) => ReactNode;
-}
-
-export const ServicesSelectList = forwardRef<HTMLDivElement, ServicesSelectListProps>(
- function({ list, renderCheckboxContent }, ref) {
- const form = useFormContext();
-
- const handleServiceToggle = (serviceId: number) => {
- const currentServices = form.getValues('services');
- const isSelected = currentServices.includes(serviceId);
-
- let updatedServices;
-
- if (isSelected) {
- updatedServices = currentServices.filter((id: number) => id !== serviceId);
- } else {
- updatedServices = [...currentServices, serviceId];
- }
-
- form.setValue('services', updatedServices, { shouldValidate: true });
- };
-
- return (
- <Stack spacing={3} ref={ref}>
- {list.map((service: Service) => (
- <Box key={service.id}>
- <Flex align="center">
- <ListCheckBox
- isChecked={form.getValues('services').includes(service.id)}
- onChange={() => handleServiceToggle(service.id)}
- value={String(service.id)}
- renderContent={() => renderCheckboxContent(service)}
- />
- </Flex>
- </Box>
- ))}
- </Stack>
- );
- }
-);
diff --git a/app/dashboard/src/pages/hosts/users/dialog/username-field.tsx b/app/dashboard/src/pages/hosts/users/dialog/username-field.tsx
deleted file mode 100644
index 05007ca..0000000
--- a/app/dashboard/src/pages/hosts/users/dialog/username-field.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { HStack, Input, Tooltip, Box, Switch, FormControl, FormLabel } from '@chakra-ui/react';
-import { TFunction } from 'i18next';
-import { Controller } from 'react-hook-form';
-
-interface UsernameFieldProps {
- form: any;
- isEditing: boolean;
- t: TFunction<'translation', undefined, 'translation'>;
- disabled: boolean;
-}
-
-export const UsernameField = ({ form, isEditing, t, disabled }: UsernameFieldProps) => {
- return (
- <FormControl mb={'10px'}>
- <FormLabel>{t('username')}</FormLabel>
- <HStack>
- <Input
- size="sm"
- type="text"
- borderRadius="6px"
- error={form.formState.errors.username?.message}
- disabled={disabled || isEditing}
- {...form.register('username')}
- />
- {isEditing && (
- <HStack px={1}>
- <Controller
- name="status"
- control={form.control}
- render={({ field }) => {
- return (
- <Tooltip
- placement="top"
- label={'status: ' + field.value}
- textTransform="capitalize"
- >
- <Box>
- <Switch
- colorScheme="primary"
- disabled={
- field.value !== 'active' &&
- field.value !== 'disabled'
- }
- isChecked={field.value === 'active'}
- onChange={(e) => {
- if (e.target.checked) {
- field.onChange('active');
- } else {
- field.onChange('disabled');
- }
- }}
- />
- </Box>
- </Tooltip>
- );
- }}
- />
- </HStack>
- )}
- </HStack>
- </FormControl>
- )
-}
diff --git a/app/dashboard/src/pages/hosts/users/filters.tsx b/app/dashboard/src/pages/hosts/users/filters.tsx
deleted file mode 100644
index 4cd071f..0000000
--- a/app/dashboard/src/pages/hosts/users/filters.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import {
- BoxProps,
- Button,
- chakra,
- Grid,
- GridItem,
- HStack,
- IconButton,
- Input,
- InputGroup,
- InputLeftElement,
- InputRightElement,
- Spinner,
-} from '@chakra-ui/react';
-import {
- ArrowPathIcon,
- MagnifyingGlassIcon,
- XMarkIcon,
-} from '@heroicons/react/24/outline';
-import classNames from 'classnames';
-import { useDashboard, useUsers } from 'stores';
-import debounce from 'lodash.debounce';
-import React, { FC, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-const iconProps = {
- baseStyle: {
- w: 4,
- h: 4,
- },
-};
-
-const SearchIcon = chakra(MagnifyingGlassIcon, iconProps);
-const ClearIcon = chakra(XMarkIcon, iconProps);
-export const ReloadIcon = chakra(ArrowPathIcon, iconProps);
-
-export type FilterProps = {} & BoxProps;
-const setSearchField = debounce((username: string) => {
- useUsers.getState().onFilterChange({
- ...useUsers.getState().usersFilters,
- offset: 0,
- username,
- });
-}, 300);
-
-export const UsersFilters: FC<FilterProps> = ({ ...props }) => {
- const { loading, } = useDashboard();
- const { usersFilters: filters, onFilterChange, refetchUsers, onCreateUser, onResetAllUsage } = useUsers();
- const { t } = useTranslation();
- const [search, setSearch] = useState('');
- const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- setSearch(e.target.value);
- setSearchField(e.target.value);
- };
- const clear = () => {
- setSearch('');
- onFilterChange({
- ...filters,
- offset: 0,
- username: '',
- });
- };
- return (
- <Grid
- id="users-filters"
- templateColumns={{
- lg: 'repeat(3, 1fr)',
- md: 'repeat(4, 1fr)',
- base: 'repeat(1, 1fr)',
- }}
- position="sticky"
- top={0}
- mx="-6"
- px="6"
- rowGap={4}
- gap={{
- lg: 4,
- base: 0,
- }}
- bg="var(--chakra-colors-chakra-body-bg)"
- py={4}
- zIndex="docked"
- {...props}
- >
- <GridItem colSpan={{ base: 1, md: 2, lg: 1 }} order={{ base: 2, md: 1 }}>
- <InputGroup>
- <InputLeftElement pointerEvents="none" children={<SearchIcon />} />
- <Input
- placeholder={t('search')}
- value={search}
- borderColor="light-border"
- onChange={onChange}
- />
- <InputRightElement>
- {loading && <Spinner size="xs" />}
- {filters.username && filters.username.length > 0 && (
- <IconButton
- onClick={clear}
- aria-label="clear"
- size="xs"
- variant="ghost"
- >
- <ClearIcon />
- </IconButton>
- )}
- </InputRightElement>
- </InputGroup>
- </GridItem>
- <GridItem colSpan={2} order={{ base: 1, md: 2 }}>
- <HStack justifyContent="flex-end" alignItems="center" h="full">
- <IconButton
- aria-label="refresh users"
- disabled={loading}
- onClick={refetchUsers}
- size="sm"
- variant="outline"
- >
- <ReloadIcon
- className={classNames({
- 'animate-spin': loading,
- })}
- />
- </IconButton>
- <Button
- colorScheme="red"
- size="sm"
- onClick={() => onResetAllUsage(true)}
- px={5}
- >
- {t('resetAllUsage')}
- </Button>
- <Button
- colorScheme="primary"
- size="sm"
- onClick={() => onCreateUser(true)}
- px={5}
- >
- {t('createUser')}
- </Button>
- </HStack>
- </GridItem>
- </Grid>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/index.tsx b/app/dashboard/src/pages/hosts/users/index.tsx
deleted file mode 100644
index 7e4d74c..0000000
--- a/app/dashboard/src/pages/hosts/users/index.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { UserDialog } from './dialog';
-import { UsersTable } from './table';
-import { UsersFilters } from './filters';
-import {
- ResetAllUsageModal,
- DeleteUserModal,
- QRCodeDialog,
- ResetUserUsageModal,
- RevokeSubscriptionModal
-} from './modals';
-import { pages } from 'stores';
-import Page from 'components/page';
-
-const UsersPage = () => {
- return (
- <Page page={pages.users}>
- <UsersFilters />
- <UsersTable />
- <UserDialog />
- <QRCodeDialog />
- <DeleteUserModal />
- <RevokeSubscriptionModal />
- <ResetAllUsageModal />
- <ResetUserUsageModal />
- </Page>
- )
-}
-
-export default UsersPage;
diff --git a/app/dashboard/src/pages/hosts/users/modals/delete-modal.tsx b/app/dashboard/src/pages/hosts/users/modals/delete-modal.tsx
deleted file mode 100644
index 69b11f4..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/delete-modal.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- useToast,
-} from '@chakra-ui/react';
-import { FC, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { useUsers } from 'stores';
-import { DeleteModal } from 'components/delete-modal';
-
-type DeleteUserModalProps = {
- deleteCallback?: () => void;
-};
-
-export const DeleteUserModal: FC<DeleteUserModalProps> = () => {
- const [loading, setLoading] = useState(false);
- const { deletingUser, onDeletingUser, deleteUser } = useUsers();
- const { t } = useTranslation();
- const toast = useToast();
- const onClose = () => {
- onDeletingUser(null);
- };
- const onDelete = () => {
- if (deletingUser) {
- setLoading(true);
- deleteUser(deletingUser)
- .then(() => {
- toast({
- title: t('deleteUser.deleteSuccess', { username: deletingUser.username }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .then(onClose)
- .finally(setLoading.bind(null, false));
- }
- };
- return (
- <DeleteModal
- onDelete={onDelete}
- loading={loading}
- isOpen={!!deletingUser}
- onClose={onClose}
- deletingObjectName={deletingUser?.username}
- deleteObjectPromptT={'deleteUser.prompt'}
- deleteObjectTitleT={'deleteUser.title'}
- />
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/modals/index.tsx b/app/dashboard/src/pages/hosts/users/modals/index.tsx
deleted file mode 100644
index b90cacb..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/index.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-export * from './reset-all-usage-modal';
-export * from './delete-modal';
-export * from './qrcode-modals';
-export * from './reset-user-usage-modal';
-export * from './revoke-subscription-modal';
diff --git a/app/dashboard/src/pages/hosts/users/modals/qrcode-modals.tsx b/app/dashboard/src/pages/hosts/users/modals/qrcode-modals.tsx
deleted file mode 100644
index f1ee343..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/qrcode-modals.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import {
- Box,
- chakra,
- HStack,
- IconButton,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalHeader,
- ModalOverlay,
- Text,
- VStack,
-} from '@chakra-ui/react';
-import {
- ChevronLeftIcon,
- ChevronRightIcon,
- QrCodeIcon,
-} from '@heroicons/react/24/outline';
-import { QRCodeCanvas } from 'qrcode.react';
-import { FC, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import Slider from 'react-slick';
-import 'slick-carousel/slick/slick-theme.css';
-import 'slick-carousel/slick/slick.css';
-import { Icon } from 'components/icon';
-import { useUsers } from 'stores';
-
-const QRCode = chakra(QRCodeCanvas);
-const NextIcon = chakra(ChevronRightIcon, {
- baseStyle: {
- w: 6,
- h: 6,
- color: 'gray.600',
- _dark: {
- color: 'white',
- },
- },
-});
-const PrevIcon = chakra(ChevronLeftIcon, {
- baseStyle: {
- w: 6,
- h: 6,
- color: 'gray.600',
- _dark: {
- color: 'white',
- },
- },
-});
-const QRIcon = chakra(QrCodeIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-export const QRCodeDialog: FC = () => {
- const { QRcodeLinks, setQRCode, setSubLink, subscribeUrl } = useUsers();
- const isOpen = QRcodeLinks !== null;
- const [index, setIndex] = useState(0);
- const { t } = useTranslation();
- const onClose = () => {
- setQRCode(null);
- setSubLink(null);
- };
-
- const subscribeQrLink = String(subscribeUrl).startsWith('/')
- ? window.location.origin + subscribeUrl
- : String(subscribeUrl);
-
- return (
- <Modal isOpen={isOpen} onClose={onClose}>
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3" w="fit-content" maxW="3xl">
- <ModalHeader pt={6}>
- <Icon color="primary">
- <QRIcon color="white" />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- {QRcodeLinks && (
- <ModalBody
- gap={{
- base: '20px',
- lg: '50px',
- }}
- pr={{
- lg: '60px',
- }}
- px={{
- base: '50px',
- }}
- display="flex"
- justifyContent="center"
- flexDirection={{
- base: 'column',
- lg: 'row',
- }}
- >
- {subscribeUrl && (
- <VStack>
- <QRCode
- mx="auto"
- size={300}
- p="2"
- level={'L'}
- includeMargin={false}
- value={subscribeQrLink}
- bg="white"
- />
- <Text display="block" textAlign="center" pb={3} mt={1}>
- {t('qrcodeDialog.sublink')}
- </Text>
- </VStack>
- )}
- <Box w="300px">
- <Slider
- centerPadding="0px"
- centerMode={true}
- slidesToShow={1}
- slidesToScroll={1}
- dots={false}
- afterChange={setIndex}
- onInit={() => setIndex(0)}
- nextArrow={
- <IconButton
- size="sm"
- position="absolute"
- display="flex !important"
- _before={{ content: '""' }}
- aria-label="next"
- mr="-4"
- >
- <NextIcon />
- </IconButton>
- }
- prevArrow={
- <IconButton
- size="sm"
- position="absolute"
- display="flex !important"
- _before={{ content: '""' }}
- aria-label="prev"
- ml="-4"
- >
- <PrevIcon />
- </IconButton>
- }
- >
- {QRcodeLinks.map((link, i) => {
- return (
- <HStack key={i}>
- <QRCode
- mx="auto"
- size={300}
- p="2"
- level={'L'}
- includeMargin={false}
- value={link}
- bg="white"
- />
- </HStack>
- );
- })}
- </Slider>
- <Text display="block" textAlign="center" pb={3} mt={1}>
- {index + 1} / {QRcodeLinks.length}
- </Text>
- </Box>
- </ModalBody>
- )}
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/modals/reset-all-usage-modal.tsx b/app/dashboard/src/pages/hosts/users/modals/reset-all-usage-modal.tsx
deleted file mode 100644
index ef4159d..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/reset-all-usage-modal.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import {
- Button,
- chakra,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Spinner,
- Text,
- useToast,
-} from '@chakra-ui/react';
-import { FC, useState } from 'react';
-import { DocumentMinusIcon } from '@heroicons/react/24/outline';
-import { Icon } from 'components/icon';
-import { useTranslation } from 'react-i18next';
-import { useUsers } from 'stores';
-
-export const ResetIcon = chakra(DocumentMinusIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-type DeleteUserModalProps = {};
-
-export const ResetAllUsageModal: FC<DeleteUserModalProps> = () => {
- const [loading, setLoading] = useState(false);
- const { isResetingAllUsage, onResetAllUsage, resetAllUsage } = useUsers();
- const { t } = useTranslation();
- const toast = useToast();
- const onClose = () => {
- onResetAllUsage(false);
- };
- const onReset = () => {
- setLoading(true);
- resetAllUsage()
- .then(() => {
- toast({
- title: t('resetAllUsage.success'),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .catch(() => {
- toast({
- title: t('resetAllUsage.error'),
- status: 'error',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .finally(() => {
- setLoading(false);
- });
- };
- return (
- <Modal isCentered isOpen={isResetingAllUsage} onClose={onClose} size="sm">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3">
- <ModalHeader pt={6}>
- <Icon color="red">
- <ResetIcon />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <ModalBody>
- <Text fontWeight="semibold" fontSize="lg">
- {t('resetAllUsage.title')}
- </Text>
- {isResetingAllUsage && (
- <Text
- mt={1}
- fontSize="sm"
- _dark={{ color: 'gray.400' }}
- color="gray.600"
- >
- {t('resetAllUsage.prompt')}
- </Text>
- )}
- </ModalBody>
- <ModalFooter display="flex">
- <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
- {t('cancel')}
- </Button>
- <Button
- size="sm"
- w="full"
- colorScheme="red"
- onClick={onReset}
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- >
- {t('reset')}
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/modals/reset-user-usage-modal.tsx b/app/dashboard/src/pages/hosts/users/modals/reset-user-usage-modal.tsx
deleted file mode 100644
index b043c23..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/reset-user-usage-modal.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import {
- Button,
- chakra,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Spinner,
- Text,
- useToast,
-} from '@chakra-ui/react';
-import { FC, useState } from 'react';
-import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { Icon } from 'components/icon';
-import { useTranslation, Trans } from 'react-i18next';
-import { useUsers } from 'stores';
-
-const ResetIcon = chakra(ArrowPathIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-type DeleteUserModalProps = {};
-
-export const ResetUserUsageModal: FC<DeleteUserModalProps> = () => {
- const [loading, setLoading] = useState(false);
- const { resetUsageUser: user, resetDataUsage } = useUsers();
- const { t } = useTranslation();
- const toast = useToast();
- const onClose = () => {
- useUsers.setState({ resetUsageUser: null });
- };
- const onReset = () => {
- if (user) {
- setLoading(true);
- resetDataUsage(user)
- .then(() => {
- toast({
- title: t('resetUserUsage.success', { username: user.username }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .catch(() => {
- toast({
- title: t('resetUserUsage.error'),
- status: 'error',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .finally(() => {
- setLoading(false);
- });
- }
- };
- return (
- <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3">
- <ModalHeader pt={6}>
- <Icon color="blue">
- <ResetIcon />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <ModalBody>
- <Text fontWeight="semibold" fontSize="lg">
- {t('resetUserUsage.title')}
- </Text>
- {user && (
- <Text
- mt={1}
- fontSize="sm"
- _dark={{ color: 'gray.400' }}
- color="gray.600"
- >
- <Trans
- components={{ b: <b /> }}>
- {t('resetUserUsage.prompt', { username: user.username })}
- </Trans>
- </Text>
- )}
- </ModalBody>
- <ModalFooter display="flex">
- <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
- {t('cancel')}
- </Button>
- <Button
- size="sm"
- w="full"
- colorScheme="blue"
- onClick={onReset}
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- >
- {t('reset')}
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/modals/revoke-subscription-modal.tsx b/app/dashboard/src/pages/hosts/users/modals/revoke-subscription-modal.tsx
deleted file mode 100644
index cff25ab..0000000
--- a/app/dashboard/src/pages/hosts/users/modals/revoke-subscription-modal.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- Button,
- chakra,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Spinner,
- Text,
- useToast,
-} from '@chakra-ui/react';
-import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { useUsers } from 'stores';
-import { FC, useState } from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-import { Icon } from 'components/icon';
-
-const ResetIcon = chakra(ArrowPathIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-export type RevokeSubscriptionModalProps = {};
-
-export const RevokeSubscriptionModal: FC<RevokeSubscriptionModalProps> = () => {
- const [loading, setLoading] = useState(false);
- const { revokeSubscriptionUser: user, revokeSubscription } = useUsers();
- const { t } = useTranslation();
- const toast = useToast();
- const onClose = () => {
- useUsers.setState({ revokeSubscriptionUser: null });
- };
- const onReset = () => {
- if (user) {
- setLoading(true);
- revokeSubscription(user)
- .then(() => {
- toast({
- title: t('revokeUserSub.success', { username: user.username }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .catch(() => {
- toast({
- title: t('revokeUserSub.error'),
- status: 'error',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .finally(() => {
- setLoading(false);
- });
- }
- };
- return (
- <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3">
- <ModalHeader pt={6}>
- <Icon color="blue">
- <ResetIcon />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <ModalBody>
- <Text fontWeight="semibold" fontSize="lg">
- {t('revokeUserSub.title')}
- </Text>
- {user && (
- <Text
- mt={1}
- fontSize="sm"
- _dark={{ color: 'gray.400' }}
- color="gray.600"
- >
- <Trans components={{ b: <b /> }}>
- {t('revokeUserSub.prompt', { username: user.username })}
- </Trans>
- </Text>
- )}
- </ModalBody>
- <ModalFooter display="flex">
- <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
- {t('cancel')}
- </Button>
- <Button
- size="sm"
- w="full"
- colorScheme="blue"
- onClick={onReset}
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- >
- {t('revoke')}
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/table/action-buttons.tsx b/app/dashboard/src/pages/hosts/users/table/action-buttons.tsx
deleted file mode 100644
index b44941d..0000000
--- a/app/dashboard/src/pages/hosts/users/table/action-buttons.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-
-import { t } from 'i18next';
-import CopyToClipboard from 'react-copy-to-clipboard';
-import { User } from 'types';
-import {
- CopiedIcon,
- CopyIcon,
- QRIcon,
- SubscriptionLinkIcon
-} from 'components/table';
-import {
- HStack,
- IconButton,
- Tooltip,
-} from '@chakra-ui/react';
-import { FC, useEffect, useState } from 'react';
-import { useUsers } from 'stores';
-
-type ActionButtonsProps = {
- user: User;
-};
-
-export const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
- const { setQRCode, setSubLink } = useUsers();
-
- const proxyLinks = user.links.join('\r\n');
-
- const [copied, setCopied] = useState([-1, false]);
- useEffect(() => {
- if (copied[1]) {
- setTimeout(() => {
- setCopied([-1, false]);
- }, 1000);
- }
- }, [copied]);
- return (
- <HStack
- justifyContent="flex-end"
- onClick={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- >
- <CopyToClipboard
- text={
- user.subscription_url.startsWith('/')
- ? window.location.origin + user.subscription_url
- : user.subscription_url
- }
- onCopy={() => {
- setCopied([0, true]);
- }}
- >
- <div>
- <Tooltip
- label={
- copied[0] == 0 && copied[1]
- ? t('usersTable.copied')
- : t('usersTable.copyLink')
- }
- placement="top"
- >
- <IconButton
- p="0 !important"
- aria-label="copy subscription link"
- bg="transparent"
- _dark={{
- _hover: {
- bg: 'gray.700',
- },
- }}
- size={{
- base: 'sm',
- md: 'md',
- }}
- >
- {copied[0] == 0 && copied[1] ? (
- <CopiedIcon />
- ) : (
- <SubscriptionLinkIcon />
- )}
- </IconButton>
- </Tooltip>
- </div>
- </CopyToClipboard>
- <CopyToClipboard
- text={proxyLinks}
- onCopy={() => {
- setCopied([1, true]);
- }}
- >
- <div>
- <Tooltip
- label={
- copied[0] == 1 && copied[1]
- ? t('usersTable.copied')
- : t('usersTable.copyConfigs')
- }
- placement="top"
- >
- <IconButton
- p="0 !important"
- aria-label="copy configs"
- bg="transparent"
- _dark={{
- _hover: {
- bg: 'gray.700',
- },
- }}
- size={{
- base: 'sm',
- md: 'md',
- }}
- >
- {copied[0] == 1 && copied[1] ? <CopiedIcon /> : <CopyIcon />}
- </IconButton>
- </Tooltip>
- </div>
- </CopyToClipboard>
- <Tooltip label="QR Code" placement="top">
- <IconButton
- p="0 !important"
- aria-label="qr code"
- bg="transparent"
- _dark={{
- _hover: {
- bg: 'gray.700',
- },
- }}
- size={{
- base: 'sm',
- md: 'md',
- }}
- onClick={() => {
- setQRCode(user.links);
- setSubLink(user.subscription_url);
- }}
- >
- <QRIcon />
- </IconButton>
- </Tooltip>
- </HStack >
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/table/index.tsx b/app/dashboard/src/pages/hosts/users/table/index.tsx
deleted file mode 100644
index b6a0015..0000000
--- a/app/dashboard/src/pages/hosts/users/table/index.tsx
+++ /dev/null
@@ -1,408 +0,0 @@
-
-import {
- Accordion,
- AccordionButton,
- AccordionItem,
- AccordionPanel,
- Box,
- HStack,
- IconButton,
- Table,
- TableProps,
- Tbody,
- Td,
- Text,
- Th,
- Thead,
- Tooltip,
- Tr,
- useBreakpointValue,
- VStack,
-} from '@chakra-ui/react';
-import classNames from 'classnames';
-import { statusColors } from 'constants/Settings';
-import { FC, Fragment, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-import { OnlineBadge } from 'components/online-badge';
-import { OnlineStatus } from 'components/online-status';
-import { StatusBadge } from 'components/status-badge';
-import {
- Pagination,
- EmptySection,
- AccordionArrowIcon,
- handleSort,
- EditIcon,
- StatusSortSelect,
- Sort,
-} from 'components/table';
-import { UsageSlider, UsageSliderCompact } from './usage-slider';
-import { useUsers } from 'stores';
-import { ActionButtons } from './action-buttons';
-import { pageSizeManagers } from 'utils/userPreferenceStorage';
-
-type ExpandedIndex = number | number[];
-
-type UsersTableProps = {} & TableProps;
-
-export const UsersTable: FC<UsersTableProps> = (props) => {
- const {
- usersFilters: filters,
- users: { users },
- users: { total },
- onEditingUser,
- onCreateUser,
- onFilterChange,
- } = useUsers();
-
- const { t } = useTranslation();
- const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
- undefined
- );
- const useTable = useBreakpointValue({ base: false, md: true });
-
- const isFiltered = users.length !== total;
-
- const handleStatusFilter = (e: any) => {
- onFilterChange({
- status: e.target.value.length > 0 ? e.target.value : undefined,
- });
- };
-
- const toggleAccordion = (index: number) => {
- setSelectedRow(index === selectedRow ? undefined : index);
- };
- return (
- <Box id="users-table" overflowX={{ base: 'unset', md: 'unset' }}>
- <Accordion
- allowMultiple
- display={{ base: 'block', md: 'none' }}
- index={selectedRow}
- >
- <Table orientation="vertical" zIndex="docked" {...props}>
- <Thead zIndex="docked" position="relative">
- <Tr>
- <Th
- position="sticky"
- minW="120px"
- pl={4}
- pr={4}
- cursor={'pointer'}
- onClick={handleSort.bind(null, filters, 'username', onFilterChange)}
- >
- <HStack>
- <span>{t('users')}</span>
- <Sort sort={filters.sort} column="username" />
- </HStack>
- </Th>
- <Th
- position="sticky"
- minW="50px"
- pl={0}
- pr={0}
- w="140px"
- cursor={'pointer'}
- >
- <HStack spacing={0} position="relative">
- <Text
- position="absolute"
- _dark={{
- bg: 'gray.750',
- }}
- _light={{
- bg: '#F9FAFB',
- }}
- userSelect="none"
- pointerEvents="none"
- zIndex={1}
- w="100%"
- >
- {t('usersTable.status')}
- {filters.status ? ': ' + filters.status : ''}
- </Text>
- <StatusSortSelect filters={filters} handleStatusFilter={handleStatusFilter} options={['active', 'disabled', 'limited', 'expired', 'on_hold']} />
- </HStack>
- </Th>
- <Th
- position="sticky"
- minW="100px"
- cursor={'pointer'}
- pr={0}
- onClick={handleSort.bind(null, filters, 'used_traffic', onFilterChange)}
- >
- <HStack>
- <span>{t('usersTable.dataUsage')}</span>
- <Sort sort={filters.sort} column="used_traffic" />
- </HStack>
- </Th>
- <Th
- position="sticky"
- minW="32px"
- w="32px"
- p={0}
- cursor={'pointer'}
- ></Th>
- </Tr>
- </Thead>
- <Tbody>
- {!useTable &&
- users?.map((user, i) => {
- return (
- <Fragment key={user.username}>
- <Tr
- onClick={toggleAccordion.bind(null, i)}
- cursor="pointer"
- >
- <Td borderBottom={0} minW="100px" pl={4} pr={4}>
- <div className="flex-status">
- <OnlineBadge lastOnline={user.online_at} />
- {user.username}
- </div>
- </Td>
- <Td borderBottom={0} minW="50px" pl={0} pr={0}>
- <StatusBadge
- compact
- showDetail={false}
- expiryDate={user.expire}
- status={user.status}
- />
- </Td>
- <Td borderBottom={0} minW="100px" pr={0}>
- <UsageSliderCompact
- totalUsedTraffic={user.lifetime_used_traffic}
- dataLimitResetStrategy={
- user.data_limit_reset_strategy
- }
- used={user.used_traffic}
- total={user.data_limit}
- colorScheme={statusColors[user.status].bandWidthColor}
- />
- </Td>
- <Td p={0} borderBottom={0} w="32px" minW="32px">
- <AccordionArrowIcon
- color="gray.600"
- _dark={{
- color: 'gray.400',
- }}
- transition="transform .2s ease-out"
- transform={
- selectedRow === i ? 'rotate(180deg)' : '0deg'
- }
- />
- </Td>
- </Tr>
- <Tr
- className="collapsible"
- onClick={toggleAccordion.bind(null, i)}
- >
- <Td p={0} colSpan={4}>
- <AccordionItem border={0}>
- <AccordionButton display="none"></AccordionButton>
- <AccordionPanel
- border={0}
- cursor="pointer"
- px={6}
- py={3}
- >
- <VStack justifyContent="space-between" spacing="4">
- <VStack
- alignItems="flex-start"
- w="full"
- spacing={-1}
- >
- <Text
- textTransform="capitalize"
- fontSize="xs"
- fontWeight="bold"
- color="gray.600"
- _dark={{
- color: 'gray.400',
- }}
- >
- {t('usersTable.dataUsage')}
- </Text>
- <Box width="full" minW="230px">
- <UsageSlider
- totalUsedTraffic={
- user.lifetime_used_traffic
- }
- dataLimitResetStrategy={
- user.data_limit_reset_strategy
- }
- used={user.used_traffic}
- total={user.data_limit}
- colorScheme={
- statusColors[user.status].bandWidthColor
- }
- />
- </Box>
- </VStack>
- <HStack w="full" justifyContent="space-between">
- <Box width="full">
- <StatusBadge
- compact
- expiryDate={user.expire}
- status={user.status}
- />
- <OnlineStatus lastOnline={user.online_at} />
- </Box>
- <HStack>
- <ActionButtons user={user} />
- <Tooltip
- label={t('userDialog.editUser')}
- placement="top"
- >
- <IconButton
- p="0 !important"
- aria-label="Edit user"
- bg="transparent"
- _dark={{
- _hover: {
- bg: 'gray.700',
- },
- }}
- size={{
- base: 'sm',
- md: 'md',
- }}
- onClick={(e) => {
- e.stopPropagation();
- onEditingUser(user);
- }}
- >
- <EditIcon />
- </IconButton>
- </Tooltip>
- </HStack>
- </HStack>
- </VStack>
- </AccordionPanel>
- </AccordionItem>
- </Td>
- </Tr>
- </Fragment>
- );
- })}
- </Tbody>
- </Table>
- </Accordion>
- <Table
- orientation="vertical"
- display={{ base: 'none', md: 'table' }}
- {...props}
- >
- <Thead zIndex="docked" position="relative">
- <Tr>
- <Th
- position="sticky"
- minW="140px"
- cursor={'pointer'}
- onClick={handleSort.bind(null, filters, 'username', onFilterChange)}
- >
- <HStack>
- <span>{t('username')}</span>
- <Sort sort={filters.sort} column="username" />
- </HStack>
- </Th>
- <Th
- position="sticky"
- width="400px"
- minW="150px"
- cursor={'pointer'}
- >
- <HStack spacing={0} position="relative">
- <Text
- position="absolute"
- _dark={{
- bg: 'gray.750',
- }}
- _light={{
- bg: '#F9FAFB',
- }}
- userSelect="none"
- pointerEvents="none"
- zIndex={1}
- w="100%"
- >
- {t('usersTable.status')}
- {filters.status ? ': ' + filters.status : ''}
- </Text>
- <StatusSortSelect filters={filters} handleStatusFilter={handleStatusFilter} options={['active', 'disabled', 'limited', 'expired', 'on_hold']} />
- </HStack>
- </Th>
- <Th
- position="sticky"
- width="350px"
- minW="230px"
- cursor={'pointer'}
- onClick={handleSort.bind(null, filters, 'used_traffic', onFilterChange)}
- >
- <HStack>
- <span>{t('usersTable.dataUsage')}</span>
- <Sort sort={filters.sort} column="used_traffic" />
- </HStack>
- </Th>
- <Th
- position="sticky"
- width="200px"
- minW="180px"
- />
- </Tr>
- </Thead>
- <Tbody>
- {useTable &&
- users?.map((user, i) => {
- return (
- <Tr
- key={user.username}
- className={classNames('interactive', {
- 'last-row': i === users.length - 1,
- })}
- onClick={() => onEditingUser(user)}
- >
- <Td minW="140px">
- <div className="flex-status">
- <OnlineBadge lastOnline={user.online_at} />
- {user.username}
- <OnlineStatus lastOnline={user.online_at} />
- </div>
- </Td>
- <Td width="400px" minW="150px">
- <StatusBadge
- expiryDate={user.expire}
- status={user.status}
- />
- </Td>
- <Td width="350px" minW="230px">
- <UsageSlider
- totalUsedTraffic={user.lifetime_used_traffic}
- dataLimitResetStrategy={user.data_limit_reset_strategy}
- used={user.used_traffic}
- total={user.data_limit}
- colorScheme={statusColors[user.status].bandWidthColor}
- />
- </Td>
- <Td width="200px" minW="180px">
- <ActionButtons user={user} />
- </Td>
- </Tr>
- );
- })}
- {users.length == 0 && (
- <Tr>
- <Td colSpan={4}>
- <EmptySection
- isFiltered={isFiltered}
- noObjectT="usersTable.noUser"
- noObjectMatchedT='usersTable.noUserMatched'
- createObjectT="createUser"
- onCreateObject={onCreateUser}
- />
- </Td>
- </Tr>
- )}
- </Tbody>
- </Table>
- <Pagination filters={filters} total={total} onFilterChange={onFilterChange} pageSizeManager={pageSizeManagers.users} />
- </Box>
- );
-};
diff --git a/app/dashboard/src/pages/hosts/users/table/usage-slider.tsx b/app/dashboard/src/pages/hosts/users/table/usage-slider.tsx
deleted file mode 100644
index cf17e65..0000000
--- a/app/dashboard/src/pages/hosts/users/table/usage-slider.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { HStack, Text, SliderTrack, SliderFilledTrack, Slider, SliderProps } from '@chakra-ui/react';
-import { resetStrategy } from 'constants/Settings';
-import { formatBytes } from 'utils/formatByte';
-import { useTranslation } from 'react-i18next';
-import { FC } from 'react';
-
-export const UsageSliderCompact: FC<UsageSliderProps> = (props) => {
- const { used, total } = props;
- const isUnlimited = total === 0 || total === null;
- return (
- <HStack
- justifyContent="space-between"
- fontSize="xs"
- fontWeight="medium"
- color="gray.600"
- _dark={{
- color: 'gray.400',
- }}
- >
- <Text>
- {formatBytes(used)} /{' '}
- {isUnlimited ? (
- <Text as="span" fontFamily="system-ui">
- ∞
- </Text>
- ) : (
- formatBytes(total)
- )}
- </Text>
- </HStack>
- );
-};
-
-type UsageSliderProps = {
- used: number;
- total: number | null;
- dataLimitResetStrategy: string | null;
- totalUsedTraffic: number;
-} & SliderProps;
-
-const getResetStrategy = (strategy: string): string => {
- for (var i = 0; i < resetStrategy.length; i++) {
- const entry = resetStrategy[i];
- if (entry.value == strategy) {
- return entry.title;
- }
- }
- return 'No';
-};
-
-export const UsageSlider: FC<UsageSliderProps> = (props) => {
- const {
- used,
- total,
- dataLimitResetStrategy,
- totalUsedTraffic,
- ...restOfProps
- } = props;
- const isUnlimited = total === 0 || total === null;
- const isReached = !isUnlimited && (used / total) * 100 >= 100;
- const { t } = useTranslation();
- return (
- <>
- <Slider
- orientation="horizontal"
- value={isUnlimited ? 100 : Math.min((used / total) * 100, 100)}
- colorScheme={isReached ? 'red' : 'primary'}
- {...restOfProps}
- >
- <SliderTrack h="6px" borderRadius="full">
- <SliderFilledTrack borderRadius="full" />
- </SliderTrack>
- </Slider>
- <HStack
- justifyContent="space-between"
- fontSize="xs"
- fontWeight="medium"
- color="gray.600"
- _dark={{
- color: 'gray.400',
- }}
- >
- <Text>
- {formatBytes(used)} /{' '}
- {isUnlimited ? (
- <Text as="span" fontFamily="system-ui">
- ∞
- </Text>
- ) : (
- formatBytes(total) +
- (dataLimitResetStrategy && dataLimitResetStrategy !== 'no_reset'
- ? ' ' +
- t(
- 'userDialog.resetStrategy' +
- getResetStrategy(dataLimitResetStrategy)
- )
- : '')
- )}
- </Text>
- <Text>
- {t('usersTable.total')}: {formatBytes(totalUsedTraffic)}
- </Text>
- </HStack>
- </>
- );
-};
diff --git a/app/dashboard/src/pages/nodes/table/index.tsx b/app/dashboard/src/pages/nodes/table/index.tsx
index b6c4f75..ecb5c6e 100644
--- a/app/dashboard/src/pages/nodes/table/index.tsx
+++ b/app/dashboard/src/pages/nodes/table/index.tsx
@@ -34,7 +34,9 @@ import {
StatusSortSelect,
handleSort,
} from 'components/table';
-import { useNodes } from 'stores';
+import { fetchNodes, useNodes } from 'stores';
+import { useQuery } from 'react-query';
+import { queryIds } from 'constants/query-ids';
type ExpandedIndex = number | number[];
@@ -43,13 +45,12 @@ type NodesTableProps = {} & TableProps;
export const NodesTable: FC<NodesTableProps> = (props) => {
const {
nodesFilters: filters,
- nodes,
onEditingNode,
onAddingNode,
onFilterChange,
} = useNodes();
-
- const total = nodes.length;
+ const { data: nodes } = useQuery(queryIds.nodes, () => { return fetchNodes() })
+ const total = nodes?.length || 0;
const { t } = useTranslation();
const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
@@ -373,7 +374,7 @@ export const NodesTable: FC<NodesTableProps> = (props) => {
</Tr>
);
})}
- {nodes.length == 0 && (
+ {nodes?.length == 0 && (
<Tr>
<Td colSpan={5}>
<EmptySection
diff --git a/app/dashboard/src/pages/services/dialog/index.tsx b/app/dashboard/src/pages/services/dialog/index.tsx
index 3f6e0c8..e5e37fc 100644
--- a/app/dashboard/src/pages/services/dialog/index.tsx
+++ b/app/dashboard/src/pages/services/dialog/index.tsx
@@ -32,7 +32,9 @@ import { schema, FormType } from './form-schema';
import { InboundsField } from './inbounds-field';
import { NameField } from './name-field';
import { InboundType, Service, ServiceCreate } from 'types';
-import { useInbounds, useServices } from 'stores';
+import { fetchInbounds, useInbounds, useServices } from 'stores';
+import { useQuery } from 'react-query';
+import { queryIds } from 'constants/query-ids';
const formatService = (service: Service): FormType => {
const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => {
@@ -56,10 +58,10 @@ export const ServiceDialog: FC<ServiceDialogProps> = () => {
createService,
onDeletingService,
} = useServices();
+ const { refetchInbounds } = useInbounds();
const {
- inbounds,
- refetchInbounds,
- } = useInbounds();
+ data: inbounds,
+ } = useQuery(queryIds.inbounds, () => fetchInbounds());
const isEditing = !!editingService;
const isOpen = isCreatingNewService || isEditing;
const [loading, setLoading] = useState(false);
@@ -149,7 +151,7 @@ export const ServiceDialog: FC<ServiceDialogProps> = () => {
w="full"
>
<NameField form={form} disabled={disabled} isEditing={isEditing} t={t} />
- <InboundsField t={t} inbounds={inbounds} form={form} />
+ <InboundsField t={t} inbounds={inbounds || []} form={form} />
</Flex>
{error && (
<Alert
diff --git a/app/dashboard/src/pages/services/table.tsx b/app/dashboard/src/pages/services/table.tsx
index 905fc9f..ba31756 100644
--- a/app/dashboard/src/pages/services/table.tsx
+++ b/app/dashboard/src/pages/services/table.tsx
@@ -22,8 +22,10 @@ import {
Pagination,
Sort
} from 'components/table';
-import { useServices } from 'stores';
+import { fetchServices, useServices } from 'stores';
import { pageSizeManagers } from 'utils/userPreferenceStorage';
+import { useQuery } from 'react-query';
+import { queryIds } from 'constants/query-ids';
type ExpandedIndex = number | number[];
type ServicesTableProps = {} & TableProps;
@@ -31,11 +33,11 @@ type ServicesTableProps = {} & TableProps;
export const ServicesTable: FC<ServicesTableProps> = (props) => {
const {
servicesFilters: filters,
- services,
onEditingService,
onCreateService,
onFilterChange,
} = useServices();
+ const { data: services } = useQuery(queryIds.services, () => { return fetchServices(filters) });
const useTable = useBreakpointValue({ base: false, md: true });
const { t } = useTranslation();
const [selectedRow,] = useState<ExpandedIndex | undefined>(
@@ -192,7 +194,7 @@ export const ServicesTable: FC<ServicesTableProps> = (props) => {
</Tr>
);
})}
- {services.length == 0 && (
+ {services?.length == 0 && (
<Tr>
<Td colSpan={3}>
<EmptySection
@@ -207,8 +209,8 @@ export const ServicesTable: FC<ServicesTableProps> = (props) => {
)}
</Tbody>
</Table>
- <Pagination
- total={services.length}
+ <Pagination
+ total={services?.length || 0}
onFilterChange={onFilterChange}
filters={filters}
pageSizeManager={pageSizeManagers.services}
diff --git a/app/dashboard/src/pages/users/dialog/index.tsx b/app/dashboard/src/pages/users/dialog/index.tsx
index 6ca47dc..e9022a0 100644
--- a/app/dashboard/src/pages/users/dialog/index.tsx
+++ b/app/dashboard/src/pages/users/dialog/index.tsx
@@ -49,7 +49,9 @@ import { DataLimitField } from './data-limit-field';
import { PeriodicUsageReset } from './periodic-usage-reset';
import { ExpireDateField } from './expire-date-field';
import { NoteField } from './note-field';
-import { useUsers, useServices } from 'stores';
+import { useUsers, useServices, fetchServices } from 'stores';
+import { queryIds } from 'constants/query-ids';
+import { useQuery } from 'react-query';
const formatUser = (user: User): FormType => {
const services: number[] = user.services.map((service: number | Service): number => {
@@ -77,7 +79,14 @@ export const UserDialog: FC<UserDialogProps> = () => {
createUser,
onDeletingUser,
} = useUsers();
- const { refetchServices, services } = useServices();
+ const { refetchServices, servicesFilters: filters } = useServices();
+ const { data: services } = useQuery({
+ queryKey: queryIds.services,
+ initialData: [],
+ queryFn: () => {
+ return fetchServices(filters);
+ }
+ });
const isEditing = !!editingUser;
const isOpen = isCreatingNewUser || isEditing;
const [loading, setLoading] = useState(false);
@@ -259,7 +268,7 @@ export const UserDialog: FC<UserDialogProps> = () => {
</GridItem>
<GridItem>
- <ServicesField t={t} services={services} form={form} />
+ <ServicesField t={t} services={services || []} form={form} />
</GridItem>
{isEditing && usageVisible && (
diff --git a/app/dashboard/src/pages/users/table/index.tsx b/app/dashboard/src/pages/users/table/index.tsx
index b6a0015..d19e863 100644
--- a/app/dashboard/src/pages/users/table/index.tsx
+++ b/app/dashboard/src/pages/users/table/index.tsx
@@ -36,9 +36,12 @@ import {
Sort,
} from 'components/table';
import { UsageSlider, UsageSliderCompact } from './usage-slider';
-import { useUsers } from 'stores';
+import { fetchUsers, useUsers } from 'stores';
import { ActionButtons } from './action-buttons';
import { pageSizeManagers } from 'utils/userPreferenceStorage';
+import { useQuery } from 'react-query';
+import { User } from 'types';
+import { queryIds } from 'constants/query-ids';
type ExpandedIndex = number | number[];
@@ -47,27 +50,27 @@ type UsersTableProps = {} & TableProps;
export const UsersTable: FC<UsersTableProps> = (props) => {
const {
usersFilters: filters,
- users: { users },
- users: { total },
onEditingUser,
onCreateUser,
onFilterChange,
} = useUsers();
-
+ const { data } = useQuery({
+ queryKey: queryIds.users,
+ queryFn: () => { return fetchUsers(filters) },
+ initialData: { total: 0, users: [] },
+ });
const { t } = useTranslation();
const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
undefined
);
const useTable = useBreakpointValue({ base: false, md: true });
- const isFiltered = users.length !== total;
-
const handleStatusFilter = (e: any) => {
onFilterChange({
status: e.target.value.length > 0 ? e.target.value : undefined,
});
};
-
+ const isFiltered = data?.total !== data?.users.length;
const toggleAccordion = (index: number) => {
setSelectedRow(index === selectedRow ? undefined : index);
};
@@ -145,7 +148,7 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
</Thead>
<Tbody>
{!useTable &&
- users?.map((user, i) => {
+ data?.users.map((user, i) => {
return (
<Fragment key={user.username}>
<Tr
@@ -350,12 +353,12 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
</Thead>
<Tbody>
{useTable &&
- users?.map((user, i) => {
+ data?.users.map((user: User, i) => {
return (
<Tr
key={user.username}
className={classNames('interactive', {
- 'last-row': i === users.length - 1,
+ 'last-row': i === data?.users.length - 1,
})}
onClick={() => onEditingUser(user)}
>
@@ -387,7 +390,7 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
</Tr>
);
})}
- {users.length == 0 && (
+ {data?.users.length == 0 && (
<Tr>
<Td colSpan={4}>
<EmptySection
@@ -402,7 +405,7 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
)}
</Tbody>
</Table>
- <Pagination filters={filters} total={total} onFilterChange={onFilterChange} pageSizeManager={pageSizeManagers.users} />
+ <Pagination filters={filters} total={data?.total || 0} onFilterChange={onFilterChange} pageSizeManager={pageSizeManagers.users} />
</Box>
);
};
diff --git a/app/dashboard/src/stores/inbounds.store.ts b/app/dashboard/src/stores/inbounds.store.ts
index 6c5f9d5..b8645d9 100644
--- a/app/dashboard/src/stores/inbounds.store.ts
+++ b/app/dashboard/src/stores/inbounds.store.ts
@@ -7,6 +7,7 @@ import { z } from 'zod';
import { HostsFilterType, InboundsFilterType } from 'types';
import { pageSizeManagers } from 'utils/userPreferenceStorage';
import { queryClient } from 'service/react-query';
+import { queryIds } from 'constants/query-ids';
const isPortThenValue = (value: number) => (value <= 65535 && value !== 0) ? value : null;
@@ -37,9 +38,7 @@ export type Hosts = HostType[];
export const fetchInbounds = async () => {
useDashboard.setState({ loading: true });
return fetch('/inbounds')
- .then((inbounds: Inbounds) => {
- useInbounds.setState({ inbounds });
- })
+ .then((inbounds: Inbounds) => inbounds)
.finally(() => {
useDashboard.setState({ loading: false });
});
@@ -59,10 +58,9 @@ type InboundsStateType = {
loading: boolean;
setLoading: (value: boolean) => void;
// Inbounds
- inbounds: Inbounds;
- refetchInbounds: () => void;
selectedInbound: InboundType | null;
selectInbound: (inbound: InboundType) => void;
+ refetchInbounds: () => void;
// Hosts
refetchHosts: () => void;
hostsFilters: HostsFilterType;
@@ -84,7 +82,6 @@ type InboundsStateType = {
export const useInbounds = create(
subscribeWithSelector<InboundsStateType>((set, get) => ({
- inbounds: [],
selectedHost: null,
selectedInbound: null,
loading: false,
@@ -121,10 +118,10 @@ export const useInbounds = create(
set({ loading: value })
},
refetchInbounds: () => {
- fetchInbounds();
+ queryClient.invalidateQueries(queryIds.inbounds);
},
refetchHosts: () => {
- queryClient.invalidateQueries('hosts');
+ queryClient.invalidateQueries(queryIds.hosts);
},
selectInbound: (host): void => {
set({ selectedInbound: host })
diff --git a/app/dashboard/src/stores/nodes.store.ts b/app/dashboard/src/stores/nodes.store.ts
index 0daad66..9cecb45 100644
--- a/app/dashboard/src/stores/nodes.store.ts
+++ b/app/dashboard/src/stores/nodes.store.ts
@@ -5,6 +5,8 @@ import { z } from 'zod';
import { FilterUsageType, NodesFilterType } from 'types/filter';
import { pageSizeManagers } from 'utils/userPreferenceStorage';
import { useDashboard } from './dashboard.store';
+import { queryClient } from 'service/react-query';
+import { queryIds } from 'constants/query-ids';
export const NodeSchema = z.object({
name: z.string().min(1),
@@ -61,12 +63,16 @@ export const getNodeCreateDefaultValues = (): NodeCreate => ({
export const FetchNodesQueryKey = 'fetch-nodes-query-key';
-export function fetchNodes() {
- return fetch('/nodes');
+export function fetchNodes(): Promise<NodeType[]> {
+ useDashboard.setState({ loading: true })
+ return fetch('/nodes').then((nodes) => {
+ return nodes;
+ }).finally(() => {
+ useDashboard.setState({ loading: false })
+ });
}
export type NodeStore = {
- nodes: NodeType[];
certificate: string,
deletingNode?: NodeCreate | null;
editingNode: NodeCreate | null;
@@ -99,7 +105,6 @@ export const useNodesQuery = () => {
};
export const useNodes = create<NodeStore>((set, get) => ({
- nodes: [],
certificate: '',
editingNode: null,
deletingNode: null,
@@ -125,11 +130,7 @@ export const useNodes = create<NodeStore>((set, get) => ({
return fetch('/nodes', { method: 'POST', body }).then(() => { get().refetchCertificate() });
},
refetchNodes: () => {
- useDashboard.setState({ loading: true })
- fetchNodes().then((nodes) => {
- useDashboard.setState({ loading: false })
- set({ nodes });
- })
+ queryClient.invalidateQueries(queryIds.nodes);
},
refetchCertificate: () => {
fetch('/nodes/settings').then((res) => {
diff --git a/app/dashboard/src/stores/services.store.ts b/app/dashboard/src/stores/services.store.ts
index 89c636e..a4a088e 100644
--- a/app/dashboard/src/stores/services.store.ts
+++ b/app/dashboard/src/stores/services.store.ts
@@ -7,9 +7,9 @@ import { subscribeWithSelector } from 'zustand/middleware';
import { fetch } from 'service/http';
import { ServicesFilterType } from 'types/filter';
import { useDashboard, useUsers } from 'stores';
+import { queryIds } from 'constants/query-ids';
type ServicesStateType = {
- services: Service[],
isCreatingNewService: boolean;
editingService: Service | null | undefined;
deletingService: Service | null;
@@ -33,7 +33,6 @@ export const fetchServices = async (query: ServicesFilterType): Promise<Service[
return fetch('/services')
.then((services) => {
- useServices.setState({ services });
return services;
})
.finally(() => {
@@ -46,7 +45,6 @@ export const useServices = create(
editingService: null,
deletingService: null,
isCreatingNewService: false,
- services: [],
servicesFilters: {
name: '',
limit: pageSizeManagers.services.getPageSize(),
@@ -62,7 +60,7 @@ export const useServices = create(
get().refetchServices();
},
refetchServices: () => {
- fetchServices(get().servicesFilters);
+ queryClient.invalidateQueries(queryIds.services);
},
onCreateService: (isCreatingNewService) => set({ isCreatingNewService }),
onEditingService: (editingService) => {
diff --git a/app/dashboard/src/stores/users.store.ts b/app/dashboard/src/stores/users.store.ts
index 4a806be..8a4c3df 100644
--- a/app/dashboard/src/stores/users.store.ts
+++ b/app/dashboard/src/stores/users.store.ts
@@ -7,15 +7,12 @@ import { useServices, useDashboard } from 'stores';
import { StatisticsQueryKey } from 'components/statistics-card';
import { queryClient } from 'service/react-query';
import { UsersFilterType, FilterUsageType } from 'types/filter';
+import { queryIds } from 'constants/query-ids';
type UsersStateType = {
isCreatingNewUser: boolean;
editingUser: User | null | undefined;
deletingUser: User | null;
- users: {
- users: User[];
- total: number;
- };
usersFilters: UsersFilterType;
resetUsageUser: User | null;
revokeSubscriptionUser: User | null;
@@ -39,7 +36,12 @@ type UsersStateType = {
fetchUserUsage: (user: User, query: FilterUsageType) => Promise<void>;
}
-export const fetchUsers = async (query: UsersFilterType): Promise<User[]> => {
+type UserResponse = {
+ total: number,
+ users: User[]
+}
+
+export const fetchUsers = async (query: UsersFilterType): Promise<UserResponse> => {
for (const key in query) {
if (!query[key as keyof UsersFilterType]) delete query[key as keyof UsersFilterType];
}
@@ -49,7 +51,6 @@ export const fetchUsers = async (query: UsersFilterType): Promise<User[]> => {
for (let i = 0; i < users.users.length; i++) {
users.users[i].service = users.users[i].service_ids;
}
- useUsers.setState({ users });
return users;
})
.finally(() => {
@@ -61,10 +62,6 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
editingUser: null,
deletingUser: null,
isCreatingNewUser: false,
- users: {
- users: [],
- total: 0,
- },
resetUsageUser: null,
revokeSubscriptionUser: null,
isResetingAllUsage: false,
@@ -82,7 +79,7 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
set({ subscribeUrl });
},
refetchUsers: () => {
- fetchUsers(get().usersFilters);
+ queryClient.invalidateQueries(queryIds.users);
},
resetAllUsage: async () => {
return fetch('/users/reset', { method: 'POST' }).then(() => {
diff --git a/makefile b/makefile
index fe53b3d..4b63f02 100644
--- a/makefile
+++ b/makefile
@@ -11,9 +11,10 @@ dashboard-build:
cp ./app/dashboard/dist/index.html ./app/dashboard/dist/404.html
dashboard-dev:
- npm run dev \
+ VITE_BASE_API=http://0.0.0.0:8000/api/ npm run dev \
+ --prefix './app/dashboard/' \
-- --host 0.0.0.0 \
- --base ../app/dashboard/ \
+ --base ./app/dashboard/ \
--clearScreen false
dashboard-cleanup: