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 ( @@ -37,7 +38,7 @@ export const InboundsSidebar = () => { - {inbounds.map((inbound, i) => { + {inbounds?.map((inbound, i) => { const active = inbounds[i] == selectedInbound return ( { onClick={() => { selectInbound(inbound) }}>
; -} - -export const DataLimitField = ({ form, disabled, t }: DataLimitFieldProps) => { - return ( - - {t('userDialog.dataLimit')} - { - return ( - - ); - }} - /> - - ) -} 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 ( - - {t('userDialog.expiryDate')} - { - 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 ( - <> - { - field.onChange({ - target: { - value: date - ? dayjs( - dayjs(date) - .set('hour', 23) - .set('minute', 59) - .set('second', 59) - ) - .utc() - .valueOf() / 1000 - : 0, - name: 'expire', - }, - }); - }} - customInput={ - - } - /> - {field.value ? ( - - {t(status, { time: time })} - - ) : ( - '' - )} - - ); - }} - /> - - ) -} 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; -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 = () => { - 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(''); - const toast = useToast(); - const { t, i18n } = useTranslation(); - - const { colorMode } = useColorMode(); - - const [usageVisible, setUsageVisible] = useState(false); - const handleUsageToggle = () => { - setUsageVisible((current) => !current); - }; - - const form = useForm({ - 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 ( - - - - -
- - - - - - - - - - - 0)} - animateOpacity - style={{ width: '100%' }} - > - - - - - - {error && ( - - - {error} - - )} - - - - - - - - {isEditing && usageVisible && ( - - - { - setUsageFilter(filter); - fetchUsageWithFilter(query); - }} - /> - - - - - - )} - - - {error && ( - - - {error} - - )} - - - - {isEditing && editingUser !== null && ( - <> - - { - onDeletingUser(editingUser); - onClose(); - }} - > - - - - - - - - - - - - )} - - - - - - -
-
-
- ); -}; 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 ( - - {t('userDialog.note')} -