diff --git a/app/dashboard/public/locales/en.json b/app/dashboard/public/locales/en.json
index 766c039..66a7620 100644
--- a/app/dashboard/public/locales/en.json
+++ b/app/dashboard/public/locales/en.json
@@ -1,4 +1,6 @@
{
+ "home": "Home",
+ "hosts": "Hosts",
"password": "Password",
"cancel": "Cancel",
"apply": "Apply",
@@ -9,6 +11,7 @@
"username": "Username",
"name": "Name",
"node": "Node",
+ "nodes": "Nodes",
"xrayVersion": "xray version",
"addNode": "Add Node",
"expires": "Expires in {{time}}",
@@ -36,13 +39,6 @@
"donation": "Donation",
"logout": "Log out"
},
- "pageHeaderTitle": {
- "Home": "Home",
- "Users": "Users",
- "Services": "Services",
- "Host": "Host",
- "Nodes": "Nodes"
- },
"deleteUser": {
"title": "Delete User",
"prompt": "Are you sure you want to delete {{username}}?",
@@ -121,8 +117,6 @@
},
"nodeDialog": {
"editNodeTitle": "Edit node",
- "editNode": "Edit node",
- "addNode": "Add node",
"createNewNode": "Create node",
"nodeEdited": "Node {{name}} edited.",
"nodeCreated": "Node {{name}} created.",
@@ -130,7 +124,25 @@
"optional": "optional",
"total": "Total: ",
"updateSubmit": "Submit update",
- "addSubmit": "Submit"
+ "addSubmit": "Submit",
+ "title": "Using Marzban-Node, you are able to scale up your connection quality by adding different nodes on different servers.",
+ "addNewMarzbanNode": "Add New Marzban Node",
+ "certificate": "Certificate",
+ "connection-hint": "To setup a Marzban Node, you need to set this certificate on the node to initialize a secure connection between main server and the node",
+ "download-certificate": "Download certificate",
+ "show-certificate": "Show certificate",
+ "hide-certificate": "Hide certificate",
+ "addHostForEveryInbound": "Add this node as a new host for every inbound",
+ "addNode": "Add Node",
+ "addNodeSuccess": "Node {{name}} added successfully",
+ "apply": "editNode",
+ "nodeName": "Name",
+ "nodeAddress": "Address",
+ "nodePort": "Port",
+ "nodeAPIPort": "API Port",
+ "editNode": "Update Node",
+ "reconnecting": "Reconnecting...",
+ "reconnect": "Reconnect"
},
"serviceDialog": {
"editServiceTitle": "Edit service",
@@ -179,26 +191,6 @@
"host.wildcard": "Use * to generate a random string (works for wildcard domain names)"
},
"revoke": "Revoke",
- "nodes": {
- "title": "Using Marzban-Node, you are able to scale up your connection quality by adding different nodes on different servers.",
- "addNewMarzbanNode": "Add New Marzban Node",
- "certificate": "Certificate",
- "connection-hint": "To setup a Marzban Node, you need to set this certificate on the node to initialize a secure connection between main server and the node",
- "download-certificate": "Download certificate",
- "show-certificate": "Show certificate",
- "hide-certificate": "Hide certificate",
- "addHostForEveryInbound": "Add this node as a new host for every inbound",
- "addNode": "Add Node",
- "addNodeSuccess": "Node {{name}} added successfully",
- "apply": "editNode",
- "nodeName": "Name",
- "nodeAddress": "Address",
- "nodePort": "Port",
- "nodeAPIPort": "API Port",
- "editNode": "Update Node",
- "reconnecting": "Reconnecting...",
- "reconnect": "Reconnect"
- },
"deleteNode.title": "Delete Node",
"deleteNode.prompt": "Are you sure you want to delete the {{name}} node?",
"deleteNode.deleteSuccess": "Node {{name}} removed successfully",
diff --git a/app/dashboard/src/components/Footer.tsx b/app/dashboard/src/components/Footer.tsx
index 3dcd9fd..94f674b 100644
--- a/app/dashboard/src/components/Footer.tsx
+++ b/app/dashboard/src/components/Footer.tsx
@@ -1,6 +1,6 @@
import { BoxProps, HStack, Link, Text } from '@chakra-ui/react';
import { ORGANIZATION_URL, REPO_URL } from 'constants/Project';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useDashboard } from 'stores';
import { FC } from 'react';
export const Footer: FC = (props) => {
diff --git a/app/dashboard/src/components/StatusBadge.tsx b/app/dashboard/src/components/StatusBadge.tsx
index b5a7c54..ab43bab 100644
--- a/app/dashboard/src/components/StatusBadge.tsx
+++ b/app/dashboard/src/components/StatusBadge.tsx
@@ -3,7 +3,7 @@ import { Badge, Text } from '@chakra-ui/react';
import { statusColors } from 'constants/Settings';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
-import { Status as StatusType } from 'types/User';
+import { Status as StatusType } from 'types';
import { relativeExpiryDate } from 'utils/dateFormatter';
type StatusProps = {
diff --git a/app/dashboard/src/components/UsageFilter.tsx b/app/dashboard/src/components/UsageFilter.tsx
index d37c683..36576fd 100644
--- 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 'types/Filter';
+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/dialog/ModalHeader.tsx b/app/dashboard/src/components/dialog/ModalHeader.tsx
index 1ee02c5..23a7599 100644
--- a/app/dashboard/src/components/dialog/ModalHeader.tsx
+++ b/app/dashboard/src/components/dialog/ModalHeader.tsx
@@ -1,6 +1,6 @@
import { Text, HStack, ModalHeader } from '@chakra-ui/react';
-import { ChakraIcon } from 'types/ChakraIcon';
+import { ChakraIcon } from 'types';
import { Icon } from '../icon';
export const DialogModalHeader = ({ HeaderIcon, title }: { HeaderIcon: ChakraIcon, title: string }) => {
diff --git a/app/dashboard/src/components/header/index.tsx b/app/dashboard/src/components/header/index.tsx
index 4f09473..1877895 100644
--- a/app/dashboard/src/components/header/index.tsx
+++ b/app/dashboard/src/components/header/index.tsx
@@ -18,8 +18,7 @@ import {
} from '@heroicons/react/24/outline';
import { pages } from 'constants/Pages';
import { DONATION_URL, REPO_URL } from 'constants/Project';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useNodes } from 'contexts/NodesContext';
+import { useNodes, useDashboard } from 'stores';
import differenceInDays from 'date-fns/differenceInDays';
import isValid from 'date-fns/isValid';
import { FC, ReactNode, useState } from 'react';
diff --git a/app/dashboard/src/components/sidebar/sidebar-item.tsx b/app/dashboard/src/components/sidebar/sidebar-item.tsx
index 5722c8b..36dc6b6 100644
--- a/app/dashboard/src/components/sidebar/sidebar-item.tsx
+++ b/app/dashboard/src/components/sidebar/sidebar-item.tsx
@@ -1,18 +1,18 @@
import { Flex, Text } from '@chakra-ui/react'
-import { useDashboard } from 'contexts/DashboardContext';
+import { useDashboard } from 'stores';
import { Link as ReactRouterLink } from 'react-router-dom'
import { Link as ChakraLink } from '@chakra-ui/react'
import { Icon } from 'components/icon';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { PageType } from 'types/Page';
+import { PageType } from 'types';
export type SideBarItemProps = Pick<
- PageType,
- | 'name'
- | 'path'
- | 'itemIcon'
+ PageType,
+ | 'name'
+ | 'path'
+ | 'itemIcon'
>
export const SidebarItem: FC = ({ itemIcon: ItemIcon, name, path, index }) => {
diff --git a/app/dashboard/src/components/Table/EmptySection.tsx b/app/dashboard/src/components/table/empty-section.tsx
similarity index 100%
rename from app/dashboard/src/components/Table/EmptySection.tsx
rename to app/dashboard/src/components/table/empty-section.tsx
diff --git a/app/dashboard/src/components/Table/Icons.ts b/app/dashboard/src/components/table/icons.ts
similarity index 100%
rename from app/dashboard/src/components/Table/Icons.ts
rename to app/dashboard/src/components/table/icons.ts
diff --git a/app/dashboard/src/components/table/index.ts b/app/dashboard/src/components/table/index.ts
new file mode 100644
index 0000000..959892b
--- /dev/null
+++ b/app/dashboard/src/components/table/index.ts
@@ -0,0 +1,5 @@
+export * from './icons';
+export * from './sort';
+export * from './pagination';
+export * from './status-sort-select';
+export * from './empty-section';
diff --git a/app/dashboard/src/components/Table/Pagination.tsx b/app/dashboard/src/components/table/pagination.tsx
similarity index 98%
rename from app/dashboard/src/components/Table/Pagination.tsx
rename to app/dashboard/src/components/table/pagination.tsx
index 995a5f8..c734d1b 100644
--- a/app/dashboard/src/components/Table/Pagination.tsx
+++ b/app/dashboard/src/components/table/pagination.tsx
@@ -13,7 +13,7 @@ import {
} from '@heroicons/react/24/outline';
import { ChangeEvent, FC } from 'react';
import { useTranslation } from 'react-i18next';
-import { FilterType } from 'types/Filter';
+import { FilterType } from 'types';
import { setUsersPerPageLimitSize } from 'utils/userPreferenceStorage';
const PrevIcon = chakra(ArrowLongLeftIcon, {
diff --git a/app/dashboard/src/components/Table/Sort.tsx b/app/dashboard/src/components/table/sort.tsx
similarity index 78%
rename from app/dashboard/src/components/Table/Sort.tsx
rename to app/dashboard/src/components/table/sort.tsx
index 98849e3..f570880 100644
--- a/app/dashboard/src/components/Table/Sort.tsx
+++ b/app/dashboard/src/components/table/sort.tsx
@@ -1,9 +1,9 @@
-import { SortIcon } from './Icons';
+import { SortIcon } from './icons';
import { FC } from 'react';
type SortType = {
- sort: string;
- column: string;
+ sort: string;
+ column: string;
};
export const Sort: FC = ({ sort, column }) => {
diff --git a/app/dashboard/src/components/Table/StatusSortSelect.tsx b/app/dashboard/src/components/table/status-sort-select.tsx
similarity index 94%
rename from app/dashboard/src/components/Table/StatusSortSelect.tsx
rename to app/dashboard/src/components/table/status-sort-select.tsx
index 24bf606..869fc05 100644
--- a/app/dashboard/src/components/Table/StatusSortSelect.tsx
+++ b/app/dashboard/src/components/table/status-sort-select.tsx
@@ -1,6 +1,6 @@
import { Select } from '@chakra-ui/react'
-import { FilterType } from 'types/Filter';
+import { FilterType } from 'types';
import React from 'react'
interface StatusSortSelectProps {
diff --git a/app/dashboard/src/constants/Pages.tsx b/app/dashboard/src/constants/Pages.tsx
index ddef501..a7f15b1 100644
--- a/app/dashboard/src/constants/Pages.tsx
+++ b/app/dashboard/src/constants/Pages.tsx
@@ -1,4 +1,4 @@
-import { PageType } from 'types/Page';
+import { PageType } from 'types';
import { chakra } from '@chakra-ui/react';
import { CubeIcon, HomeIcon, ServerIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline';
import { IconBaseStyle } from 'constants/IconBaseStyle';
@@ -11,28 +11,28 @@ const HomeSidebarItemIcon = chakra(HomeIcon, IconBaseStyle);
export const pages: PageType[] = [
{
- name: 'pageHeaderTitle.Home',
+ name: 'home',
path: '/',
itemIcon: HomeSidebarItemIcon,
},
{
- name: 'pageHeaderTitle.Users',
+ name: 'users',
path: '/users',
itemIcon: UsersSidebarItemIcon,
},
{
- name: 'pageHeaderTitle.Services',
+ name: 'services',
path: '/services',
itemIcon: ServicesSidebarItemIcon,
},
{
- name: 'pageHeaderTitle.Nodes',
+ name: 'nodes',
path: '/nodes',
itemIcon: NodesSidebarItemIcon,
},
{
- name: 'pageHeaderTitle.Host',
- path: '/host',
+ name: 'hosts',
+ path: '/hosts',
itemIcon: HostSidebarItemIcon,
},
];
diff --git a/app/dashboard/src/constants/Proxies.tsx b/app/dashboard/src/constants/Proxies.tsx
index 127316a..ba5cbb1 100644
--- a/app/dashboard/src/constants/Proxies.tsx
+++ b/app/dashboard/src/constants/Proxies.tsx
@@ -1,4 +1,4 @@
-import { ProxyHostSecurity } from 'types/ProxyHosts';
+import { ProxyHostSecurity } from 'types';
export const proxyHostSecurity: { title: string; value: ProxyHostSecurity }[] =
[
diff --git a/app/dashboard/src/constants/Settings.tsx b/app/dashboard/src/constants/Settings.tsx
index f049fb2..f8c92a3 100644
--- a/app/dashboard/src/constants/Settings.tsx
+++ b/app/dashboard/src/constants/Settings.tsx
@@ -6,7 +6,7 @@ import {
WifiIcon,
} from '@heroicons/react/24/outline';
import { ForwardRefExoticComponent, SVGProps } from 'react';
-import { DataLimitResetStrategy } from 'types/User';
+import { DataLimitResetStrategy } from 'types';
const iconProps = {
baseStyle: {
diff --git a/app/dashboard/src/contexts/HostsContext.ts b/app/dashboard/src/contexts/HostsContext.ts
deleted file mode 100644
index 5c911ff..0000000
--- a/app/dashboard/src/contexts/HostsContext.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { fetch } from 'service/http';
-import { create } from 'zustand';
-
-type HostsSchema = Record<
- string,
- {
- remark: string;
- address: string;
- port: number | null;
- path: string | null;
- sni: string | null;
- host: string | null;
- }[]
->;
-
-type HostsStore = {
- isLoading: boolean;
- isPostLoading: boolean;
- hosts: HostsSchema;
- fetchHosts: () => void;
- setHosts: (hosts: HostsSchema) => Promise;
- onEditingHosts: (isEditingHosts: boolean) => void;
- isEditingHosts: boolean;
-};
-export const useHosts = create((set) => ({
- isLoading: false,
- isPostLoading: false,
- hosts: {},
- isEditingHosts: false,
- onEditingHosts: (isEditingHosts: boolean) => {
- set({ isEditingHosts });
- },
- fetchHosts: () => {
- set({ isLoading: true });
- fetch('/hosts')
- .then((hosts) => set({ hosts }))
- .finally(() => set({ isLoading: false }));
- },
- setHosts: (body) => {
- set({ isPostLoading: true });
- return fetch('/hosts', { method: 'PUT', body }).finally(() => {
- set({ isPostLoading: false });
- });
- },
-}));
diff --git a/app/dashboard/src/contexts/InboundsContext.ts b/app/dashboard/src/contexts/InboundsContext.ts
deleted file mode 100644
index 580d7bd..0000000
--- a/app/dashboard/src/contexts/InboundsContext.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-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(() => ({
- inbounds: [],
- refetchInbounds: () => {
- fetchInbounds();
- },
- }))
-);
diff --git a/app/dashboard/src/layouts/console-layout.tsx b/app/dashboard/src/layouts/console-layout.tsx
index 1f768f8..be59e79 100644
--- a/app/dashboard/src/layouts/console-layout.tsx
+++ b/app/dashboard/src/layouts/console-layout.tsx
@@ -5,7 +5,7 @@ import { Header } from 'components/header';
import { Sidebar } from 'components/sidebar';
import { Suspense, useRef } from 'react';
import { Outlet } from 'react-router-dom';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useDashboard } from 'stores';
export const ConsoleLayout = () => {
const ref = useRef(null);
diff --git a/app/dashboard/src/pages/home/statistics.tsx b/app/dashboard/src/pages/home/statistics.tsx
index ae7c6b6..8e3c166 100644
--- a/app/dashboard/src/pages/home/statistics.tsx
+++ b/app/dashboard/src/pages/home/statistics.tsx
@@ -4,7 +4,7 @@ import {
ChartPieIcon,
UsersIcon,
} from '@heroicons/react/24/outline';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useDashboard } from 'stores';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
@@ -75,7 +75,7 @@ export const Statistics: FC = (props) => {
display="inline-block"
pb="5px"
>
- / {numberWithCommas(systemData.total_user)}
+ / {numberWithCommas(systemData.total_user)}
)
@@ -86,9 +86,9 @@ export const Statistics: FC = (props) => {
title={t('dataUsage')}
content={
systemData &&
- formatBytes(
- systemData.incoming_bandwidth + systemData.outgoing_bandwidth
- )
+ formatBytes(
+ systemData.incoming_bandwidth + systemData.outgoing_bandwidth
+ )
}
icon={}
/>
diff --git a/app/dashboard/src/pages/host/dialog.tsx b/app/dashboard/src/pages/host/dialog.tsx
deleted file mode 100644
index 51357c0..0000000
--- a/app/dashboard/src/pages/host/dialog.tsx
+++ /dev/null
@@ -1,906 +0,0 @@
-import {
- Accordion,
- AccordionButton,
- AccordionIcon,
- AccordionItem,
- AccordionPanel,
- Badge,
- Box,
- Button,
- Select as ChakraSelect,
- FormControl,
- FormErrorMessage,
- FormLabel,
- HStack,
- IconButton,
- InputGroup,
- InputRightElement,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalHeader,
- ModalOverlay,
- Popover,
- PopoverArrow,
- PopoverBody,
- PopoverCloseButton,
- PopoverContent,
- PopoverTrigger,
- Portal,
- Text,
- Tooltip,
- VStack,
- chakra,
- useToast,
-} from '@chakra-ui/react';
-import { InformationCircleIcon, LinkIcon } from '@heroicons/react/24/outline';
-import { zodResolver } from '@hookform/resolvers/zod';
-import {
- proxyALPN,
- proxyFingerprint,
- 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,
- useFieldArray,
- useForm,
- useFormContext,
-} from 'react-hook-form';
-import { Trans, useTranslation } from 'react-i18next';
-import 'slick-carousel/slick/slick-theme.css';
-import 'slick-carousel/slick/slick.css';
-import { z } from 'zod';
-import { DeleteIcon } from 'components/dialog/Icons';
-import { Icon } from 'components/icon';
-import { Input as CustomInput } from 'components/Input';
-
-const Select = chakra(ChakraSelect, {
- baseStyle: {
- bg: 'white',
- _dark: {
- bg: 'gray.700',
- },
- },
-});
-
-const Input = chakra(CustomInput, {
- baseStyle: {
- bg: 'white',
- _dark: {
- bg: 'gray.700',
- },
- },
-});
-
-const ModalIcon = chakra(LinkIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-const InfoIcon = chakra(InformationCircleIcon, {
- baseStyle: {
- w: 4,
- h: 4,
- color: 'gray.400',
- cursor: 'pointer',
- },
-});
-
-const hostsSchema = z.record(
- z.string().min(1),
- z.array(
- z.object({
- remark: z.string().min(1, 'Remark is required'),
- address: z.string().min(1, 'Address is required'),
- port: z
- .string()
- .or(z.number())
- .nullable()
- .transform((value) => {
- if (typeof value === 'number') return value;
- if (value !== null && !isNaN(parseInt(value)))
- return Number(parseInt(value));
- return null;
- }),
- path: z.string().nullable(),
- sni: z.string().nullable(),
- host: z.string().nullable(),
- security: z.string(),
- alpn: z.string(),
- fingerprint: z.string(),
- }),
- ),
-);
-
-const Error = chakra(FormErrorMessage, {
- baseStyle: {
- color: 'red.400',
- display: 'block',
- textAlign: 'left',
- w: '100%',
- },
-});
-
-type AccordionInboundType = {
- hostKey: string;
- isOpen: boolean;
- toggleAccordion: () => void;
-};
-
-const AccordionInbound: FC = ({
- hostKey,
- isOpen,
- toggleAccordion,
-}) => {
- const { inbounds } = useInbounds();
- const inbound = [...inbounds.values()]
- .flat()
- .filter((inbound) => inbound.tag === hostKey)[0];
-
- const form = useFormContext>();
- const {
- fields: hosts,
- append: addHost,
- remove: removeHost,
- } = useFieldArray({
- control: form.control,
- name: hostKey,
- });
- const { errors } = form.formState;
- const { t } = useTranslation();
- const accordionErrors = errors[hostKey];
- const handleAddHost = () => {
- addHost({
- host: '',
- sni: '',
- port: null,
- path: null,
- address: '',
- remark: '',
- security: 'inbound_default',
- alpn: '',
- fingerprint: '',
- });
- };
- useEffect(() => {
- if (accordionErrors && !isOpen) {
- toggleAccordion();
- }
- }, [accordionErrors]);
- return (
-
-
-
- {hostKey}
-
-
-
-
-
- {hosts.map((host, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.desc')}
-
-
- {'{'}SERVER_IP{'}'}
- {' '}
- {t('hostsDialog.currentServer')}
-
-
-
- {'{'}USERNAME{'}'}
- {' '}
- {t('hostsDialog.username')}
-
-
-
- {'{'}DATA_USAGE{'}'}
- {' '}
- {t('hostsDialog.dataUsage')}
-
-
-
- {'{'}DATA_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingData')}
-
-
-
- {'{'}DATA_LIMIT{'}'}
- {' '}
- {t('hostsDialog.dataLimit')}
-
-
-
- {'{'}DAYS_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingDays')}
-
-
-
- {'{'}EXPIRE_DATE{'}'}
- {' '}
- {t('hostsDialog.expireDate')}
-
-
-
- {'{'}JALALI_EXPIRE_DATE{'}'}
- {' '}
- {t('hostsDialog.jalaliExpireDate')}
-
-
-
- {'{'}TIME_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingTime')}
-
-
-
- {'{'}STATUS_EMOJI{'}'}
- {' '}
- {t('hostsDialog.statusEmoji')}
-
-
-
- {'{'}PROTOCOL{'}'}
- {' '}
- {t('hostsDialog.proxyProtocol')}
-
-
-
- {'{'}TRANSPORT{'}'}
- {' '}
- {t('hostsDialog.proxyMethod')}
-
-
-
-
-
-
-
-
- {accordionErrors && accordionErrors[index]?.remark && (
- {accordionErrors[index]?.remark?.message}
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.desc')}
-
-
- {'{'}SERVER_IP{'}'}
- {' '}
- {t('hostsDialog.currentServer')}
-
-
-
- {'{'}USERNAME{'}'}
- {' '}
- {t('hostsDialog.username')}
-
-
-
- {'{'}DATA_USAGE{'}'}
- {' '}
- {t('hostsDialog.dataUsage')}
-
-
-
- {'{'}DATA_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingData')}
-
-
-
- {'{'}DATA_LIMIT{'}'}
- {' '}
- {t('hostsDialog.dataLimit')}
-
-
-
- {'{'}DAYS_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingDays')}
-
-
-
- {'{'}EXPIRE_DATE{'}'}
- {' '}
- {t('hostsDialog.expireDate')}
-
-
-
- {'{'}JALALI_EXPIRE_DATE{'}'}
- {' '}
- {t('hostsDialog.jalaliExpireDate')}
-
-
-
- {'{'}TIME_LEFT{'}'}
- {' '}
- {t('hostsDialog.remainingTime')}
-
-
-
- {'{'}STATUS_EMOJI{'}'}
- {' '}
- {t('hostsDialog.statusEmoji')}
-
-
-
- {'{'}PROTOCOL{'}'}
- {' '}
- {t('hostsDialog.proxyProtocol')}
-
-
-
- {'{'}TRANSPORT{'}'}
- {' '}
- {t('hostsDialog.proxyMethod')}
-
-
-
-
-
-
-
-
- {accordionErrors && accordionErrors[index]?.address && (
- {accordionErrors[index]?.address?.message}
- )}
-
-
-
-
-
-
- {t('hostsDialog.advancedOptions')}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.port')}
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.port.info')}
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.sni')}
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.sni.info')}
-
-
- ,
- }}
- />
-
-
- ,
- }}
- />
-
-
-
-
-
-
- {accordionErrors && accordionErrors[index]?.sni && (
- {accordionErrors[index]?.sni?.message}
- )}
-
-
-
- {t('hostsDialog.host')}
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.host.info')}
-
-
- ,
- }}
- />
-
-
- ,
- }}
- />
-
-
-
-
-
-
- {accordionErrors && accordionErrors[index]?.host && (
- {accordionErrors[index]?.host?.message}
- )}
-
-
-
-
- {t('hostsDialog.path')}
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.path.info')}
-
-
-
-
-
-
- {accordionErrors && accordionErrors[index]?.path && (
- {accordionErrors[index]?.path?.message}
- )}
-
-
-
-
- {t('hostsDialog.security')}
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.security.info')}
-
-
-
-
-
-
-
-
-
-
- {t('hostsDialog.alpn')}
-
-
-
-
-
-
- {t('hostsDialog.fingerprint')}
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
- );
-};
-
-export const HostsDialog: FC = () => {
- const { refetchUsers } = useUsers();
- const { isEditingHosts, onEditingHosts, isLoading, hosts, fetchHosts, isPostLoading, setHosts } = useHosts();
- const toast = useToast();
- const { t } = useTranslation();
- const [openAccordions, setOpenAccordions] = useState({});
-
- useEffect(() => {
- if (isEditingHosts) fetchHosts();
- }, [isEditingHosts]);
- const form = useForm>({
- resolver: zodResolver(hostsSchema),
- });
-
- useEffect(() => {
- if (hosts && isEditingHosts) {
- form.reset(hosts);
- }
- }, [hosts]);
-
- const onClose = () => {
- setOpenAccordions({});
- onEditingHosts(false);
- };
- const handleFormSubmit = (hosts: z.infer) => {
- setHosts(hosts)
- .then(() => {
- toast({
- title: t('hostsDialog.savedSuccess'),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- refetchUsers();
- })
- .catch((err) => {
- if (err?.response?.status === 409 || err?.response?.status === 400) {
- toast({
- title: err.response?._data?.detail,
- status: 'error',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- }
- if (err?.response?.status === 422) {
- Object.keys(err.response._data.detail).forEach((key) => {
- toast({
- title: err.response._data.detail[key] + ' (' + key + ')',
- status: 'error',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- });
- }
- });
- };
-
- const toggleAccordion = (index: number) => {
- if (openAccordions[String(index)]) {
- delete openAccordions[String(index)];
- } else openAccordions[String(index)] = {};
-
- setOpenAccordions({ ...openAccordions });
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/app/dashboard/src/pages/hosts/dialog/default-values.ts b/app/dashboard/src/pages/hosts/dialog/default-values.ts
new file mode 100644
index 0000000..f6627cc
--- /dev/null
+++ b/app/dashboard/src/pages/hosts/dialog/default-values.ts
@@ -0,0 +1,14 @@
+import { HostSchema } from 'stores';
+export const getDefaultValues = (): HostSchema => {
+ return {
+ host: '',
+ sni: '',
+ port: null,
+ path: '',
+ address: '',
+ remark: '',
+ security: 'inbound_default',
+ alpn: '',
+ fingerprint: '',
+ }
+};
diff --git a/app/dashboard/src/pages/hosts/dialog/icons.tsx b/app/dashboard/src/pages/hosts/dialog/icons.tsx
new file mode 100644
index 0000000..2787c25
--- /dev/null
+++ b/app/dashboard/src/pages/hosts/dialog/icons.tsx
@@ -0,0 +1,19 @@
+
+import { chakra } from '@chakra-ui/react';
+import { InformationCircleIcon, LinkIcon } from '@heroicons/react/24/outline';
+
+export const ModalIcon = chakra(LinkIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export const InfoIcon = chakra(InformationCircleIcon, {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ color: 'gray.400',
+ cursor: 'pointer',
+ },
+});
diff --git a/app/dashboard/src/pages/hosts/dialog/index.tsx b/app/dashboard/src/pages/hosts/dialog/index.tsx
new file mode 100644
index 0000000..25e7398
--- /dev/null
+++ b/app/dashboard/src/pages/hosts/dialog/index.tsx
@@ -0,0 +1,482 @@
+import {
+ Button,
+ Select as ChakraSelect,
+ FormControl,
+ FormLabel,
+ HStack,
+ IconButton,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalOverlay,
+ Tooltip,
+ VStack,
+ chakra,
+ useToast,
+ Spacer,
+ Popover,
+ PopoverTrigger,
+ Portal,
+ PopoverContent,
+ PopoverArrow,
+ PopoverCloseButton,
+ Text,
+ Badge,
+ Alert,
+ AlertIcon,
+ InputRightElement,
+ InputGroup,
+} from '@chakra-ui/react';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { FC, useEffect, useState } from 'react';
+import { useInbounds, HostType, HostSchema, hostSchema } from 'stores';
+import { Trans, useTranslation } from 'react-i18next';
+import { getDefaultValues } from './default-values';
+import { useForm } from 'react-hook-form';
+import 'slick-carousel/slick/slick-theme.css';
+import 'slick-carousel/slick/slick.css';
+import { AddIcon, DeleteIcon } from 'components/dialog/Icons';
+import { Input as CustomInput } from 'components/Input';
+import { DialogModalHeader, } from 'components/dialog/ModalHeader';
+import { EditIcon } from 'components/table';
+import { InfoIcon } from './icons';
+import {
+ proxyALPN,
+ proxyFingerprint,
+ proxyHostSecurity,
+} from 'constants/Proxies';
+import { RemarkFieldPopover } from './remark-field-popover';
+
+export const Select = chakra(ChakraSelect, {
+ baseStyle: {
+ bg: 'white',
+ _dark: {
+ bg: 'gray.700',
+ },
+ },
+});
+
+export const Input = chakra(CustomInput, {
+ baseStyle: {
+ bg: 'white',
+ _dark: {
+ bg: 'gray.700',
+ },
+ },
+});
+
+/* TODO: Impelement specific errors and input verifications
+ * */
+export const HostsDialog: FC = () => {
+ const {
+ selectedHost, isEditingHost, isCreatingHost, selectedInbound,
+ onEditingHost, editHost, createHost, onCreatingHost, onDeletingHost
+ } = useInbounds();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const { t } = useTranslation();
+ const toast = useToast();
+ const defaultValueHost: HostSchema = (isEditingHost === true && selectedHost !== null) ? selectedHost : getDefaultValues();
+ const form = useForm({
+ resolver: zodResolver(hostSchema),
+ defaultValues: defaultValueHost,
+ });
+ const isOpen = isEditingHost || isCreatingHost;
+ const onClose = () => {
+ onCreatingHost(false);
+ onEditingHost(false, null);
+ };
+ useEffect(() => {
+ if (isOpen) {
+ form.reset(defaultValueHost);
+ }
+ }, [isOpen]);
+
+ const submit = async (values: HostSchema | HostType) => {
+ setLoading(true);
+ const methods = { edited: editHost, created: createHost };
+ const method = isEditingHost ? 'edited' : 'created';
+ const refId = (isEditingHost ? selectedHost?.id : null) ?? selectedInbound?.id;
+ setError(null);
+ await methods[method](refId, values)
+ .then(() => {
+ toast({
+ title: t(
+ isEditingHost ? 'hostDialog.editHost' : 'hostDialog.createNewHost',
+ { hoshtName: values.host }
+ ),
+ status: 'success',
+ isClosable: true,
+ position: 'top',
+ duration: 3000,
+ });
+ // refetchNodes();
+ 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 'address' | 'port' | 'host' | 'path' | 'remark' | 'sni' | 'security' | 'alpn' | 'fingerprint',
+ {
+ type: 'custom',
+ message: err.response._data.detail[key],
+ }
+ );
+ });
+ }
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ {loading && 'loading...'}
+
+
+
+
+
+ );
+};
diff --git a/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx b/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx
new file mode 100644
index 0000000..0e665ed
--- /dev/null
+++ b/app/dashboard/src/pages/hosts/dialog/remark-field-popover.tsx
@@ -0,0 +1,113 @@
+
+import { InfoIcon } from './icons';
+import {
+ Badge,
+ Box,
+ Popover,
+ PopoverArrow,
+ PopoverBody,
+ PopoverCloseButton,
+ PopoverContent,
+ PopoverTrigger,
+ Portal,
+ Text,
+} from '@chakra-ui/react';
+import { useTranslation } from 'react-i18next';
+import 'slick-carousel/slick/slick-theme.css';
+import 'slick-carousel/slick/slick.css';
+
+export const RemarkFieldPopover = () => {
+ const { t } = useTranslation();
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('hostsDialog.desc')}
+
+
+ {'{'}SERVER_IP{'}'}
+ {' '}
+ {t('hostsDialog.currentServer')}
+
+
+
+ {'{'}USERNAME{'}'}
+ {' '}
+ {t('hostsDialog.username')}
+
+
+
+ {'{'}DATA_USAGE{'}'}
+ {' '}
+ {t('hostsDialog.dataUsage')}
+
+
+
+ {'{'}DATA_LEFT{'}'}
+ {' '}
+ {t('hostsDialog.remainingData')}
+
+
+
+ {'{'}DATA_LIMIT{'}'}
+ {' '}
+ {t('hostsDialog.dataLimit')}
+
+
+
+ {'{'}DAYS_LEFT{'}'}
+ {' '}
+ {t('hostsDialog.remainingDays')}
+
+
+
+ {'{'}EXPIRE_DATE{'}'}
+ {' '}
+ {t('hostsDialog.expireDate')}
+
+
+
+ {'{'}JALALI_EXPIRE_DATE{'}'}
+ {' '}
+ {t('hostsDialog.jalaliExpireDate')}
+
+
+
+ {'{'}TIME_LEFT{'}'}
+ {' '}
+ {t('hostsDialog.remainingTime')}
+
+
+
+ {'{'}STATUS_EMOJI{'}'}
+ {' '}
+ {t('hostsDialog.statusEmoji')}
+
+
+
+ {'{'}PROTOCOL{'}'}
+ {' '}
+ {t('hostsDialog.proxyProtocol')}
+
+
+
+ {'{'}TRANSPORT{'}'}
+ {' '}
+ {t('hostsDialog.proxyMethod')}
+
+
+
+
+
+
+ );
+}
diff --git a/app/dashboard/src/pages/host/index.tsx b/app/dashboard/src/pages/hosts/index.tsx
similarity index 72%
rename from app/dashboard/src/pages/host/index.tsx
rename to app/dashboard/src/pages/hosts/index.tsx
index 16efd76..eb7101b 100644
--- a/app/dashboard/src/pages/host/index.tsx
+++ b/app/dashboard/src/pages/hosts/index.tsx
@@ -1,7 +1,7 @@
import { HostsDialog } from './dialog';
import Page from 'components/page';
-const HostPage = () => {
+const HostsPage = () => {
return (
@@ -9,4 +9,4 @@ const HostPage = () => {
)
}
-export default HostPage;
+export default HostsPage;
diff --git a/app/dashboard/src/pages/login/index.tsx b/app/dashboard/src/pages/login/index.tsx
index b5f1cfa..f770ae7 100644
--- a/app/dashboard/src/pages/login/index.tsx
+++ b/app/dashboard/src/pages/login/index.tsx
@@ -22,7 +22,7 @@ import { fetch } from 'service/http';
import { ReactComponent as Logo } from 'assets/logo.svg';
import { useTranslation } from 'react-i18next';
import { Language } from 'components/Language';
-import { useAdmin } from 'contexts/AdminContext';
+import { useAdmin } from 'stores';
const schema = z.object({
username: z.string().min(1, 'login.fieldRequired'),
diff --git a/app/dashboard/src/pages/nodes/dialog.tsx b/app/dashboard/src/pages/nodes/dialog.tsx
index c3d7af2..d6507db 100644
--- a/app/dashboard/src/pages/nodes/dialog.tsx
+++ b/app/dashboard/src/pages/nodes/dialog.tsx
@@ -22,7 +22,7 @@ import {
NodeCreate,
NodeCreateSchema,
useNodes,
-} from 'contexts/NodesContext';
+} from 'stores';
import { FC, useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -133,7 +133,7 @@ export const NodesDialog: FC = () => {
{
{
{
{!editingNode && (
- {t('nodes.addHostForEveryInbound')}
+ {t('nodeDialog.addHostForEveryInbound')}
)}
diff --git a/app/dashboard/src/pages/nodes/filters.tsx b/app/dashboard/src/pages/nodes/filters.tsx
index 8bb9a9b..5686420 100644
--- a/app/dashboard/src/pages/nodes/filters.tsx
+++ b/app/dashboard/src/pages/nodes/filters.tsx
@@ -19,13 +19,12 @@ import {
XMarkIcon,
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useNodes } from 'contexts/NodesContext';
+import { useNodes, useDashboard } from 'stores';
import debounce from 'lodash.debounce';
import React, { FC, useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useTranslation } from 'react-i18next';
-import { CopiedIcon, SubscriptionLinkIcon } from 'components/Table/Icons';
+import { CopiedIcon, SubscriptionLinkIcon } from 'components/table';
const iconProps = {
baseStyle: {
diff --git a/app/dashboard/src/pages/nodes/index.tsx b/app/dashboard/src/pages/nodes/index.tsx
index 64cd905..2919d0c 100644
--- a/app/dashboard/src/pages/nodes/index.tsx
+++ b/app/dashboard/src/pages/nodes/index.tsx
@@ -6,8 +6,7 @@ import { NodesUsage } from './usage';
import { NodesTable } from './table';
import { NodesFilters } from './filters';
import { DeleteNodeModal } from './modals/delete-modal';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useNodes } from 'contexts/NodesContext';
+import { useNodes, useDashboard } from 'stores';
import { pages } from 'constants/Pages';
import Page from 'components/page';
diff --git a/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
index 64f7831..fbbbe60 100644
--- a/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
+++ b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
@@ -27,8 +27,7 @@ import {
} from '@heroicons/react/24/outline';
import { joinPaths } from '@remix-run/router';
import classNames from 'classnames';
-import { useCoreSettings } from 'contexts/CoreSettingsContext';
-import { useNodes } from 'contexts/NodesContext';
+import { useNodes, useCoreSettings } from 'stores';
import debounce from 'lodash.debounce';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
diff --git a/app/dashboard/src/pages/nodes/modals/delete-modal.tsx b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx
index c44e774..7b61390 100644
--- a/app/dashboard/src/pages/nodes/modals/delete-modal.tsx
+++ b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx
@@ -1,7 +1,7 @@
import {
useToast,
} from '@chakra-ui/react';
-import { useNodes } from 'contexts/NodesContext';
+import { useNodes } from 'stores';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DeleteModal } from 'components/delete-modal';
diff --git a/app/dashboard/src/pages/nodes/table.tsx b/app/dashboard/src/pages/nodes/table.tsx
index 4688da3..0ee9dbe 100644
--- a/app/dashboard/src/pages/nodes/table.tsx
+++ b/app/dashboard/src/pages/nodes/table.tsx
@@ -25,16 +25,16 @@ import classNames from 'classnames';
import { t } from 'i18next';
import { FC, Fragment, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Pagination } from 'components/Table/Pagination';
import { StatusBadge } from 'components/StatusBadge';
-import { EmptySection } from 'components/Table/EmptySection';
import {
+ Pagination,
+ EmptySection,
AccordionArrowIcon,
EditIcon,
-} from 'components/Table/Icons';
-import { Sort } from 'components/Table/Sort';
-import { StatusSortSelect } from 'components/Table/StatusSortSelect';
-import { useNodes } from 'contexts/NodesContext';
+ Sort,
+ StatusSortSelect,
+} from 'components/table';
+import { useNodes } from 'stores';
import { Cog6ToothIcon } from '@heroicons/react/24/outline';
const iconProps = {
diff --git a/app/dashboard/src/pages/nodes/usage.tsx b/app/dashboard/src/pages/nodes/usage.tsx
index ba4fdd5..3678b60 100644
--- a/app/dashboard/src/pages/nodes/usage.tsx
+++ b/app/dashboard/src/pages/nodes/usage.tsx
@@ -6,8 +6,8 @@ import {
useColorMode,
} from '@chakra-ui/react';
import { ChartPieIcon } from '@heroicons/react/24/outline';
-import { FilterUsageType } from 'types/Filter';
-import { useNodes } from 'contexts/NodesContext';
+import { FilterUsageType } from 'types/filter';
+import { useNodes } from 'stores';
import dayjs from 'dayjs';
import { FC, Suspense, useEffect, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
diff --git a/app/dashboard/src/pages/routes.tsx b/app/dashboard/src/pages/routes.tsx
index db1637c..cd95a40 100644
--- a/app/dashboard/src/pages/routes.tsx
+++ b/app/dashboard/src/pages/routes.tsx
@@ -4,7 +4,7 @@ import { fetchAdminLoader } from 'service/http';
const ConsoleLayout = lazy(() => import('layouts/console-layout'));
const Home = lazy(() => import('./home'));
-const Host = lazy(() => import('./host'));
+const Hosts = lazy(() => import('./hosts'));
const Users = lazy(() => import('./users'));
const Login = lazy(() => import('./login'));
const Nodes = lazy(() => import('./nodes'));
@@ -34,8 +34,8 @@ export const router = createHashRouter([
element: ,
},
{
- path: '/host',
- element: ,
+ path: '/hosts',
+ element: ,
},
],
},
diff --git a/app/dashboard/src/pages/services/dialog/form-schema.ts b/app/dashboard/src/pages/services/dialog/form-schema.ts
index 36f96e4..79fb2b1 100644
--- a/app/dashboard/src/pages/services/dialog/form-schema.ts
+++ b/app/dashboard/src/pages/services/dialog/form-schema.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
-import { ServiceCreate } from 'types/Service';
+import { ServiceCreate } from 'types';
export type FormType = Pick;
export const schema = z.object({
diff --git a/app/dashboard/src/pages/services/dialog/inbounds-field.tsx b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx
index d9b46eb..32a320b 100644
--- a/app/dashboard/src/pages/services/dialog/inbounds-field.tsx
+++ b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx
@@ -1,5 +1,5 @@
import { FormControl, FormLabel, FormErrorMessage, Center, Spinner, Text, Spacer, Flex } from '@chakra-ui/react';
-import { Inbounds, InboundType } from 'types/Inbounds';
+import { Inbounds, InboundType } from 'types';
import { TFunction } from 'i18next';
import { Controller } from 'react-hook-form';
import { InboundsSelectList } from './inbounds-select-list';
diff --git a/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
index 5bcb5ea..fd665dc 100644
--- a/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
+++ b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
@@ -5,7 +5,7 @@ import {
Stack,
} from '@chakra-ui/react';
import { ListCheckBox } from 'components/list-check-box';
-import { Inbounds, InboundType } from 'types/Inbounds';
+import { Inbounds, InboundType } from 'types';
import { forwardRef, ReactNode } from 'react';
import {
useFormContext,
diff --git a/app/dashboard/src/pages/services/dialog/index.tsx b/app/dashboard/src/pages/services/dialog/index.tsx
index 850019a..c79797c 100644
--- a/app/dashboard/src/pages/services/dialog/index.tsx
+++ b/app/dashboard/src/pages/services/dialog/index.tsx
@@ -17,7 +17,6 @@ import {
IconButton,
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { InboundType } from 'types/Inbounds';
import { FC, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@@ -28,9 +27,8 @@ import { getDefaultValues } from './default-values';
import { schema, FormType } from './form-schema';
import { InboundsField } from './inbounds-field';
import { NameField } from './name-field';
-import { Service, ServiceCreate } from 'types/Service';
-import { useServices } from 'contexts/ServicesContext';
-import { useInbounds } from 'contexts/InboundsContext';
+import { InboundType, Service, ServiceCreate } from 'types';
+import { useInbounds, useServices } from 'stores';
const formatService = (service: Service): FormType => {
const inbounds: number[] = service.inbounds.map((inbound: number | InboundType): number => {
diff --git a/app/dashboard/src/pages/services/filters.tsx b/app/dashboard/src/pages/services/filters.tsx
index 6cfd7f8..e846b0a 100644
--- a/app/dashboard/src/pages/services/filters.tsx
+++ b/app/dashboard/src/pages/services/filters.tsx
@@ -18,8 +18,7 @@ import {
XMarkIcon,
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useServices } from 'contexts/ServicesContext';
+import { useDashboard, useServices } from 'stores';
import debounce from 'lodash.debounce';
import React, { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/app/dashboard/src/pages/services/index.tsx b/app/dashboard/src/pages/services/index.tsx
index ed826a0..1a8a5ff 100644
--- a/app/dashboard/src/pages/services/index.tsx
+++ b/app/dashboard/src/pages/services/index.tsx
@@ -4,8 +4,7 @@ import { ServicesFilters } from './filters';
import { ServicesTable } from './table';
import { pages } from 'constants/Pages';
import { ServiceDialog } from './dialog';
-import { useServices } from 'contexts/ServicesContext';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useDashboard, useServices } from 'stores';
import { DeleteServiceModal } from './modals/delete-modal';
import Page from 'components/page';
diff --git a/app/dashboard/src/pages/services/modals/delete-modal.tsx b/app/dashboard/src/pages/services/modals/delete-modal.tsx
index 3bb8133..e8d8d0f 100644
--- a/app/dashboard/src/pages/services/modals/delete-modal.tsx
+++ b/app/dashboard/src/pages/services/modals/delete-modal.tsx
@@ -1,7 +1,7 @@
import {
useToast,
} from '@chakra-ui/react';
-import { useServices } from 'contexts/ServicesContext';
+import { useServices } from 'stores';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DeleteModal } from 'components/delete-modal';
diff --git a/app/dashboard/src/pages/services/table.tsx b/app/dashboard/src/pages/services/table.tsx
index 1bb658e..08e6fb5 100644
--- a/app/dashboard/src/pages/services/table.tsx
+++ b/app/dashboard/src/pages/services/table.tsx
@@ -15,10 +15,12 @@ import {
import classNames from 'classnames';
import { FC, Fragment, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { EmptySection } from 'components/Table/EmptySection';
-import { Pagination } from 'components/Table/Pagination';
-import { Sort } from 'components/Table/Sort';
-import { useServices } from 'contexts/ServicesContext';
+import {
+ EmptySection,
+ Pagination,
+ Sort
+} from 'components/table';
+import { useServices } from 'stores';
type ServicesTableProps = {} & TableProps;
diff --git a/app/dashboard/src/pages/users/dialog/form-schema.ts b/app/dashboard/src/pages/users/dialog/form-schema.ts
index 868b3fd..9f452c8 100644
--- a/app/dashboard/src/pages/users/dialog/form-schema.ts
+++ b/app/dashboard/src/pages/users/dialog/form-schema.ts
@@ -1,6 +1,6 @@
import { z } from 'zod';
-import { UserCreate } from 'types/User';
+import { UserCreate } from 'types';
export type FormType = Pick;
export const schema = z.object({
diff --git a/app/dashboard/src/pages/users/dialog/index.tsx b/app/dashboard/src/pages/users/dialog/index.tsx
index af6c13e..0cb254d 100644
--- a/app/dashboard/src/pages/users/dialog/index.tsx
+++ b/app/dashboard/src/pages/users/dialog/index.tsx
@@ -21,7 +21,6 @@ import {
HStack,
} from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
-import { FilterUsageType } from 'types/Filter';
import dayjs from 'dayjs';
import { FC, useEffect, useState } from 'react';
import ReactApexChart from 'react-apexcharts';
@@ -29,8 +28,10 @@ import { FormProvider, useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import {
User,
+ FilterUsageType,
UserCreate,
-} from 'types/User';
+ Service
+} from 'types';
import { UsageFilter, createUsageConfig } from 'components/UsageFilter';
import { AddIcon, EditIcon } from 'components/dialog/Icons';
import { DialogModalHeader } from 'components/dialog/ModalHeader';
@@ -45,9 +46,7 @@ import { DataLimitField } from './data-limit-field';
import { PeriodicUsageReset } from './periodic-usage-reset';
import { ExpireDateField } from './expire-date-field';
import { NoteField } from './note-field';
-import { Service } from 'types/Service';
-import { useUsers } from 'contexts/UsersContext';
-import { useServices } from 'contexts/ServicesContext';
+import { useUsers, useServices } from 'stores';
const formatUser = (user: User): FormType => {
const services: number[] = user.services.map((service: number | Service): number => {
diff --git a/app/dashboard/src/pages/users/dialog/service-property-status.tsx b/app/dashboard/src/pages/users/dialog/service-property-status.tsx
index e1651fb..63cd06b 100644
--- a/app/dashboard/src/pages/users/dialog/service-property-status.tsx
+++ b/app/dashboard/src/pages/users/dialog/service-property-status.tsx
@@ -1,16 +1,15 @@
import { Flex } from '@chakra-ui/react';
import { FC, useState } from 'react';
-import { ChakraIcon } from 'types/ChakraIcon';
+import { ChakraIcon } from 'types';
type ServicePropertyStatusProps = {
- value: number;
- StatusIcon: ChakraIcon;
+ value: number;
+ StatusIcon: ChakraIcon;
}
export const ServicePropertyStatus: FC = ({ value, StatusIcon }) => {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [valueState, _] = useState(value);
+ const [valueState,] = useState(value);
return (
diff --git a/app/dashboard/src/pages/users/dialog/services-field.tsx b/app/dashboard/src/pages/users/dialog/services-field.tsx
index 30dbb66..e81e283 100644
--- a/app/dashboard/src/pages/users/dialog/services-field.tsx
+++ b/app/dashboard/src/pages/users/dialog/services-field.tsx
@@ -3,7 +3,7 @@ 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/Service';
+import { Service } from 'types';
import { ServicePropertyStatus } from './service-property-status';
import { ServicesSelectList } from './services-select-list';
diff --git a/app/dashboard/src/pages/users/dialog/services-select-list.tsx b/app/dashboard/src/pages/users/dialog/services-select-list.tsx
index f320714..ee3e4b4 100644
--- a/app/dashboard/src/pages/users/dialog/services-select-list.tsx
+++ b/app/dashboard/src/pages/users/dialog/services-select-list.tsx
@@ -9,7 +9,7 @@ import { forwardRef, ReactNode } from 'react';
import {
useFormContext,
} from 'react-hook-form';
-import { Service } from 'types/Service';
+import { Service } from 'types';
interface ServicesSelectListProps {
list: Service[];
diff --git a/app/dashboard/src/pages/users/filters.tsx b/app/dashboard/src/pages/users/filters.tsx
index eb8c0f5..4cd071f 100644
--- a/app/dashboard/src/pages/users/filters.tsx
+++ b/app/dashboard/src/pages/users/filters.tsx
@@ -18,8 +18,7 @@ import {
XMarkIcon,
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useUsers } from 'contexts/UsersContext';
+import { useDashboard, useUsers } from 'stores';
import debounce from 'lodash.debounce';
import React, { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/app/dashboard/src/pages/users/index.tsx b/app/dashboard/src/pages/users/index.tsx
index 6fa5d9a..e906cc8 100644
--- a/app/dashboard/src/pages/users/index.tsx
+++ b/app/dashboard/src/pages/users/index.tsx
@@ -1,6 +1,6 @@
import { UserDialog } from './dialog';
import { UsersTable } from './table';
-import { useDashboard } from 'contexts/DashboardContext';
+import { useUsers, useDashboard } from 'stores';
import { useEffect } from 'react';
import { UsersFilters } from './filters';
import {
@@ -11,7 +11,6 @@ import {
RevokeSubscriptionModal
} from './modals';
import { pages } from 'constants/Pages';
-import { useUsers } from 'contexts/UsersContext';
import Page from 'components/page';
diff --git a/app/dashboard/src/pages/users/modals/delete-modal.tsx b/app/dashboard/src/pages/users/modals/delete-modal.tsx
index 2d8e418..69b11f4 100644
--- a/app/dashboard/src/pages/users/modals/delete-modal.tsx
+++ b/app/dashboard/src/pages/users/modals/delete-modal.tsx
@@ -3,7 +3,7 @@ import {
} from '@chakra-ui/react';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { useUsers } from 'contexts/UsersContext';
+import { useUsers } from 'stores';
import { DeleteModal } from 'components/delete-modal';
type DeleteUserModalProps = {
diff --git a/app/dashboard/src/pages/users/modals/qrcode-modals.tsx b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx
index ce43006..f1ee343 100644
--- a/app/dashboard/src/pages/users/modals/qrcode-modals.tsx
+++ b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx
@@ -24,7 +24,7 @@ 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 'contexts/UsersContext';
+import { useUsers } from 'stores';
const QRCode = chakra(QRCodeCanvas);
const NextIcon = chakra(ChevronRightIcon, {
diff --git a/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
index f9b177c..ef4159d 100644
--- a/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
+++ b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
@@ -16,7 +16,7 @@ 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 'contexts/UsersContext';
+import { useUsers } from 'stores';
export const ResetIcon = chakra(DocumentMinusIcon, {
baseStyle: {
diff --git a/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
index 5191f04..b043c23 100644
--- a/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
+++ b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
@@ -16,7 +16,7 @@ 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 'contexts/UsersContext';
+import { useUsers } from 'stores';
const ResetIcon = chakra(ArrowPathIcon, {
baseStyle: {
diff --git a/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
index 4c953b5..cff25ab 100644
--- a/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
+++ b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
@@ -13,7 +13,7 @@ import {
useToast,
} from '@chakra-ui/react';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { useUsers } from 'contexts/UsersContext';
+import { useUsers } from 'stores';
import { FC, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Icon } from 'components/icon';
diff --git a/app/dashboard/src/pages/users/table/index.tsx b/app/dashboard/src/pages/users/table/index.tsx
index db0e18b..f32fd3b 100644
--- a/app/dashboard/src/pages/users/table/index.tsx
+++ b/app/dashboard/src/pages/users/table/index.tsx
@@ -26,24 +26,24 @@ import { t } from 'i18next';
import { FC, Fragment, useEffect, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import { useTranslation } from 'react-i18next';
-import { User } from 'types/User';
+import { User } from 'types';
import { OnlineBadge } from 'components/OnlineBadge';
import { OnlineStatus } from 'components/OnlineStatus';
-import { Pagination } from 'components/Table/Pagination';
import { StatusBadge } from 'components/StatusBadge';
-import { EmptySection } from 'components/Table/EmptySection';
import {
+ Pagination,
+ EmptySection,
AccordionArrowIcon,
EditIcon,
CopiedIcon,
CopyIcon,
+ StatusSortSelect,
+ Sort,
QRIcon,
SubscriptionLinkIcon
-} from 'components/Table/Icons';
-import { Sort } from 'components/Table/Sort';
+} from 'components/table';
import { UsageSlider, UsageSliderCompact } from './usage-slider';
-import { StatusSortSelect } from 'components/Table/StatusSortSelect';
-import { useUsers } from 'contexts/UsersContext';
+import { useUsers } from 'stores';
type ExpandedIndex = number | number[];
diff --git a/app/dashboard/src/contexts/AdminContext.ts b/app/dashboard/src/stores/admin.store.ts
similarity index 100%
rename from app/dashboard/src/contexts/AdminContext.ts
rename to app/dashboard/src/stores/admin.store.ts
diff --git a/app/dashboard/src/contexts/CoreSettingsContext.ts b/app/dashboard/src/stores/core-settings.store.ts
similarity index 100%
rename from app/dashboard/src/contexts/CoreSettingsContext.ts
rename to app/dashboard/src/stores/core-settings.store.ts
diff --git a/app/dashboard/src/contexts/DashboardContext.ts b/app/dashboard/src/stores/dashboard.store.ts
similarity index 100%
rename from app/dashboard/src/contexts/DashboardContext.ts
rename to app/dashboard/src/stores/dashboard.store.ts
diff --git a/app/dashboard/src/stores/inbounds.store.ts b/app/dashboard/src/stores/inbounds.store.ts
new file mode 100644
index 0000000..738f191
--- /dev/null
+++ b/app/dashboard/src/stores/inbounds.store.ts
@@ -0,0 +1,139 @@
+import { create } from 'zustand';
+import { subscribeWithSelector } from 'zustand/middleware';
+import { useDashboard } from './dashboard.store';
+import { fetch } from 'service/http';
+import { Inbounds, InboundType } from 'types/inbounds';
+import { z } from 'zod';
+
+const isPortThenValue = (value: number) => (value <= 65535 && value !== 0) ? value : null;
+
+export const hostSchema = z.object({
+ remark: z.string().min(1, 'Remark is required'),
+ address: z.string().min(1, 'Address is required'),
+ port: z
+ .string()
+ .or(z.number())
+ .nullable()
+ .transform((value) => {
+ if (typeof value === 'number') return isPortThenValue(value);
+ if (value !== null && !isNaN(parseInt(value))) return isPortThenValue(Number(parseInt(value)));
+ return null;
+ }),
+ path: z.string(),
+ sni: z.string(),
+ host: z.string(),
+ security: z.enum(['inbound_default', 'none', 'tls']),
+ alpn: z.string().optional(),
+ fingerprint: z.string().optional(),
+});
+
+export type HostSchema = z.infer;
+export type HostType = HostSchema & { id?: number };
+export type Hosts = HostType[];
+
+export const fetchInbounds = async () => {
+ useDashboard.setState({ loading: true });
+ return fetch('/inbounds')
+ .then((inbounds: Inbounds) => {
+ useInbounds.setState({ inbounds });
+ })
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+export const fetchInboundHosts = async (id: string): Promise => {
+ useDashboard.setState({ loading: true });
+ return fetch(`/inbounds/${id}/hosts`)
+ .then((hosts: Hosts) => hosts)
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+};
+
+type InboundsStateType = {
+ // Loading
+ loading: boolean;
+ setLoading: (value: boolean) => void;
+ // Inbounds
+ inbounds: Inbounds;
+ refetchInbounds: () => void;
+ selectedInbound: InboundType | null;
+ selectInbound: (inbound: InboundType) => void;
+ // Hosts
+ selectedHost: HostType | null;
+ selectHost: (host: HostType) => void;
+ isEditingHost: boolean;
+ onEditingHost: (isEditingHost: boolean, host: HostType | null) => void;
+ editHost: (inboundId: number | undefined, host: HostType) => Promise;
+ isCreatingHost: boolean;
+ onCreatingHost: (isCreatingHost: boolean) => void;
+ createHost: (inboundId: number | undefined, host: HostSchema) => Promise;
+ isDeletingHost: boolean;
+ onDeletingHost: (isDeletingHost: boolean, host: HostType) => void;
+ deleteHost: (hostId: number) => void;
+};
+
+export const useInbounds = create(
+ subscribeWithSelector((set,) => ({
+ inbounds: [],
+ selectedHost: null,
+ selectedInbound: null,
+ loading: false,
+ isDeletingHost: false,
+ isCreatingHost: false,
+ isEditingHost: false,
+ setLoading: (value) => {
+ set({ loading: value })
+ },
+ refetchInbounds: () => {
+ fetchInbounds();
+ },
+ selectInbound: (host): void => {
+ set({ selectedInbound: host })
+ },
+ selectHost: (host: HostType): void => {
+ set({ selectedHost: host })
+ },
+ onEditingHost: (isEditingHost: boolean, host: HostType | null) => {
+ set({ isEditingHost, selectedHost: host });
+ },
+ editHost: async (hostId: number | undefined, body: HostSchema): Promise => {
+ if (hostId !== undefined) {
+ useDashboard.setState({ loading: true });
+ return fetch(`/inbounds/hosts/${hostId}`, { method: 'PUT', body })
+ .then(() => true)
+ .catch(() => false)
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+ }
+ },
+ onCreatingHost: (isCreatingHost: boolean) => {
+ set({ isCreatingHost });
+ },
+ createHost: async (inboundId: number | undefined, body: HostSchema): Promise => {
+ if (inboundId !== undefined) {
+ useDashboard.setState({ loading: true });
+ return fetch(`/inbounds/${inboundId}/hosts/${body}`, { method: 'POST', body })
+ .then(() => true)
+ .catch(() => false)
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+ }
+ },
+ onDeletingHost: (isDeletingHost: boolean, host: HostType | null) => {
+ set({ isDeletingHost, selectedHost: host });
+ },
+ deleteHost: async (hostId: number): Promise => {
+ useDashboard.setState({ loading: true });
+ return fetch(`/inbounds/hosts/${hostId}`, { method: 'DELETE' })
+ .then(() => true)
+ .catch(() => false)
+ .finally(() => {
+ useDashboard.setState({ loading: false });
+ });
+ },
+ }))
+);
diff --git a/app/dashboard/src/stores/index.ts b/app/dashboard/src/stores/index.ts
new file mode 100644
index 0000000..afc1b15
--- /dev/null
+++ b/app/dashboard/src/stores/index.ts
@@ -0,0 +1,7 @@
+export * from './admin.store';
+export * from './inbounds.store';
+export * from './users.store';
+export * from './services.store';
+export * from './core-settings.store';
+export * from './dashboard.store';
+export * from './nodes.store';
diff --git a/app/dashboard/src/contexts/NodesContext.ts b/app/dashboard/src/stores/nodes.store.ts
similarity index 97%
rename from app/dashboard/src/contexts/NodesContext.ts
rename to app/dashboard/src/stores/nodes.store.ts
index 7f0ba53..9d39167 100644
--- a/app/dashboard/src/contexts/NodesContext.ts
+++ b/app/dashboard/src/stores/nodes.store.ts
@@ -2,9 +2,9 @@ import { useQuery } from 'react-query';
import { fetch } from 'service/http';
import { create } from 'zustand';
import { z } from 'zod';
-import { FilterUsageType, NodesFilterType } from 'types/Filter';
+import { FilterUsageType, NodesFilterType } from 'types/filter';
import { getNodesPerPageLimitSize } from 'utils/userPreferenceStorage';
-import { useDashboard } from './DashboardContext';
+import { useDashboard } from './dashboard.store';
export const NodeSchema = z.object({
name: z.string().min(1),
diff --git a/app/dashboard/src/contexts/ServicesContext.ts b/app/dashboard/src/stores/services.store.ts
similarity index 93%
rename from app/dashboard/src/contexts/ServicesContext.ts
rename to app/dashboard/src/stores/services.store.ts
index 1bf45ba..cea9c9a 100644
--- a/app/dashboard/src/contexts/ServicesContext.ts
+++ b/app/dashboard/src/stores/services.store.ts
@@ -1,13 +1,12 @@
import { queryClient } from 'service/react-query';
import { StatisticsQueryKey } from 'components/statistics-card';
-import { Service, ServiceCreate } from 'types/Service';
+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';
+import { ServicesFilterType } from 'types/filter';
+import { useDashboard, useUsers } from 'stores';
type ServicesStateType = {
services: Service[],
diff --git a/app/dashboard/src/contexts/UsersContext.ts b/app/dashboard/src/stores/users.store.ts
similarity index 95%
rename from app/dashboard/src/contexts/UsersContext.ts
rename to app/dashboard/src/stores/users.store.ts
index 4bd43d0..279852e 100644
--- a/app/dashboard/src/contexts/UsersContext.ts
+++ b/app/dashboard/src/stores/users.store.ts
@@ -1,13 +1,12 @@
-import { User, UserCreate } from 'types/User';
+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 { useServices, useDashboard } from 'stores';
import { StatisticsQueryKey } from 'components/statistics-card';
import { queryClient } from 'service/react-query';
-import { useServices } from './ServicesContext';
-import { UsersFilterType, FilterUsageType } from 'types/Filter';
+import { UsersFilterType, FilterUsageType } from 'types/filter';
type UsersStateType = {
isCreatingNewUser: boolean;
diff --git a/app/dashboard/src/types/ChakraIcon.ts b/app/dashboard/src/types/chakra-icon.ts
similarity index 100%
rename from app/dashboard/src/types/ChakraIcon.ts
rename to app/dashboard/src/types/chakra-icon.ts
diff --git a/app/dashboard/src/types/Filter.ts b/app/dashboard/src/types/filter.ts
similarity index 100%
rename from app/dashboard/src/types/Filter.ts
rename to app/dashboard/src/types/filter.ts
diff --git a/app/dashboard/src/types/Inbounds.ts b/app/dashboard/src/types/inbounds.ts
similarity index 81%
rename from app/dashboard/src/types/Inbounds.ts
rename to app/dashboard/src/types/inbounds.ts
index fa39bf1..4e3caae 100644
--- a/app/dashboard/src/types/Inbounds.ts
+++ b/app/dashboard/src/types/inbounds.ts
@@ -1,4 +1,4 @@
-import { ProtocolType } from './Protocol';
+import { ProtocolType } from './protocol';
export type InboundType = {
id: number;
diff --git a/app/dashboard/src/types/index.ts b/app/dashboard/src/types/index.ts
new file mode 100644
index 0000000..0a04667
--- /dev/null
+++ b/app/dashboard/src/types/index.ts
@@ -0,0 +1,8 @@
+export * from './inbounds';
+export * from './user';
+export * from './service';
+export * from './page';
+export * from './protocol';
+export * from './filter';
+export * from './proxy-hosts';
+export * from './chakra-icon';
diff --git a/app/dashboard/src/types/Page.ts b/app/dashboard/src/types/page.ts
similarity index 66%
rename from app/dashboard/src/types/Page.ts
rename to app/dashboard/src/types/page.ts
index 608b210..4b35bf3 100644
--- a/app/dashboard/src/types/Page.ts
+++ b/app/dashboard/src/types/page.ts
@@ -1,4 +1,4 @@
-import { ChakraIcon } from 'types/ChakraIcon'
+import { ChakraIcon } from 'types'
export interface PageType {
path: string;
diff --git a/app/dashboard/src/types/Protocol.ts b/app/dashboard/src/types/protocol.ts
similarity index 100%
rename from app/dashboard/src/types/Protocol.ts
rename to app/dashboard/src/types/protocol.ts
diff --git a/app/dashboard/src/types/ProxyHosts.ts b/app/dashboard/src/types/proxy-hosts.ts
similarity index 100%
rename from app/dashboard/src/types/ProxyHosts.ts
rename to app/dashboard/src/types/proxy-hosts.ts
diff --git a/app/dashboard/src/types/Service.ts b/app/dashboard/src/types/service.ts
similarity index 100%
rename from app/dashboard/src/types/Service.ts
rename to app/dashboard/src/types/service.ts
diff --git a/app/dashboard/src/types/User.ts b/app/dashboard/src/types/user.ts
similarity index 100%
rename from app/dashboard/src/types/User.ts
rename to app/dashboard/src/types/user.ts