diff --git a/app/dashboard/src/components/CoreSettingsModal.tsx b/app/dashboard/src/components/CoreSettingsModal.tsx
@@ -28,7 +28,7 @@ import {
import { joinPaths } from '@remix-run/router';
import classNames from 'classnames';
import { useCoreSettings } from 'contexts/CoreSettingsContext';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useNodes } from 'contexts/NodesContext';
import debounce from 'lodash.debounce';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
@@ -102,7 +102,7 @@ const getWebsocketUrl = () => {
let logsTmp: string[] = [];
const CoreSettingModalContent: FC = () => {
- const { isEditingCore } = useDashboard();
+ const { isEditingCore } = useNodes();
const {
fetchCoreSettings,
updateConfig,
@@ -314,8 +314,8 @@ const CoreSettingModalContent: FC = () => {
);
};
export const CoreSettingsModal: FC = () => {
- const { isEditingCore } = useDashboard();
- const onClose = useDashboard.setState.bind(null, { isEditingCore: false });
+ const { isEditingCore } = useNodes();
+ const onClose = useNodes.setState.bind(null, { isEditingCore: false });
const { t } = useTranslation();
return (
diff --git a/app/dashboard/src/components/DeleteUserModal.tsx b/app/dashboard/src/components/DeleteUserModal.tsx
@@ -12,18 +12,18 @@ import {
Text,
useToast,
} from '@chakra-ui/react';
-import { useDashboard } from 'contexts/DashboardContext';
import { FC, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Icon } from './Icon';
+import { useUsers } from 'contexts/UsersContext';
export type DeleteUserModalProps = {
- deleteCallback?: () => void;
+ deleteCallback?: () => void;
};
export const DeleteUserModal: FC<DeleteUserModalProps> = () => {
const [loading, setLoading] = useState(false);
- const { deletingUser: user, onDeletingUser, deleteUser } = useDashboard();
+ const { deletingUser: user, onDeletingUser, deleteUser } = useUsers();
const { t } = useTranslation();
const toast = useToast();
const onClose = () => {
diff --git a/app/dashboard/src/components/Footer.tsx b/app/dashboard/src/components/Footer.tsx
@@ -18,7 +18,7 @@ export const Footer: FC<BoxProps> = (props) => {
Marzneshin
</Link>
{version ? ` (v${version}), ` : ', '}
- Made with ❤️ in{' '}
+ Made in{' '}
<Link color="blue.400" href={ORGANIZATION_URL}>
khodedawosh
</Link>
diff --git a/app/dashboard/src/components/Header.tsx b/app/dashboard/src/components/Header.tsx
@@ -25,6 +25,9 @@ import {
import { pages } from 'constants/Pages';
import { DONATION_URL, REPO_URL } from 'constants/Project';
import { useDashboard } from 'contexts/DashboardContext';
+import { useHosts } from 'contexts/HostsContext';
+import { useNodes } from 'contexts/NodesContext';
+import { useUsers } from 'contexts/UsersContext';
import differenceInDays from 'date-fns/differenceInDays';
import isValid from 'date-fns/isValid';
import { FC, ReactNode, useState } from 'react';
@@ -83,12 +86,18 @@ export const shouldShowDonation = (): boolean => {
export const Header: FC<HeaderProps> = () => {
const {
- onEditingHosts,
+ activePage,
+ } = useDashboard();
+ const {
onResetAllUsage,
+ } = useUsers();
+ const {
+ onEditingHosts,
+ } = useHosts();
+ const {
onEditingNodes,
onShowingNodesUsage,
- activePage,
- } = useDashboard();
+ } = useNodes();
const { t } = useTranslation();
const { colorMode, toggleColorMode } = useColorMode();
const [showDonationNotif, setShowDonationNotif] = useState(
@@ -192,7 +201,7 @@ export const Header: FC<HeaderProps> = () => {
variant="outline"
aria-label="core settings"
onClick={() => {
- useDashboard.setState({ isEditingCore: true });
+ useNodes.setState({ isEditingCore: true });
}}
>
<CoreSettingsIcon />
diff --git a/app/dashboard/src/components/HostsDialog.tsx b/app/dashboard/src/components/HostsDialog.tsx
@@ -42,6 +42,8 @@ import {
proxyHostSecurity,
} from 'constants/Proxies';
import { useHosts } from 'contexts/HostsContext';
+import { useInbounds } from 'contexts/InboundsContext';
+import { useUsers } from 'contexts/UsersContext';
import { FC, useEffect, useState } from 'react';
import {
FormProvider,
@@ -53,7 +55,6 @@ import { Trans, useTranslation } from 'react-i18next';
import 'slick-carousel/slick/slick-theme.css';
import 'slick-carousel/slick/slick.css';
import { z } from 'zod';
-import { useDashboard } from '../contexts/DashboardContext';
import { DeleteIcon } from './Dialog/Icons';
import { Icon } from './Icon';
import { Input as CustomInput } from './Input';
@@ -138,7 +139,7 @@ const AccordionInbound: FC<AccordionInboundType> = ({
isOpen,
toggleAccordion,
}) => {
- const { inbounds } = useDashboard();
+ const { inbounds } = useInbounds();
const inbound = [...inbounds.values()]
.flat()
.filter((inbound) => inbound.tag === hostKey)[0];
@@ -772,8 +773,8 @@ const AccordionInbound: FC<AccordionInboundType> = ({
};
export const HostsDialog: FC = () => {
- const { isEditingHosts, onEditingHosts, refetchUsers } = useDashboard();
- const { isLoading, hosts, fetchHosts, isPostLoading, setHosts } = useHosts();
+ const { refetchUsers } = useUsers();
+ const { isEditingHosts, onEditingHosts, isLoading, hosts, fetchHosts, isPostLoading, setHosts } = useHosts();
const toast = useToast();
const { t } = useTranslation();
const [openAccordions, setOpenAccordions] = useState<any>({});
diff --git a/app/dashboard/src/components/NodesModal.tsx b/app/dashboard/src/components/NodesModal.tsx
@@ -61,7 +61,6 @@ import {
generateErrorMessage,
generateSuccessMessage,
} from 'utils/toastHandler';
-import { useDashboard } from '../contexts/DashboardContext';
import { DeleteNodeModal } from './DeleteNodeModal';
import { DeleteIcon } from './Dialog/Icons';
import { ReloadIcon } from './UsersFilters';
@@ -130,11 +129,7 @@ const NodeAccordion: FC<AccordionInboundType> = ({ toggleAccordion, node }) => {
}
);
- const nodeStatus: Status = isReconnecting
- ? 'connecting'
- : node.status
- ? node.status
- : 'error';
+ const nodeStatus: Status = isReconnecting ? 'connecting' : node.status ? node.status : 'unhealthy';
return (
<AccordionItem
@@ -184,7 +179,7 @@ const NodeAccordion: FC<AccordionInboundType> = ({ toggleAccordion, node }) => {
</AccordionButton>
<AccordionPanel px={2} pb={2}>
<VStack pb={3} alignItems="flex-start">
- {nodeStatus === 'error' && (
+ {nodeStatus === 'unhealthy' && (
<Alert status="error" size="xs">
<Box>
<HStack w="full">
@@ -489,15 +484,6 @@ const NodeForm: NodeFormType = ({
error={form.formState?.errors?.port?.message}
/>
</Box>
- <Box w="25%">
- <CustomInput
- label={t('nodes.nodeAPIPort')}
- size="sm"
- placeholder="62051"
- {...form.register('api_port')}
- error={form.formState?.errors?.api_port?.message}
- />
- </Box>
</HStack>
{addAsHost && (
<FormControl py={1}>
@@ -527,7 +513,7 @@ const NodeForm: NodeFormType = ({
};
export const NodesDialog: FC = () => {
- const { isEditingNodes, onEditingNodes } = useDashboard();
+ const { isEditingNodes, onEditingNodes } = useNodes();
const { t } = useTranslation();
const [openAccordions, setOpenAccordions] = useState<any>({});
const { data: nodes, isLoading } = useNodesQuery();
diff --git a/app/dashboard/src/components/NodesUsage.tsx b/app/dashboard/src/components/NodesUsage.tsx
@@ -15,7 +15,7 @@ import {
useColorMode,
} from '@chakra-ui/react';
import { ChartPieIcon } from '@heroicons/react/24/outline';
-import { FilterUsageType, useDashboard } from 'contexts/DashboardContext';
+import { FilterUsageType } from 'types/Filter';
import { useNodes } from 'contexts/NodesContext';
import dayjs from 'dayjs';
import { FC, Suspense, useEffect, useState } from 'react';
@@ -34,7 +34,7 @@ const UsageIcon = chakra(ChartPieIcon, {
export type NodesUsageProps = {};
export const NodesUsage: FC<NodesUsageProps> = () => {
- const { isShowingNodesUsage, onShowingNodesUsage } = useDashboard();
+ const { isShowingNodesUsage, onShowingNodesUsage } = useNodes();
const { fetchNodesUsage } = useNodes();
const { t } = useTranslation();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/app/dashboard/src/components/QRCodeDialog.tsx b/app/dashboard/src/components/QRCodeDialog.tsx
@@ -23,8 +23,8 @@ import { useTranslation } from 'react-i18next';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick-theme.css';
import 'slick-carousel/slick/slick.css';
-import { useDashboard } from '../contexts/DashboardContext';
import { Icon } from './Icon';
+import { useUsers } from 'contexts/UsersContext';
const QRCode = chakra(QRCodeCanvas);
const NextIcon = chakra(ChevronRightIcon, {
@@ -55,7 +55,7 @@ const QRIcon = chakra(QrCodeIcon, {
});
export const QRCodeDialog: FC = () => {
- const { QRcodeLinks, setQRCode, setSubLink, subscribeUrl } = useDashboard();
+ const { QRcodeLinks, setQRCode, setSubLink, subscribeUrl } = useUsers();
const isOpen = QRcodeLinks !== null;
const [index, setIndex] = useState(0);
const { t } = useTranslation();
diff --git a/app/dashboard/src/components/ResetAllUsageModal.tsx b/app/dashboard/src/components/ResetAllUsageModal.tsx
@@ -15,8 +15,8 @@ import {
import { FC, useState } from 'react';
import { DocumentMinusIcon } from '@heroicons/react/24/outline';
import { Icon } from './Icon';
-import { useDashboard } from 'contexts/DashboardContext';
import { useTranslation } from 'react-i18next';
+import { useUsers } from 'contexts/UsersContext';
export const ResetIcon = chakra(DocumentMinusIcon, {
baseStyle: {
@@ -29,7 +29,7 @@ export type DeleteUserModalProps = {};
export const ResetAllUsageModal: FC<DeleteUserModalProps> = () => {
const [loading, setLoading] = useState(false);
- const { isResetingAllUsage, onResetAllUsage, resetAllUsage } = useDashboard();
+ const { isResetingAllUsage, onResetAllUsage, resetAllUsage } = useUsers();
const { t } = useTranslation();
const toast = useToast();
const onClose = () => {
diff --git a/app/dashboard/src/components/ResetUserUsageModal.tsx b/app/dashboard/src/components/ResetUserUsageModal.tsx
@@ -15,8 +15,8 @@ import {
import { FC, useState } from 'react';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { Icon } from './Icon';
-import { useDashboard } from 'contexts/DashboardContext';
import { useTranslation, Trans } from 'react-i18next';
+import { useUsers } from 'contexts/UsersContext';
export const ResetIcon = chakra(ArrowPathIcon, {
baseStyle: {
@@ -29,11 +29,11 @@ export type DeleteUserModalProps = {};
export const ResetUserUsageModal: FC<DeleteUserModalProps> = () => {
const [loading, setLoading] = useState(false);
- const { resetUsageUser: user, resetDataUsage } = useDashboard();
+ const { resetUsageUser: user, resetDataUsage } = useUsers();
const { t } = useTranslation();
const toast = useToast();
const onClose = () => {
- useDashboard.setState({ resetUsageUser: null });
+ useUsers.setState({ resetUsageUser: null });
};
const onReset = () => {
if (user) {
diff --git a/app/dashboard/src/components/RevokeSubscriptionModal.tsx b/app/dashboard/src/components/RevokeSubscriptionModal.tsx
@@ -13,7 +13,7 @@ import {
useToast,
} from '@chakra-ui/react';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useUsers } from 'contexts/UsersContext';
import { FC, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Icon } from './Icon';
@@ -29,11 +29,11 @@ export type RevokeSubscriptionModalProps = {};
export const RevokeSubscriptionModal: FC<RevokeSubscriptionModalProps> = () => {
const [loading, setLoading] = useState(false);
- const { revokeSubscriptionUser: user, revokeSubscription } = useDashboard();
+ const { revokeSubscriptionUser: user, revokeSubscription } = useUsers();
const { t } = useTranslation();
const toast = useToast();
const onClose = () => {
- useDashboard.setState({ revokeSubscriptionUser: null });
+ useUsers.setState({ revokeSubscriptionUser: null });
};
const onReset = () => {
if (user) {
diff --git a/app/dashboard/src/components/ServiceDialog/InboundsField.tsx b/app/dashboard/src/components/ServiceDialog/InboundsField.tsx
@@ -1,14 +1,14 @@
import { FormControl, FormLabel, FormErrorMessage, Center, Spinner, Text, Spacer, Flex } from '@chakra-ui/react';
-import { Inbounds, InboundType } from 'contexts/DashboardContext';
+import { Inbounds, InboundType } from 'types/Inbounds';
import { TFunction } from 'i18next';
import { Controller } from 'react-hook-form';
import { InboundsSelectList } from './InboundsSelectList';
interface InboundsFieldProps {
- form: any;
- inbounds: Inbounds;
- t: TFunction<'translation', undefined, 'translation'>;
+ form: any;
+ inbounds: Inbounds;
+ t: TFunction<'translation', undefined, 'translation'>;
}
export const InboundsField = ({ form, t, inbounds }: InboundsFieldProps) => {
diff --git a/app/dashboard/src/components/ServiceDialog/InboundsSelectList.tsx b/app/dashboard/src/components/ServiceDialog/InboundsSelectList.tsx
@@ -5,15 +5,15 @@ import {
Stack,
} from '@chakra-ui/react';
import { ListCheckBox } from 'components/ListCheckBox';
-import { Inbounds, InboundType } from 'contexts/DashboardContext';
+import { Inbounds, InboundType } from 'types/Inbounds';
import { forwardRef, ReactNode } from 'react';
import {
useFormContext,
} from 'react-hook-form';
interface InboundsSelectListProps {
- list: Inbounds;
- renderCheckboxContent: (inbound: InboundType) => ReactNode;
+ list: Inbounds;
+ renderCheckboxContent: (inbound: InboundType) => ReactNode;
}
export const InboundsSelectList = forwardRef<HTMLDivElement, InboundsSelectListProps>(
diff --git a/app/dashboard/src/components/ServiceDialog/index.tsx b/app/dashboard/src/components/ServiceDialog/index.tsx
@@ -17,7 +17,7 @@ import {
IconButton,
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { InboundType, useDashboard } from 'contexts/DashboardContext';
+import { InboundType } from 'types/Inbounds';
import { FC, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -29,6 +29,8 @@ import { schema, FormType } from './FormSchema';
import { InboundsField } from './InboundsField';
import { NameField } from './NameField';
import { Service, ServiceCreate } from 'types/Service';
+import { useServices } from 'contexts/ServicesContext';
+import { useInbounds } from 'contexts/InboundsContext';
const formatService = (service: Service): FormType => {
const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => {
@@ -46,14 +48,16 @@ export const ServiceDialog: FC<ServiceDialogProps> = () => {
const {
editingService,
isCreatingNewService,
- inbounds,
onCreateService,
editService,
- refetchInbounds,
onEditingService,
createService,
onDeletingService,
- } = useDashboard();
+ } = useServices();
+ const {
+ inbounds,
+ refetchInbounds,
+ } = useInbounds();
const isEditing = !!editingService;
const isOpen = isCreatingNewService || isEditing;
const [loading, setLoading] = useState(false);
diff --git a/app/dashboard/src/components/ServicesFilters.tsx b/app/dashboard/src/components/ServicesFilters.tsx
@@ -19,6 +19,7 @@ import {
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
import { useDashboard } from 'contexts/DashboardContext';
+import { useServices } from 'contexts/ServicesContext';
import debounce from 'lodash.debounce';
import React, { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -35,17 +36,18 @@ const ClearIcon = chakra(XMarkIcon, iconProps);
export const ReloadIcon = chakra(ArrowPathIcon, iconProps);
export type FilterProps = {} & BoxProps;
-const setSearchField = debounce((username: string) => {
- useDashboard.getState().onFilterChange({
- ...useDashboard.getState().usersFilters,
+const setSearchField = debounce((name: string) => {
+ useServices.getState().onFilterChange({
+ ...useServices.getState().servicesFilters,
offset: 0,
- username,
+ name,
});
}, 300);
export const ServicesFilters: FC<FilterProps> = ({ ...props }) => {
- const { loading, servicesFilters: filters, onFilterChange, refetchUsers, onCreateService } =
- useDashboard();
+ const { loading } =
+ useDashboard();
+ const { servicesFilters: filters, refetchServices, onFilterChange, onCreateService } = useServices();
const { t } = useTranslation();
const [search, setSearch] = useState('');
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -57,7 +59,7 @@ export const ServicesFilters: FC<FilterProps> = ({ ...props }) => {
onFilterChange({
...filters,
offset: 0,
- username: '',
+ name: '',
});
};
return (
@@ -91,7 +93,6 @@ export const ServicesFilters: FC<FilterProps> = ({ ...props }) => {
borderColor="light-border"
onChange={onChange}
/>
-
<InputRightElement>
{loading && <Spinner size="xs" />}
{filters.name && filters.name.length > 0 && (
@@ -110,9 +111,9 @@ export const ServicesFilters: FC<FilterProps> = ({ ...props }) => {
<GridItem colSpan={2} order={{ base: 1, md: 2 }}>
<HStack justifyContent="flex-end" alignItems="center" h="full">
<IconButton
- aria-label="refresh users"
+ aria-label="refresh services"
disabled={loading}
- onClick={refetchUsers}
+ onClick={refetchServices}
size="sm"
variant="outline"
>
diff --git a/app/dashboard/src/components/ServicesTable.tsx b/app/dashboard/src/components/ServicesTable/index.tsx
rename from app/dashboard/src/components/ServicesTable.tsx
rename to app/dashboard/src/components/ServicesTable/index.tsx
@@ -1,42 +1,25 @@
import {
Box,
- Button,
- chakra,
HStack,
Table,
TableProps,
Tbody,
Td,
- Text,
Th,
Thead,
Tr,
useBreakpointValue,
} from '@chakra-ui/react';
-
-import { ReactComponent as AddFileIcon } from 'assets/add_file.svg';
import classNames from 'classnames';
-import { useDashboard } from 'contexts/DashboardContext';
-import { t } from 'i18next';
import { FC, Fragment, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Pagination } from './Pagination';
-import { SortType } from './UsersTable';
-import { SortIcon } from './UsersTable/Icons';
-
-const EmptySectionIcon = chakra(AddFileIcon);
+import { EmptySection } from 'components/Table/EmptySection';
+import { Pagination } from 'components/Table/Pagination';
+import { Sort } from 'components/Table/Sort';
+import { useServices } from 'contexts/ServicesContext';
-export const Sort: FC<SortType> = ({ sort, column }) => {
- if (sort.includes(column))
- return (
- <SortIcon
- transform={sort.startsWith('-') ? undefined : 'rotate(180deg)'}
- />
- );
- return null;
-};
type ServicesTableProps = {} & TableProps;
export const ServicesTable: FC<ServicesTableProps> = (props) => {
@@ -44,8 +27,9 @@ export const ServicesTable: FC<ServicesTableProps> = (props) => {
servicesFilters: filters,
services,
onEditingService,
+ onCreateService,
onFilterChange,
- } = useDashboard();
+ } = useServices();
const { t } = useTranslation();
const marginTop = useBreakpointValue({ base: 120, lg: 72 }) || 72;
@@ -121,11 +105,10 @@ export const ServicesTable: FC<ServicesTableProps> = (props) => {
<Sort sort={filters.sort} column="inbounds_number" />
</HStack>
</Th>
-
</Tr>
</Thead>
<Tbody>
- {services ? services.map((service, i) => {
+ {services.length !== 0 ? services.map((service, i) => {
return (
<Fragment key={service.name}>
<Tr
@@ -154,68 +137,19 @@ export const ServicesTable: FC<ServicesTableProps> = (props) => {
}) :
<Tr>
<Td colSpan={3}>
- <EmptySection isFiltered={false} />
+ <EmptySection
+ isFiltered={false}
+ createObjectT='createService'
+ onCreateObject={onCreateService}
+ noObjectT="servicesTable.noService"
+ noObjectMatchedT='servicesTable.noServiceMatched'
+ />
</Td>
</Tr>
}
</Tbody>
</Table>
- <Pagination />
- </Box>
- );
-};
-
-type EmptySectionProps = {
- isFiltered: boolean;
-};
-
-const EmptySection: FC<EmptySectionProps> = ({ isFiltered }) => {
- const { onCreateService } = useDashboard();
- return (
- <Box
- padding="5"
- py="8"
- display="flex"
- alignItems="center"
- flexDirection="column"
- gap={4}
- w="full"
- >
- <EmptySectionIcon
- maxHeight="200px"
- maxWidth="200px"
- _dark={{
- 'path[fill="#fff"]': {
- fill: 'gray.800',
- },
- 'path[fill="#f2f2f2"], path[fill="#e6e6e6"], path[fill="#ccc"]': {
- fill: 'gray.700',
- },
- 'circle[fill="#3182CE"]': {
- fill: 'primary.300',
- },
- }}
- _light={{
- 'path[fill="#f2f2f2"], path[fill="#e6e6e6"], path[fill="#ccc"]': {
- fill: 'gray.300',
- },
- 'circle[fill="#3182CE"]': {
- fill: 'primary.500',
- },
- }}
- />
- <Text fontWeight="medium" color="gray.600" _dark={{ color: 'gray.400' }}>
- {isFiltered ? t('servicesTable.noServiceMatched') : t('servicesTable.noService')}
- </Text>
- {!isFiltered && (
- <Button
- size="sm"
- colorScheme="primary"
- onClick={() => onCreateService(true)}
- >
- {t('createService')}
- </Button>
- )}
+ <Pagination total={services.length} onFilterChange={onFilterChange} filters={filters} />
</Box>
);
};
diff --git a/app/dashboard/src/components/UsersTable/EmptySection.tsx b/app/dashboard/src/components/Table/EmptySection.tsx
rename from app/dashboard/src/components/UsersTable/EmptySection.tsx
rename to app/dashboard/src/components/Table/EmptySection.tsx
@@ -1,18 +1,30 @@
+
import { Box, Text, Button, chakra } from '@chakra-ui/react';
-import { useDashboard } from 'contexts/DashboardContext';
import { FC } from 'react';
import { ReactComponent as AddFileIcon } from 'assets/add_file.svg';
-import { useTranslation } from 'react-i18next';
+import { t } from 'i18next';
type EmptySectionProps = {
isFiltered: boolean;
+ noObjectMatchedT: string;
+ noObjectT: string;
+ createObjectT: string;
+ onCreateObject: (isOpen: boolean) => void;
};
const EmptySectionIcon = chakra(AddFileIcon);
-export const EmptySection: FC<EmptySectionProps> = ({ isFiltered }) => {
- const { onCreateUser } = useDashboard();
- const { t } = useTranslation();
+/**
+ * Component to provide visual feedback for the result of table filtering
+ * and table emptiness
+ *
+ * @param noObjectMatchedT - i18n token for "No <object> where Matched"
+ * @param noObjectT - i18n token for "No <object> where found"
+ * @param createObjectT - i18n token for "Create <object>" Button
+ * @param onCreateObject - Create object event handler on click
+ *
+ */
+export const EmptySection: FC<EmptySectionProps> = ({ isFiltered, noObjectT, noObjectMatchedT, createObjectT, onCreateObject }) => {
return (
<Box
padding="5"
@@ -47,15 +59,15 @@ export const EmptySection: FC<EmptySectionProps> = ({ isFiltered }) => {
}}
/>
<Text fontWeight="medium" color="gray.600" _dark={{ color: 'gray.400' }}>
- {isFiltered ? t('usersTable.noUserMatched') : t('usersTable.noUser')}
+ {isFiltered ? t(noObjectMatchedT) : t(noObjectT)}
</Text>
{!isFiltered && (
<Button
size="sm"
colorScheme="primary"
- onClick={() => onCreateUser(true)}
+ onClick={() => onCreateObject(true)}
>
- {t('createUser')}
+ {t(createObjectT)}
</Button>
)}
</Box>
diff --git a/app/dashboard/src/components/UsersTable/Icons.ts b/app/dashboard/src/components/Table/Icons.ts
rename from app/dashboard/src/components/UsersTable/Icons.ts
rename to app/dashboard/src/components/Table/Icons.ts
@@ -1,3 +1,4 @@
+
import { chakra } from '@chakra-ui/react';
import {
CheckIcon,
diff --git a/app/dashboard/src/components/Pagination.tsx b/app/dashboard/src/components/Table/Pagination.tsx
rename from app/dashboard/src/components/Pagination.tsx
rename to app/dashboard/src/components/Table/Pagination.tsx
@@ -11,9 +11,9 @@ import {
ArrowLongLeftIcon,
ArrowLongRightIcon,
} from '@heroicons/react/24/outline';
-import { useDashboard } from 'contexts/DashboardContext';
import { ChangeEvent, FC } from 'react';
import { useTranslation } from 'react-i18next';
+import { FilterType } from 'types/Filter';
import { setUsersPerPageLimitSize } from 'utils/userPreferenceStorage';
const PrevIcon = chakra(ArrowLongLeftIcon, {
@@ -75,12 +75,13 @@ function generatePageItems(total: number, current: number, width: number) {
return items;
}
-export const Pagination: FC = () => {
- const {
- usersFilters: filters,
- onFilterChange,
- users: { total },
- } = useDashboard();
+type PaginationProps = {
+ total: number;
+ onFilterChange: (filters: Partial<FilterType>) => void;
+ filters: FilterType;
+}
+
+export const Pagination: FC<PaginationProps> = ({ filters, total, onFilterChange }) => {
const { limit: perPage, offset } = filters;
const page = (offset || 0) / (perPage || 1);
diff --git a/app/dashboard/src/components/Table/Sort.tsx b/app/dashboard/src/components/Table/Sort.tsx
@@ -0,0 +1,16 @@
+import { SortIcon } from './Icons';
+import { FC } from 'react';
+
+type SortType = {
+ sort: string;
+ column: string;
+};
+
+export const Sort: FC<SortType> = ({ sort, column }) => {
+ if (sort.includes(column))
+ return (
+ <SortIcon
+ transform={sort.startsWith('-') ? undefined : 'rotate(180deg)'} />
+ );
+ return null;
+};
diff --git a/app/dashboard/src/components/UsageFilter.tsx b/app/dashboard/src/components/UsageFilter.tsx
@@ -20,7 +20,7 @@ import {
} from '@chakra-ui/react';
import { CalendarIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
import { ApexOptions } from 'apexcharts';
-import { FilterUsageType } from 'contexts/DashboardContext';
+import { FilterUsageType } from 'types/Filter';
import dayjs, { ManipulateType } from 'dayjs';
import { FC, useRef, useState } from 'react';
import ReactDatePicker from 'react-datepicker';
diff --git a/app/dashboard/src/components/UserDialog/index.tsx b/app/dashboard/src/components/UserDialog/index.tsx
@@ -21,7 +21,7 @@ import {
HStack,
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { FilterUsageType, useDashboard } from 'contexts/DashboardContext';
+import { FilterUsageType } from 'types/Filter';
import dayjs from 'dayjs';
import { FC, useEffect, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
@@ -46,6 +46,8 @@ import { PeriodicUsageReset as PeriodicUsageReset } from './PeriodicUsageReset';
import { ExpireDateField } from './ExpireDateField';
import { NoteField } from './NoteField';
import { Service } from 'types/Service';
+import { useUsers } from 'contexts/UsersContext';
+import { useServices } from 'contexts/ServicesContext';
const formatUser = (user: User): FormType => {
const services: number[] = user.services.map((service: number | Service): number => {
@@ -66,15 +68,14 @@ export const UserDialog: FC<UserDialogProps> = () => {
const {
editingUser,
isCreatingNewUser,
- services,
onCreateUser,
editUser,
fetchUserUsage,
- refetchServices,
onEditingUser,
createUser,
onDeletingUser,
- } = useDashboard();
+ } = useUsers();
+ const { refetchServices, services } = useServices();
const isEditing = !!editingUser;
const isOpen = isCreatingNewUser || isEditing;
const [loading, setLoading] = useState(false);
@@ -198,11 +199,11 @@ export const UserDialog: FC<UserDialogProps> = () => {
};
const handleResetUsage = () => {
- useDashboard.setState({ resetUsageUser: editingUser });
+ useUsers.setState({ resetUsageUser: editingUser });
};
const handleRevokeSubscription = () => {
- useDashboard.setState({ revokeSubscriptionUser: editingUser });
+ useUsers.setState({ revokeSubscriptionUser: editingUser });
};
const disabled = loading;
diff --git a/app/dashboard/src/components/UsersFilters.tsx b/app/dashboard/src/components/UsersFilters.tsx
@@ -19,6 +19,7 @@ import {
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
import { useDashboard } from 'contexts/DashboardContext';
+import { useUsers } from 'contexts/UsersContext';
import debounce from 'lodash.debounce';
import React, { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -36,16 +37,17 @@ export const ReloadIcon = chakra(ArrowPathIcon, iconProps);
export type FilterProps = {} & BoxProps;
const setSearchField = debounce((username: string) => {
- useDashboard.getState().onFilterChange({
- ...useDashboard.getState().usersFilters,
+ useUsers.getState().onFilterChange({
+ ...useUsers.getState().usersFilters,
offset: 0,
username,
});
}, 300);
export const UsersFilters: FC<FilterProps> = ({ ...props }) => {
- const { loading, usersFilters: filters, onFilterChange, refetchUsers, onCreateUser } =
+ const { loading, } =
useDashboard();
+ const { usersFilters: filters, onFilterChange, refetchUsers, onCreateUser } = useUsers();
const { t } = useTranslation();
const [search, setSearch] = useState('');
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -91,7 +93,6 @@ export const UsersFilters: FC<FilterProps> = ({ ...props }) => {
borderColor="light-border"
onChange={onChange}
/>
-
<InputRightElement>
{loading && <Spinner size="xs" />}
{filters.username && filters.username.length > 0 && (
diff --git a/app/dashboard/src/components/UsersTable/StatusSortSelect.tsx b/app/dashboard/src/components/UsersTable/StatusSortSelect.tsx
@@ -1,11 +1,11 @@
import { Select } from '@chakra-ui/react'
-import { UsersFilterType } from 'contexts/DashboardContext'
+import { UsersFilterType } from 'types/Filter';
import React from 'react'
interface StatusSortSelectProps {
- filters: UsersFilterType;
- handleStatusFilter: (e: any) => void;
+ filters: UsersFilterType;
+ handleStatusFilter: (e: any) => void;
}
export const StatusSortSelect: React.FC<StatusSortSelectProps> = ({ filters, handleStatusFilter }) => {
diff --git a/app/dashboard/src/components/UsersTable/index.tsx b/app/dashboard/src/components/UsersTable/index.tsx
@@ -22,7 +22,6 @@ import {
import classNames from 'classnames';
import { statusColors } from 'constants/UserSettings';
-import { useDashboard } from 'contexts/DashboardContext';
import { t } from 'i18next';
import { FC, Fragment, useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
@@ -30,41 +29,35 @@ import { useTranslation } from 'react-i18next';
import { User } from 'types/User';
import { OnlineBadge } from '../OnlineBadge';
import { OnlineStatus } from '../OnlineStatus';
-import { Pagination } from '../Pagination';
+import { Pagination } from 'components/Table/Pagination';
import { StatusBadge } from '../StatusBadge';
-import { EmptySection } from './EmptySection';
-import { AccordionArrowIcon, SortIcon, EditIcon, CopiedIcon, CopyIcon, QRIcon, SubscriptionLinkIcon } from 'components/UsersTable/Icons';
+import { EmptySection } from 'components/Table/EmptySection';
+import {
+ AccordionArrowIcon,
+ EditIcon,
+ CopiedIcon,
+ CopyIcon,
+ QRIcon,
+ SubscriptionLinkIcon
+} from 'components/Table/Icons';
+import { Sort } from 'components/Table/Sort';
import { UsageSlider, UsageSliderCompact } from './UsageSlider';
import { StatusSortSelect } from './StatusSortSelect';
+import { useUsers } from 'contexts/UsersContext';
type ExpandedIndex = number | number[];
-
-export type SortType = {
- sort: string;
- column: string;
-};
-
-export const Sort: FC<SortType> = ({ sort, column }) => {
- if (sort.includes(column))
- return (
- <SortIcon
- transform={sort.startsWith('-') ? undefined : 'rotate(180deg)'}
- />
- );
- return null;
-};
-
type UsersTableProps = {} & TableProps;
export const UsersTable: FC<UsersTableProps> = (props) => {
const {
usersFilters: filters,
users: { users },
- users: totalUsers,
+ users: { total },
onEditingUser,
+ onCreateUser,
onFilterChange,
- } = useDashboard();
+ } = useUsers();
const { t } = useTranslation();
const [selectedRow, setSelectedRow] = useState<ExpandedIndex | undefined>(
@@ -83,7 +76,7 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
() => window.removeEventListener('scroll', calcTop);
}, []);
- const isFiltered = users.length !== totalUsers.total;
+ const isFiltered = users.length !== total;
const handleSort = (column: string) => {
let newSort = filters.sort;
@@ -189,143 +182,143 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
</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)}
+ 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"
>
- <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>
- );
- })}
+ <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>
@@ -398,63 +391,69 @@ export const UsersTable: FC<UsersTableProps> = (props) => {
</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?.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} />
+ <EmptySection
+ isFiltered={isFiltered}
+ noObjectT="usersTable.noUser"
+ noObjectMatchedT='usersTable.noUserMatched'
+ createObjectT="createUser"
+ onCreateObject={onCreateUser}
+ />
</Td>
</Tr>
)}
</Tbody>
</Table>
- <Pagination />
+ <Pagination filters={filters} total={total} onFilterChange={onFilterChange} />
</Box>
);
};
type ActionButtonsProps = {
- user: User;
+ user: User;
};
const ActionButtons: FC<ActionButtonsProps> = ({ user }) => {
- const { setQRCode, setSubLink } = useDashboard();
+ const { setQRCode, setSubLink } = useUsers();
const proxyLinks = user.links.join('\r\n');
diff --git a/app/dashboard/src/contexts/AdminContext.tsx b/app/dashboard/src/contexts/AdminContext.ts
rename from app/dashboard/src/contexts/AdminContext.tsx
rename to app/dashboard/src/contexts/AdminContext.ts
diff --git a/app/dashboard/src/contexts/CoreSettingsContext.tsx b/app/dashboard/src/contexts/CoreSettingsContext.ts
rename from app/dashboard/src/contexts/CoreSettingsContext.tsx
rename to app/dashboard/src/contexts/CoreSettingsContext.ts
diff --git a/app/dashboard/src/contexts/DashboardContext.ts b/app/dashboard/src/contexts/DashboardContext.ts
@@ -0,0 +1,22 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+
+type PageId = number;
+
+type DashboardStateType = {
+ version: string | null;
+ loading: boolean;
+ activePage: number;
+ activatePage: (pageId: PageId) => void;
+};
+
+export const useDashboard = create(
+ subscribeWithSelector<DashboardStateType>((set,) => ({
+ version: null,
+ loading: true,
+ activePage: 0,
+ activatePage: (pageId: number) => {
+ set({ activePage: pageId });
+ },
+ }))
+);
diff --git a/app/dashboard/src/contexts/DashboardContext.tsx b/app/dashboard/src/contexts/DashboardContext.tsx
@@ -1,310 +0,0 @@
-import { StatisticsQueryKey } from 'components/Statistics';
-import { fetch } from 'service/http';
-import { User, UserCreate } from 'types/User';
-import { Service, ServiceCreate } from 'types/Service';
-import { queryClient } from 'utils/react-query';
-import { getServicesPerPageLimitSize, getUsersPerPageLimitSize } from 'utils/userPreferenceStorage';
-import { create } from 'zustand';
-import { subscribeWithSelector } from 'zustand/middleware';
-
-export type UsersFilterType = {
- username?: string;
- limit?: number;
- offset?: number;
- sort: string;
- status?: 'active' | 'disabled' | 'limited' | 'expired' | 'on_hold';
-};
-
-export type ServicesFilterType = {
- name?: string;
- users?: number;
- inbounds?: number;
- limit?: number;
- offset?: number;
- sort: string;
-};
-
-export type ProtocolType = 'vmess' | 'vless' | 'trojan' | 'shadowsocks';
-
-export type FilterUsageType = {
- start?: string;
- end?: string;
-};
-
-export type InboundType = {
- id: number;
- tag: string;
- protocol: ProtocolType;
- network: string;
- tls: string;
- port?: number;
-};
-
-export type Inbounds = InboundType[];
-type PageId = number;
-
-type DashboardStateType = {
- isCreatingNewUser: boolean;
- editingUser: User | null | undefined;
- deletingUser: User | null;
- isCreatingNewService: boolean;
- editingService: Service | null | undefined;
- deletingService: Service | null;
- version: string | null;
- users: {
- users: User[];
- total: number;
- };
- inbounds: Inbounds;
- services: Service[];
- loading: boolean;
- usersFilters: UsersFilterType;
- servicesFilters: ServicesFilterType;
- subscribeUrl: string | null;
- QRcodeLinks: string[] | null;
- isEditingHosts: boolean;
- isEditingNodes: boolean;
- isShowingNodesUsage: boolean;
- isResetingAllUsage: boolean;
- resetUsageUser: User | null;
- revokeSubscriptionUser: User | null;
- isEditingCore: boolean;
- activePage: number;
- activatePage: (pageId: PageId) => void;
- onCreateUser: (isOpen: boolean) => void;
- onCreateService: (isOpen: boolean) => void;
- onEditingUser: (user: User | null) => void;
- onEditingService: (service: Service | null) => void;
- onDeletingUser: (user: User | null) => void;
- onDeletingService: (service: Service | null) => void;
- onResetAllUsage: (isResetingAllUsage: boolean) => void;
- refetchUsers: () => void;
- refetchInbounds: () => void;
- refetchServices: () => void;
- resetAllUsage: () => Promise<void>;
- onFilterChange: (filters: Partial<UsersFilterType>) => void;
- deleteUser: (user: User) => Promise<void>;
- createUser: (user: UserCreate) => Promise<void>;
- editUser: (user: UserCreate) => Promise<void>;
- deleteService: (service: Service) => Promise<void>;
- createService: (service: ServiceCreate) => Promise<void>;
- editService: (service: ServiceCreate) => Promise<void>;
- fetchUserUsage: (user: User, query: FilterUsageType) => Promise<void>;
- setQRCode: (links: string[] | null) => void;
- setSubLink: (subscribeURL: string | null) => void;
- onEditingHosts: (isEditingHosts: boolean) => void;
- onEditingNodes: (isEditingHosts: boolean) => void;
- onShowingNodesUsage: (isShowingNodesUsage: boolean) => void;
- resetDataUsage: (user: User) => Promise<void>;
- revokeSubscription: (user: User) => Promise<void>;
-};
-
-export const fetchUsers = async (query: UsersFilterType): Promise<User[]> => {
- for (const key in query) {
- if (!query[key as keyof UsersFilterType]) delete query[key as keyof UsersFilterType];
- }
- useDashboard.setState({ loading: true });
- return fetch('/users', { query })
- .then((users) => {
- for (let i = 0; i < users.users.length; i++) {
- users.users[i].service = users.users[i].service_ids;
- }
- useDashboard.setState({ users });
- return users;
- })
- .finally(() => {
- useDashboard.setState({ loading: false });
- });
-};
-
-export const fetchServices = async (query: UsersFilterType): Promise<Service[]> => {
- for (const key in query) {
- if (!query[key as keyof UsersFilterType]) delete query[key as keyof UsersFilterType];
- }
- useDashboard.setState({ loading: true });
- return fetch('/services')
- .then((services) => {
- useDashboard.setState({ services });
- return services;
- })
- .finally(() => {
- useDashboard.setState({ loading: false });
- });
-};
-
-export const fetchInbounds = async () => {
- return fetch('/inbounds')
- .then((inbounds: Inbounds) => {
- useDashboard.setState({ inbounds });
- })
- .finally(() => {
- useDashboard.setState({ loading: false });
- });
-};
-
-export const useDashboard = create(
- subscribeWithSelector<DashboardStateType>((set, get) => ({
- version: null,
- editingUser: null,
- editingService: null,
- deletingUser: null,
- deletingService: null,
- isCreatingNewUser: false,
- isCreatingNewService: false,
- QRcodeLinks: null,
- subscribeUrl: null,
- users: {
- users: [],
- total: 0,
- },
- loading: true,
- services: [],
- isResetingAllUsage: false,
- isEditingHosts: false,
- isEditingNodes: false,
- isShowingNodesUsage: false,
- resetUsageUser: null,
- revokeSubscriptionUser: null,
- servicesFilters: {
- name: '',
- limit: getServicesPerPageLimitSize(),
- sort: '-created_at',
- },
- usersFilters: {
- username: '',
- limit: getUsersPerPageLimitSize(),
- sort: '-created_at',
- },
- inbounds: [],
- isEditingCore: false,
- activePage: 0,
- activatePage: (pageId: number) => {
- set({ activePage: pageId });
- },
- refetchServices: () => {
- fetchServices(get().servicesFilters);
- },
- refetchUsers: () => {
- fetchUsers(get().usersFilters);
- },
- refetchInbounds: () => {
- fetchInbounds();
- },
- resetAllUsage: async () => {
- return fetch('/users/reset', { method: 'POST' }).then(() => {
- get().onResetAllUsage(false);
- get().refetchUsers();
- });
- },
- onResetAllUsage: (isResetingAllUsage) => set({ isResetingAllUsage }),
- onCreateUser: (isCreatingNewUser) => set({ isCreatingNewUser }),
- onCreateService: (isCreatingNewService) => set({ isCreatingNewService }),
- onEditingService: (editingService) => {
- set({ editingService });
- },
- onEditingUser: (editingUser) => {
- set({ editingUser });
- },
- onDeletingUser: (deletingUser) => {
- set({ deletingUser });
- },
- onDeletingService: (deletingService) => {
- set({ deletingService });
- },
- onFilterChange: (filters) => {
- set({
- usersFilters: {
- ...get().usersFilters,
- ...filters,
- },
- });
- get().refetchUsers();
- },
- setQRCode: (QRcodeLinks) => {
- set({ QRcodeLinks });
- },
- deleteUser: async (user: User) => {
- set({ editingUser: null });
- return fetch(`/user/${user.username}`, { method: 'DELETE' }).then(() => {
- set({ deletingUser: null });
- get().refetchUsers();
- queryClient.invalidateQueries(StatisticsQueryKey);
- });
- },
- createUser: async (body: UserCreate) => {
- return fetch('/user', { method: 'POST', body }).then(() => {
- set({ editingUser: null });
- get().refetchUsers();
- get().refetchServices();
- queryClient.invalidateQueries(StatisticsQueryKey);
- });
- },
- editUser: async (body: UserCreate) => {
- return fetch(`/user/${body.username}`, { method: 'PUT', body }).then(
- () => {
- get().onEditingUser(null);
- get().refetchUsers();
- }
- );
- },
- deleteService: async (service: Service) => {
- set({ editingUser: null });
- return fetch(`/service/${service.id}`, { method: 'DELETE' }).then(() => {
- set({ deletingUser: null });
- get().refetchUsers();
- queryClient.invalidateQueries(StatisticsQueryKey);
- });
- },
- createService: async (body: ServiceCreate) => {
- return fetch('/service', { method: 'POST', body }).then(() => {
- set({ editingService: null });
- get().refetchServices();
- get().refetchServices();
- queryClient.invalidateQueries(StatisticsQueryKey);
- });
- },
- editService: async (body: ServiceCreate) => {
- return fetch(`/service/${body.id}`, { method: 'PUT', body }).then(
- () => {
- get().onEditingUser(null);
- get().refetchUsers();
- }
- );
- },
- fetchUserUsage: (body: User, query: FilterUsageType) => {
- for (const key in query) {
- if (!query[key as keyof FilterUsageType])
- delete query[key as keyof FilterUsageType];
- }
- return fetch(`/user/${body.username}/usage`, { method: 'GET', query });
- },
- onEditingHosts: (isEditingHosts: boolean) => {
- set({ isEditingHosts });
- },
- onEditingNodes: (isEditingNodes: boolean) => {
- set({ isEditingNodes });
- },
- onShowingNodesUsage: (isShowingNodesUsage: boolean) => {
- set({ isShowingNodesUsage });
- },
- setSubLink: (subscribeUrl) => {
- set({ subscribeUrl });
- },
- resetDataUsage: async (user) => {
- return fetch(`/user/${user.username}/reset`, { method: 'POST' }).then(
- () => {
- set({ resetUsageUser: null });
- get().refetchUsers();
- }
- );
- },
- revokeSubscription: async (user) => {
- return fetch(`/user/${user.username}/revoke_sub`, {
- method: 'POST',
- }).then((user) => {
- set({ revokeSubscriptionUser: null, editingUser: user });
- get().refetchUsers();
- });
- },
- }))
-);
diff --git a/app/dashboard/src/contexts/HostsContext.tsx b/app/dashboard/src/contexts/HostsContext.ts
rename from app/dashboard/src/contexts/HostsContext.tsx
rename to app/dashboard/src/contexts/HostsContext.ts
@@ -19,11 +19,17 @@ type HostsStore = {
hosts: HostsSchema;
fetchHosts: () => void;
setHosts: (hosts: HostsSchema) => Promise<void>;
+ onEditingHosts: (isEditingHosts: boolean) => void;
+ isEditingHosts: boolean;
};
export const useHosts = create<HostsStore>((set) => ({
isLoading: false,
isPostLoading: false,
hosts: {},
+ isEditingHosts: false,
+ onEditingHosts: (isEditingHosts: boolean) => {
+ set({ isEditingHosts });
+ },
fetchHosts: () => {
set({ isLoading: true });
fetch('/hosts')
@@ -36,4 +42,4 @@ export const useHosts = create<HostsStore>((set) => ({
set({ isPostLoading: false });
});
},
-}));
\ No newline at end of file
+}));
diff --git a/app/dashboard/src/contexts/InboundsContext.ts b/app/dashboard/src/contexts/InboundsContext.ts
@@ -0,0 +1,32 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+import { useDashboard } from './DashboardContext';
+import { fetch } from 'service/http';
+import { Inbounds } from 'types/Inbounds';
+
+
+export const fetchInbounds = async () => {
+ useDashboard.setState({ loading: true });
+ return fetch('/inbounds')
+ .then((inbounds: Inbounds) => {
+ useInbounds.setState({ inbounds });
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+
+type InboundsStateType = {
+ inbounds: Inbounds;
+ refetchInbounds: () => void;
+};
+
+export const useInbounds = create(
+ subscribeWithSelector<InboundsStateType>(() => ({
+ inbounds: [],
+ refetchInbounds: () => {
+ fetchInbounds();
+ },
+ }))
+);
diff --git a/app/dashboard/src/contexts/NodesContext.tsx b/app/dashboard/src/contexts/NodesContext.ts
rename from app/dashboard/src/contexts/NodesContext.tsx
rename to app/dashboard/src/contexts/NodesContext.ts
@@ -2,7 +2,7 @@ import { useQuery } from 'react-query';
import { fetch } from 'service/http';
import { z } from 'zod';
import { create } from 'zustand';
-import { FilterUsageType, useDashboard } from './DashboardContext';
+import { FilterUsageType } from 'types/Filter';
export const NodeSchema = z.object({
name: z.string().min(1),
@@ -11,14 +11,10 @@ export const NodeSchema = z.object({
.number()
.min(1)
.or(z.string().transform((v) => parseFloat(v))),
- api_port: z
- .number()
- .min(1)
- .or(z.string().transform((v) => parseFloat(v))),
xray_version: z.string().nullable().optional(),
id: z.number().nullable().optional(),
status: z
- .enum(['connected', 'connecting', 'error', 'disabled'])
+ .enum(['healthy', 'unhealthy', 'disabled'])
.nullable()
.optional(),
message: z.string().nullable().optional(),
@@ -31,7 +27,6 @@ export const getNodeDefaultValues = (): NodeType => ({
name: '',
address: '',
port: 62050,
- api_port: 62051,
xray_version: '',
});
@@ -39,28 +34,35 @@ export const FetchNodesQueryKey = 'fetch-nodes-query-key';
export type NodeStore = {
nodes: NodeType[];
+ deletingNode?: NodeType | null;
+ isEditingNodes: boolean;
+ isShowingNodesUsage: boolean;
+ isEditingCore: boolean;
addNode: (node: NodeType) => Promise<unknown>;
fetchNodes: () => Promise<NodeType[]>;
fetchNodesUsage: (query: FilterUsageType) => Promise<void>;
updateNode: (node: NodeType) => Promise<unknown>;
reconnectNode: (node: NodeType) => Promise<unknown>;
- deletingNode?: NodeType | null;
deleteNode: () => Promise<unknown>;
setDeletingNode: (node: NodeType | null) => void;
+ onEditingNodes: (isEditingHosts: boolean) => void;
+ onShowingNodesUsage: (isShowingNodesUsage: boolean) => void;
};
export const useNodesQuery = () => {
- const { isEditingNodes } = useDashboard();
return useQuery({
queryKey: FetchNodesQueryKey,
queryFn: useNodes.getState().fetchNodes,
- refetchInterval: isEditingNodes ? 3000 : undefined,
+ refetchInterval: useNodes.getState().isEditingNodes ? 3000 : undefined,
refetchOnWindowFocus: false,
});
};
export const useNodes = create<NodeStore>((set, get) => ({
nodes: [],
+ isEditingNodes: false,
+ isShowingNodesUsage: false,
+ isEditingCore: false,
addNode(body) {
return fetch('/node', { method: 'POST', body });
},
@@ -89,4 +91,10 @@ export const useNodes = create<NodeStore>((set, get) => ({
method: 'DELETE',
});
},
+ onEditingNodes: (isEditingNodes: boolean) => {
+ set({ isEditingNodes });
+ },
+ onShowingNodesUsage: (isShowingNodesUsage: boolean) => {
+ set({ isShowingNodesUsage });
+ },
}));
diff --git a/app/dashboard/src/contexts/ServicesContext.ts b/app/dashboard/src/contexts/ServicesContext.ts
@@ -0,0 +1,101 @@
+
+import { queryClient } from 'utils/react-query';
+import { StatisticsQueryKey } from 'components/Statistics';
+import { Service, ServiceCreate } from 'types/Service';
+import { getServicesPerPageLimitSize } from 'utils/userPreferenceStorage';
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+import { useDashboard } from 'contexts/DashboardContext';
+import { fetch } from 'service/http';
+import { ServicesFilterType } from 'types/Filter';
+import { useUsers } from './UsersContext';
+
+type ServicesStateType = {
+ services: Service[],
+ isCreatingNewService: boolean;
+ editingService: Service | null | undefined;
+ deletingService: Service | null;
+ servicesFilters: ServicesFilterType;
+ onCreateService: (isOpen: boolean) => void;
+ onEditingService: (service: Service | null) => void;
+ onDeletingService: (service: Service | null) => void;
+ refetchServices: () => void;
+ deleteService: (service: Service) => Promise<void>;
+ createService: (service: ServiceCreate) => Promise<void>;
+ editService: (service: ServiceCreate) => Promise<void>;
+ onFilterChange: (filters: Partial<ServicesFilterType>) => void;
+};
+
+export const fetchServices = async (query: ServicesFilterType): Promise<Service[]> => {
+ for (const key in query) {
+ if (!query[key as keyof ServicesFilterType]) delete query[key as keyof ServicesFilterType];
+ }
+
+ useDashboard.setState({ loading: true });
+
+ return fetch('/services')
+ .then((services) => {
+ useServices.setState({ services });
+ return services;
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const useServices = create(
+ subscribeWithSelector<ServicesStateType>((set, get) => ({
+ editingService: null,
+ deletingService: null,
+ isCreatingNewService: false,
+ services: [],
+ servicesFilters: {
+ name: '',
+ limit: getServicesPerPageLimitSize(),
+ sort: '-created_at',
+ },
+ onFilterChange: (filters) => {
+ set({
+ servicesFilters: {
+ ...get().servicesFilters,
+ ...filters,
+ },
+ });
+ get().refetchServices();
+ },
+ refetchServices: () => {
+ fetchServices(get().servicesFilters);
+ },
+ onCreateService: (isCreatingNewService) => set({ isCreatingNewService }),
+ onEditingService: (editingService) => {
+ set({ editingService });
+ },
+ onDeletingService: (deletingService) => {
+ set({ deletingService });
+ },
+ deleteService: async (service: Service) => {
+ set({ editingService: null });
+ return fetch(`/service/${service.id}`, { method: 'DELETE' }).then(() => {
+ set({ deletingService: null });
+ get().refetchServices();
+ useUsers.getState().refetchUsers();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ createService: async (body: ServiceCreate) => {
+ return fetch('/service', { method: 'POST', body }).then(() => {
+ set({ editingService: null });
+ get().refetchServices();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ editService: async (body: ServiceCreate) => {
+ return fetch(`/service/${body.id}`, { method: 'PUT', body }).then(
+ () => {
+ get().onEditingService(null);
+ get().refetchServices();
+ }
+ );
+ },
+ }))
+);
diff --git a/app/dashboard/src/contexts/UsersContext.ts b/app/dashboard/src/contexts/UsersContext.ts
@@ -0,0 +1,160 @@
+
+import { User, UserCreate } from 'types/User';
+import { subscribeWithSelector } from 'zustand/middleware';
+import { create } from 'zustand';
+import { getUsersPerPageLimitSize } from 'utils/userPreferenceStorage';
+import { fetch } from 'service/http';
+import { useDashboard } from 'contexts/DashboardContext';
+import { StatisticsQueryKey } from 'components/Statistics';
+import { queryClient } from 'utils/react-query';
+import { useServices } from './ServicesContext';
+import { UsersFilterType, FilterUsageType } from 'types/Filter';
+
+type UsersStateType = {
+ isCreatingNewUser: boolean;
+ editingUser: User | null | undefined;
+ deletingUser: User | null;
+ users: {
+ users: User[];
+ total: number;
+ };
+ usersFilters: UsersFilterType;
+ resetUsageUser: User | null;
+ revokeSubscriptionUser: User | null;
+ isResetingAllUsage: boolean;
+ subscribeUrl: string | null;
+ QRcodeLinks: string[] | null;
+ setQRCode: (links: string[] | null) => void;
+ setSubLink: (subscribeURL: string | null) => void;
+ onResetAllUsage: (isResetingAllUsage: boolean) => void;
+ resetAllUsage: () => Promise<void>;
+ resetDataUsage: (user: User) => Promise<void>;
+ revokeSubscription: (user: User) => Promise<void>;
+ onCreateUser: (isOpen: boolean) => void;
+ onEditingUser: (user: User | null) => void;
+ onDeletingUser: (user: User | null) => void;
+ refetchUsers: () => void;
+ onFilterChange: (filters: Partial<UsersFilterType>) => void;
+ deleteUser: (user: User) => Promise<void>;
+ createUser: (user: UserCreate) => Promise<void>;
+ editUser: (user: UserCreate) => Promise<void>;
+ fetchUserUsage: (user: User, query: FilterUsageType) => Promise<void>;
+}
+
+export const fetchUsers = async (query: UsersFilterType): Promise<User[]> => {
+ for (const key in query) {
+ if (!query[key as keyof UsersFilterType]) delete query[key as keyof UsersFilterType];
+ }
+ useDashboard.setState({ loading: true });
+ return fetch('/users', { query })
+ .then((users) => {
+ for (let i = 0; i < users.users.length; i++) {
+ users.users[i].service = users.users[i].service_ids;
+ }
+ useUsers.setState({ users });
+ return users;
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get) => ({
+ editingUser: null,
+ deletingUser: null,
+ isCreatingNewUser: false,
+ users: {
+ users: [],
+ total: 0,
+ },
+ resetUsageUser: null,
+ revokeSubscriptionUser: null,
+ isResetingAllUsage: false,
+ usersFilters: {
+ username: '',
+ limit: getUsersPerPageLimitSize(),
+ sort: '-created_at',
+ },
+ QRcodeLinks: null,
+ subscribeUrl: null,
+ setQRCode: (QRcodeLinks) => {
+ set({ QRcodeLinks });
+ },
+ setSubLink: (subscribeUrl) => {
+ set({ subscribeUrl });
+ },
+ refetchUsers: () => {
+ fetchUsers(get().usersFilters);
+ },
+ resetAllUsage: async () => {
+ return fetch('/users/reset', { method: 'POST' }).then(() => {
+ get().onResetAllUsage(false);
+ get().refetchUsers();
+ });
+ },
+ onResetAllUsage: (isResetingAllUsage) => set({ isResetingAllUsage }),
+ onCreateUser: (isCreatingNewUser) => set({ isCreatingNewUser }),
+ onEditingUser: (editingUser) => {
+ set({ editingUser });
+ },
+ onDeletingUser: (deletingUser) => {
+ set({ deletingUser });
+ },
+ onFilterChange: (filters) => {
+ set({
+ usersFilters: {
+ ...get().usersFilters,
+ ...filters,
+ },
+ });
+ get().refetchUsers();
+ },
+ deleteUser: async (user: User) => {
+ set({ editingUser: null });
+ return fetch(`/user/${user.username}`, { method: 'DELETE' }).then(() => {
+ set({ deletingUser: null });
+ get().refetchUsers();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ createUser: async (body: UserCreate) => {
+ return fetch('/user', { method: 'POST', body }).then(() => {
+ set({ editingUser: null });
+ get().refetchUsers();
+ useServices.getState().refetchServices();
+ queryClient.invalidateQueries(StatisticsQueryKey);
+ });
+ },
+ editUser: async (body: UserCreate) => {
+ return fetch(`/user/${body.username}`, { method: 'PUT', body }).then(
+ () => {
+ get().onEditingUser(null);
+ get().refetchUsers();
+ }
+ );
+ },
+
+ fetchUserUsage: (body: User, query: FilterUsageType) => {
+ for (const key in query) {
+ if (!query[key as keyof FilterUsageType])
+ delete query[key as keyof FilterUsageType];
+ }
+ return fetch(`/user/${body.username}/usage`, { method: 'GET', query });
+ },
+ resetDataUsage: async (user) => {
+ return fetch(`/user/${user.username}/reset`, { method: 'POST' }).then(
+ () => {
+ set({ resetUsageUser: null });
+ get().refetchUsers();
+ }
+ );
+ },
+ revokeSubscription: async (user) => {
+ return fetch(`/user/${user.username}/revoke_sub`, {
+ method: 'POST',
+ }).then((user) => {
+ set({ revokeSubscriptionUser: null, editingUser: user });
+ get().refetchUsers();
+ });
+ },
+})));
diff --git a/app/dashboard/src/pages/Dashboard.tsx b/app/dashboard/src/pages/Dashboard.tsx
@@ -1,12 +1,8 @@
import { Box, VStack } from '@chakra-ui/react';
-import { useDashboard } from 'contexts/DashboardContext';
-import { FC, useEffect } from 'react';
+import { FC } from 'react';
import { Statistics } from '../components/Statistics';
export const Dashboard: FC = () => {
- useEffect(() => {
- useDashboard.getState().refetchUsers();
- }, []);
return (
<VStack justifyContent="space-between" p="6" rowGap={4}>
<Box w="full">
diff --git a/app/dashboard/src/pages/Services.tsx b/app/dashboard/src/pages/Services.tsx
@@ -2,15 +2,16 @@ import { FC, useEffect } from 'react';
import { Box, VStack } from '@chakra-ui/react';
import { ServicesFilters } from 'components/ServicesFilters';
import { ServicesTable } from 'components/ServicesTable';
-import { useDashboard } from 'contexts/DashboardContext';
import { pages } from 'constants/Pages';
import { ServiceDialog } from 'components/ServiceDialog';
+import { useServices } from 'contexts/ServicesContext';
+import { useDashboard } from 'contexts/DashboardContext';
export const ServicesPage: FC = () => {
useEffect(() => {
useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/services'));
- useDashboard.getState().refetchServices();
+ useServices.getState().refetchServices();
}, []);
return (
<VStack justifyContent="space-between" maxH="100vh" p="2rem" rowGap={4}>
diff --git a/app/dashboard/src/pages/Users.tsx b/app/dashboard/src/pages/Users.tsx
@@ -11,12 +11,13 @@ import { QRCodeDialog } from 'components/QRCodeDialog';
import { ResetUserUsageModal } from 'components/ResetUserUsageModal';
import { RevokeSubscriptionModal } from 'components/RevokeSubscriptionModal';
import { pages } from 'constants/Pages';
+import { useUsers } from 'contexts/UsersContext';
export const UsersPage = () => {
useEffect(() => {
useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/users'));
- useDashboard.getState().refetchUsers();
+ useUsers.getState().refetchUsers();
}, []);
return (
<VStack justifyContent="space-between" maxH="100vh" p="2rem" rowGap={4}>
diff --git a/app/dashboard/src/types/Filter.ts b/app/dashboard/src/types/Filter.ts
@@ -0,0 +1,23 @@
+
+
+export type FilterType = {
+ limit?: number;
+ offset?: number;
+ sort: string;
+}
+
+export type UsersFilterType = FilterType & {
+ username?: string;
+ status?: 'active' | 'disabled' | 'limited' | 'expired' | 'on_hold';
+};
+
+export type FilterUsageType = {
+ start?: string;
+ end?: string;
+};
+
+export type ServicesFilterType = FilterType & {
+ name?: string;
+ users?: number;
+ inbounds?: number;
+};
diff --git a/app/dashboard/src/types/Inbounds.ts b/app/dashboard/src/types/Inbounds.ts
@@ -0,0 +1,12 @@
+import { ProtocolType } from './Protocol';
+
+export type InboundType = {
+ id: number;
+ tag: string;
+ protocol: ProtocolType;
+ network: string;
+ tls: string;
+ port?: number;
+};
+
+export type Inbounds = InboundType[];
diff --git a/app/dashboard/src/types/Protocol.ts b/app/dashboard/src/types/Protocol.ts
@@ -0,0 +1,3 @@
+
+export type ProtocolType = 'vmess' | 'vless' | 'trojan' | 'shadowsocks';
+
diff --git a/app/dashboard/src/types/User.ts b/app/dashboard/src/types/User.ts
@@ -6,6 +6,8 @@ export type Status =
| 'on_hold'
| 'error'
| 'connecting'
+ | 'healthy'
+ | 'unhealthy'
| 'connected';
export type ProxyKeys = ('vmess' | 'vless' | 'trojan' | 'shadowsocks')[];
export type ProxyType = {