gitscope
← marzneshin / Diff · bedeeff → bedeeff
Changes bedeeff..bedeeff
Patch commit
diff --git a/app/dashboard/package-lock.json b/app/dashboard/package-lock.json
index 78e7874..76d77e3 100644
--- a/app/dashboard/package-lock.json
+++ b/app/dashboard/package-lock.json
@@ -40,6 +40,7 @@
"react-json-editor-ajrm": "^2.5.14",
"react-loading-skeleton": "^3.1.0",
"react-query": "^3.39.3",
+ "react-resizable-panels": "^2.0.9",
"react-router-dom": "^6.4.3",
"react-slick": "^0.29.0",
"react-use-websocket": "^4.3.1",
@@ -8431,6 +8432,15 @@
}
}
},
+ "node_modules/react-resizable-panels": {
+ "version": "2.0.9",
+ "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.0.9.tgz",
+ "integrity": "sha512-ZylBvs7oG7Y/INWw3oYGolqgpFvoPW8MPeg9l1fURDeKpxrmUuCHBUmPj47BdZ11MODImu3kZYXG85rbySab7w==",
+ "peerDependencies": {
+ "react": "^16.14.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-router": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz",
diff --git a/app/dashboard/package.json b/app/dashboard/package.json
index 4d989ce..da5fe22 100644
--- a/app/dashboard/package.json
+++ b/app/dashboard/package.json
@@ -43,6 +43,7 @@
"react-json-editor-ajrm": "^2.5.14",
"react-loading-skeleton": "^3.1.0",
"react-query": "^3.39.3",
+ "react-resizable-panels": "^2.0.9",
"react-router-dom": "^6.4.3",
"react-slick": "^0.29.0",
"react-use-websocket": "^4.3.1",
diff --git a/app/dashboard/public/locales/en.json b/app/dashboard/public/locales/en.json
index b73a0b4..766c039 100644
--- a/app/dashboard/public/locales/en.json
+++ b/app/dashboard/public/locales/en.json
@@ -17,6 +17,12 @@
"services": "Services",
"inbound": "Inbound",
"inbounds": "Inbounds",
+ "sidebar": {
+ "groups": {
+ "management": "Management",
+ "settings": "Settings"
+ }
+ },
"login": {
"login": "Login",
"loginYourAccount": "Login to your account",
diff --git a/app/dashboard/src/App.tsx b/app/dashboard/src/App.tsx
index 7dfc343..1fbd734 100644
--- a/app/dashboard/src/App.tsx
+++ b/app/dashboard/src/App.tsx
@@ -1,53 +1,15 @@
-import { Grid, GridItem } from '@chakra-ui/react';
-import { Footer } from 'components/Footer';
-import { Header } from 'components/Header';
-import { pages, PageRoute } from 'constants/Pages';
-import { Sidebar } from 'components/Sidebar';
-import Login from 'pages/Login';
import 'react-datepicker/dist/react-datepicker.css';
import 'react-loading-skeleton/dist/skeleton.css';
-import { Navigate, Route, Routes } from 'react-router-dom';
-import { fetchAdminLoader } from 'components/modules/Router';
-import { useAdmin } from 'contexts/AdminContext';
+import { RouterProvider } from 'react-router-dom';
+import { FC, Suspense } from 'react';
+import { router } from 'pages/routes';
-function App() {
- const { isLoggedIn } = useAdmin();
+const App: FC = () => {
return (
<main>
- <Routes>
- <Route
- path="/login"
- element={<Login />}
- />
- </Routes>
-
- <Grid
- h="100%"
- templateAreas={`"nav header"
- "nav main"
- "nav footer"`}
- gridTemplateRows={'20px 1fr 20px'}
- gridTemplateColumns={'15rem 1fr'}
- >
- <GridItem area={'header'} my="12px" mx="30px">
- <Header />
- </GridItem>
- <GridItem area={'nav'}>
- <Sidebar />
- </GridItem>
- <GridItem area={'main'}>
- <Routes>
- {pages.map((route: PageRoute, i) =>
- <Route {...route} errorElement={<Login />} loader={fetchAdminLoader} key={i} />
-
- )}
- </Routes>
- </GridItem>
- <GridItem area={'footer'}>
- <Footer />
- </GridItem>
- </Grid>
- {!isLoggedIn && <Navigate to="/login" />}
+ <Suspense>
+ <RouterProvider router={router} />
+ </Suspense>
</main>
);
}
diff --git a/app/dashboard/src/components/DeleteServiceModal.tsx b/app/dashboard/src/components/DeleteServiceModal.tsx
deleted file mode 100644
index 3932e15..0000000
--- a/app/dashboard/src/components/DeleteServiceModal.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import {
- Button,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Spinner,
- Text,
- useToast,
-} from '@chakra-ui/react';
-import { useServices } from 'contexts/ServicesContext';
-import { FC, useState } from 'react';
-import { Trans, useTranslation } from 'react-i18next';
-import { DeleteIcon } from './Dialog/Icons';
-import { Icon } from './Icon';
-
-export const DeleteServiceModal: FC = () => {
- const { deleteService, refetchServices, deletingService, onDeletingService } = useServices();
- const { t } = useTranslation();
- const toast = useToast();
- const [loading, setLoading] = useState(false);
- const onClose = () => {
- onDeletingService(null);
- };
-
- const onDelete = () => {
- if (deletingService) {
- setLoading(true);
- deleteService(deletingService)
- .then(() => {
- refetchServices();
- toast({
- title: t('deleteService.deleteSuccess', { name: deletingService.name }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .then(onClose)
- .finally(setLoading.bind(null, false));
- }
- };
-
- return (
- <Modal isCentered isOpen={!!deletingService} onClose={onClose} size="sm">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3">
- <ModalHeader pt={6}>
- <Icon color="red">
- <DeleteIcon />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <ModalBody>
- <Text fontWeight="semibold" fontSize="lg">
- {t('deleteService.title')}
- </Text>
- {deletingService && (
- <Text
- mt={1}
- fontSize="sm"
- _dark={{ color: 'gray.400' }}
- color="gray.600"
- >
- <Trans components={{ b: <b /> }}>
- {t('deleteService.prompt', { name: deletingService.name })}
- </Trans>
- </Text>
- )}
- </ModalBody>
- <ModalFooter display="flex">
- <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
- {t('cancel')}
- </Button>
- <Button
- size="sm"
- w="full"
- colorScheme="red"
- onClick={() => onDelete()}
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- >
- {t('delete')}
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/components/DeleteUserModal.tsx b/app/dashboard/src/components/DeleteUserModal.tsx
deleted file mode 100644
index 12e6014..0000000
--- a/app/dashboard/src/components/DeleteUserModal.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { DeleteIcon } from 'components/Dialog/Icons';
-import {
- Button,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Spinner,
- Text,
- useToast,
-} from '@chakra-ui/react';
-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;
-};
-
-export const DeleteUserModal: FC<DeleteUserModalProps> = () => {
- const [loading, setLoading] = useState(false);
- const { deletingUser: user, onDeletingUser, deleteUser } = useUsers();
- const { t } = useTranslation();
- const toast = useToast();
- const onClose = () => {
- onDeletingUser(null);
- };
- const onDelete = () => {
- if (user) {
- setLoading(true);
- deleteUser(user)
- .then(() => {
- toast({
- title: t('deleteUser.deleteSuccess', { username: user.username }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .then(onClose)
- .finally(setLoading.bind(null, false));
- }
- };
- return (
- <Modal isCentered isOpen={!!user} onClose={onClose} size="sm">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3">
- <ModalHeader pt={6}>
- <Icon color="red">
- <DeleteIcon />
- </Icon>
- </ModalHeader>
- <ModalCloseButton mt={3} />
- <ModalBody>
- <Text fontWeight="semibold" fontSize="lg">
- {t('deleteUser.title')}
- </Text>
- {user && (
- <Text
- mt={1}
- fontSize="sm"
- _dark={{ color: 'gray.400' }}
- color="gray.600"
- >
- <Trans components={{ b: <b /> }}>
- {t('deleteUser.prompt', { username: user.username })}
- </Trans>
- </Text>
- )}
- </ModalBody>
- <ModalFooter display="flex">
- <Button size="sm" onClick={onClose} mr={3} w="full" variant="outline">
- {t('cancel')}
- </Button>
- <Button
- size="sm"
- w="full"
- colorScheme="red"
- onClick={onDelete}
- leftIcon={loading ? <Spinner size="xs" /> : undefined}
- >
- {t('delete')}
- </Button>
- </ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/components/Footer.tsx b/app/dashboard/src/components/Footer.tsx
index 6ee4317..3dcd9fd 100644
--- a/app/dashboard/src/components/Footer.tsx
+++ b/app/dashboard/src/components/Footer.tsx
@@ -20,7 +20,7 @@ export const Footer: FC<BoxProps> = (props) => {
{version ? ` (v${version}), ` : ', '}
Made in{' '}
<Link color="blue.400" href={ORGANIZATION_URL}>
- khodedawosh
+ khodedawsh
</Link>
</Text>
</HStack>
diff --git a/app/dashboard/src/components/NodesUsage.tsx b/app/dashboard/src/components/NodesUsage.tsx
deleted file mode 100644
index c4fa983..0000000
--- a/app/dashboard/src/components/NodesUsage.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import {
- Box,
- CircularProgress,
- HStack,
- Modal,
- ModalBody,
- ModalCloseButton,
- ModalContent,
- ModalFooter,
- ModalHeader,
- ModalOverlay,
- Text,
- VStack,
- chakra,
- useColorMode,
-} from '@chakra-ui/react';
-import { ChartPieIcon } from '@heroicons/react/24/outline';
-import { FilterUsageType } from 'types/Filter';
-import { useNodes } from 'contexts/NodesContext';
-import dayjs from 'dayjs';
-import { FC, Suspense, useEffect, useState } from 'react';
-import ReactApexChart from 'react-apexcharts';
-import { useTranslation } from 'react-i18next';
-import { Icon } from './Icon';
-import { UsageFilter, createUsageConfig } from './UsageFilter';
-
-const UsageIcon = chakra(ChartPieIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-export type NodesUsageProps = {};
-
-export const NodesUsage: FC<NodesUsageProps> = () => {
- const { isShowingNodesUsage, onShowingNodesUsage } = useNodes();
- const { fetchNodesUsage } = useNodes();
- const { t } = useTranslation();
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [loading, _] = useState(false);
- const { colorMode } = useColorMode();
-
- const usageTitle = t('userDialog.total');
- const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
- const [usageFilter, setUsageFilter] = useState('1m');
- const fetchUsageWithFilter = (query: FilterUsageType) => {
- fetchNodesUsage(query).then((data: any) => {
- const labels = [];
- const series = [];
- for (const key in data.usages) {
- const entry = data.usages[key];
- series.push(entry.uplink + entry.downlink);
- labels.push(entry.node_name);
- }
- setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
- });
- };
-
- useEffect(() => {
- if (isShowingNodesUsage) {
- fetchUsageWithFilter({
- start: dayjs().utc().subtract(30, 'day').format('YYYY-MM-DDTHH:00:00'),
- });
- }
- }, [isShowingNodesUsage]);
-
- const onClose = () => {
- onShowingNodesUsage(false);
- setUsageFilter('1m');
- };
-
- const disabled = loading;
-
- return (
- <Modal isOpen={isShowingNodesUsage} onClose={onClose} size="2xl">
- <ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
- <ModalContent mx="3" w="full">
- <ModalHeader pt={6}>
- <HStack gap={2}>
- <Icon color="primary">
- <UsageIcon color="white" />
- </Icon>
- <Text fontWeight="semibold" fontSize="lg">
- {t('header.nodesUsage')}
- </Text>
- </HStack>
- </ModalHeader>
- <ModalCloseButton mt={3} disabled={disabled} />
- <ModalBody>
- <VStack gap={4}>
- <UsageFilter
- defaultValue={usageFilter}
- onChange={(filter, query) => {
- setUsageFilter(filter);
- fetchUsageWithFilter(query);
- }}
- />
- <Box justifySelf="center" w="full" maxW="300px" mt="4">
- <Suspense fallback={<CircularProgress isIndeterminate />}>
- <ReactApexChart
- options={usage.options}
- series={usage.series}
- type="donut"
- height="500px"
- />
- </Suspense>
- </Box>
- </VStack>
- </ModalBody>
- <ModalFooter mt="3"></ModalFooter>
- </ModalContent>
- </Modal>
- );
-};
diff --git a/app/dashboard/src/components/Sidebar/index.tsx b/app/dashboard/src/components/Sidebar/index.tsx
deleted file mode 100644
index a3de888..0000000
--- a/app/dashboard/src/components/Sidebar/index.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { HamburgerIcon } from '@chakra-ui/icons';
-import { chakra, Image, Flex, VStack, Text } from '@chakra-ui/react'
-import { useDashboard } from 'contexts/DashboardContext';
-import { Link as ReactRouterLink } from 'react-router-dom'
-import { Link as ChakraLink } from '@chakra-ui/react'
-import { pages } from 'constants/Pages';
-import { PageType } from 'types/Page';
-import { Icon } from 'components/Icon';
-import { FC, useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
-
-export type SideBarItemProps = Pick<
- PageType,
- | 'name'
- | 'path'
- | 'itemIcon'
->
-
-export const SidebarMenuIcon = chakra(HamburgerIcon, {
- baseStyle: {
- w: 5,
- h: 5,
- },
-});
-
-export const SidebarItem: FC<SideBarItemProps & { index: number }> = ({ itemIcon: ItemIcon, name, path, index, }) => {
- const { t } = useTranslation();
- const { activePage, activatePage } = useDashboard();
- const [active, activate] = useState(index === activePage)
-
- useEffect(() => { activate(index === activePage) }, [active, activePage]);
-
- const darkStyle = { bg: active ? 'primary.900' : 'transparent' };
- const hoverStyle = {
- borderRadius: 10,
- bg: active ? undefined : 'gray.200',
- _dark: {
- bg: 'gray.700'
- },
- transition: 'background-color 0.9s',
- };
-
- return (
- <ChakraLink
- as={ReactRouterLink}
- borderRadius={10}
- bg={active ? 'primary.500' : 'transparent'}
- border="4px"
- ps="4px"
- borderColor="transparent"
- _dark={darkStyle}
- to={path}
- w="13rem"
- _hover={hoverStyle}
- onClick={() => {
- activatePage(index);
- }}
- >
- <Flex flexDir="row" alignItems="center" >
- <Icon color="">
- <ItemIcon color={active ? 'gray.100' : undefined} _dark={{ color: active ? 'gray.100' : undefined }} />
- </Icon>
- <Text color={active ? 'gray.100' : 'gray.700'} _dark={{ color: 'gray.100' }} fontSize="xl" >
- {t(name)}
- </Text>
- </Flex>
- </ChakraLink>
- );
-}
-
-export const Sidebar = () => {
- return (
- <aside>
- <Flex
- pos="sticky"
- left="5"
- borderRightWidth="4px"
- borderRightColor='gray.200'
- borderRightStyle="solid"
- flexDir="column"
- h="100vh"
- bg="gray.50"
- _dark={{ bg: 'gray.800', borderRightColor: 'gray.700' }}
- >
- <VStack pt="30px" gap="2">
- <Image src="favicon/logo.svg" alt="marzneshin" boxSize="50px" />
- {pages.map((item: SideBarItemProps, index: number) =>
- <SidebarItem path={item.path} name={item.name} itemIcon={item.itemIcon} index={index} key={index} />)}
- </VStack>
- </Flex>
- </aside>
- )
-}
diff --git a/app/dashboard/src/components/DeleteNodeModal.tsx b/app/dashboard/src/components/delete-modal/index.tsx
similarity index 55%
rename from app/dashboard/src/components/DeleteNodeModal.tsx
rename to app/dashboard/src/components/delete-modal/index.tsx
index 566c8bd..449d3a8 100644
--- a/app/dashboard/src/components/DeleteNodeModal.tsx
+++ b/app/dashboard/src/components/delete-modal/index.tsx
@@ -1,3 +1,5 @@
+
+
import {
Button,
Modal,
@@ -9,44 +11,35 @@ import {
ModalOverlay,
Spinner,
Text,
- useToast,
} from '@chakra-ui/react';
-import { useNodes } from 'contexts/NodesContext';
-import { FC, useState } from 'react';
+import { FC } from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import { DeleteIcon } from './Dialog/Icons';
-import { Icon } from './Icon';
+import { DeleteIcon } from 'components/dialog/Icons';
+import { Icon } from 'components/icon';
-export const DeleteNodeModal: FC = () => {
- const { deleteNode, refetchNodes, deletingNode, onDeletingNode } = useNodes();
- const { t } = useTranslation();
- const toast = useToast();
- const [loading, setLoading] = useState(false);
- const onClose = () => {
- onDeletingNode(undefined);
- };
+type DeleteModalProps = {
+ onDelete: () => void,
+ deleteObjectPromptT: string,
+ deleteObjectTitleT: string,
+ deletingObjectName?: string,
+ isOpen: boolean,
+ onClose: () => void,
+ loading: boolean,
+}
- const onDelete = () => {
- if (deletingNode) {
- setLoading(true);
- deleteNode(deletingNode)
- .then(() => {
- refetchNodes();
- toast({
- title: t('deleteNode.deleteSuccess', { name: deletingNode.name }),
- status: 'success',
- isClosable: true,
- position: 'top',
- duration: 3000,
- });
- })
- .then(onClose)
- .finally(setLoading.bind(null, false));
- }
- };
+export const DeleteModal: FC<DeleteModalProps> = ({
+ onDelete,
+ isOpen,
+ onClose,
+ deletingObjectName,
+ loading,
+ deleteObjectTitleT,
+ deleteObjectPromptT
+}) => {
+ const { t } = useTranslation();
return (
- <Modal isCentered isOpen={!!deletingNode} onClose={onClose} size="sm">
+ <Modal isCentered isOpen={isOpen} onClose={onClose} size="sm">
<ModalOverlay bg="blackAlpha.300" backdropFilter="blur(10px)" />
<ModalContent mx="3">
<ModalHeader pt={6}>
@@ -57,9 +50,9 @@ export const DeleteNodeModal: FC = () => {
<ModalCloseButton mt={3} />
<ModalBody>
<Text fontWeight="semibold" fontSize="lg">
- {t('deleteNode.title')}
+ {t(deleteObjectTitleT)}
</Text>
- {deletingNode && (
+ {deletingObjectName && (
<Text
mt={1}
fontSize="sm"
@@ -67,7 +60,7 @@ export const DeleteNodeModal: FC = () => {
color="gray.600"
>
<Trans components={{ b: <b /> }}>
- {t('deleteNode.prompt', { name: deletingNode.name })}
+ {t(deleteObjectPromptT, { name: deletingObjectName })}
</Trans>
</Text>
)}
diff --git a/app/dashboard/src/components/Dialog/Icons.tsx b/app/dashboard/src/components/dialog/Icons.tsx
similarity index 100%
rename from app/dashboard/src/components/Dialog/Icons.tsx
rename to app/dashboard/src/components/dialog/Icons.tsx
diff --git a/app/dashboard/src/components/Dialog/ModalFooter.tsx b/app/dashboard/src/components/dialog/ModalFooter.tsx
similarity index 100%
rename from app/dashboard/src/components/Dialog/ModalFooter.tsx
rename to app/dashboard/src/components/dialog/ModalFooter.tsx
diff --git a/app/dashboard/src/components/Dialog/ModalHeader.tsx b/app/dashboard/src/components/dialog/ModalHeader.tsx
similarity index 94%
rename from app/dashboard/src/components/Dialog/ModalHeader.tsx
rename to app/dashboard/src/components/dialog/ModalHeader.tsx
index 683bef6..1ee02c5 100644
--- a/app/dashboard/src/components/Dialog/ModalHeader.tsx
+++ b/app/dashboard/src/components/dialog/ModalHeader.tsx
@@ -1,7 +1,7 @@
import { Text, HStack, ModalHeader } from '@chakra-ui/react';
import { ChakraIcon } from 'types/ChakraIcon';
-import { Icon } from '../Icon';
+import { Icon } from '../icon';
export const DialogModalHeader = ({ HeaderIcon, title }: { HeaderIcon: ChakraIcon, title: string }) => {
return (
diff --git a/app/dashboard/src/components/Header.tsx b/app/dashboard/src/components/header/index.tsx
similarity index 70%
rename from app/dashboard/src/components/Header.tsx
rename to app/dashboard/src/components/header/index.tsx
index e9364d0..4f09473 100644
--- a/app/dashboard/src/components/Header.tsx
+++ b/app/dashboard/src/components/header/index.tsx
@@ -13,28 +13,21 @@ import {
import {
ArrowLeftStartOnRectangleIcon,
Bars3Icon,
- ChartPieIcon,
Cog6ToothIcon,
CurrencyDollarIcon,
- DocumentMinusIcon,
- LinkIcon,
- MoonIcon,
- SunIcon,
} from '@heroicons/react/24/outline';
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';
import GitHubButton from 'react-github-btn';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
-import { updateThemeColor } from 'utils/themeColor';
-import { Language } from './Language';
+import { Language } from '../Language';
+import { ThemeToggle } from '../theme-toggler';
type HeaderProps = {
actions?: ReactNode;
@@ -46,15 +39,10 @@ const iconProps = {
},
};
-const DarkIcon = chakra(MoonIcon, iconProps);
-const LightIcon = chakra(SunIcon, iconProps);
const CoreSettingsIcon = chakra(Cog6ToothIcon, iconProps);
const SettingsIcon = chakra(Bars3Icon, iconProps);
const LogoutIcon = chakra(ArrowLeftStartOnRectangleIcon, iconProps);
const DonationIcon = chakra(CurrencyDollarIcon, iconProps);
-const HostsIcon = chakra(LinkIcon, iconProps);
-const NodesUsageIcon = chakra(ChartPieIcon, iconProps);
-const ResetUsageIcon = chakra(DocumentMinusIcon, iconProps);
const NotificationCircle = chakra(Box, {
baseStyle: {
bg: 'yellow.500',
@@ -86,17 +74,8 @@ export const Header: FC<HeaderProps> = () => {
const {
activePage,
} = useDashboard();
- const {
- onResetAllUsage,
- } = useUsers();
- const {
- onEditingHosts,
- } = useHosts();
- const {
- onShowingNodesUsage,
- } = useNodes();
const { t } = useTranslation();
- const { colorMode, toggleColorMode } = useColorMode();
+ const { colorMode } = useColorMode();
const [showDonationNotif, setShowDonationNotif] = useState(
shouldShowDonation()
);
@@ -139,30 +118,15 @@ export const Header: FC<HeaderProps> = () => {
position="relative"
></MenuButton>
<MenuList minW="170px" zIndex={99999} className="menuList">
- <MenuItem
- maxW="170px"
- fontSize="sm"
- icon={<HostsIcon />}
- onClick={onEditingHosts.bind(null, true)}
- >
- {t('header.hostSettings')}
- </MenuItem>
- <MenuItem
- maxW="170px"
- fontSize="sm"
- icon={<NodesUsageIcon />}
- onClick={onShowingNodesUsage.bind(null, true)}
- >
- {t('header.nodesUsage')}
- </MenuItem>
- <MenuItem
- maxW="170px"
- fontSize="sm"
- icon={<ResetUsageIcon />}
- onClick={onResetAllUsage.bind(null, true)}
- >
- {t('resetAllUsage')}
- </MenuItem>
+ {/* NOTE: Host settings trigger */}
+ {/* <MenuItem */}
+ {/* maxW="170px" */}
+ {/* fontSize="sm" */}
+ {/* icon={<HostsIcon />} */}
+ {/* onClick={onEditingHosts.bind(null, true)} */}
+ {/* > */}
+ {/* {t('header.hostSettings')} */}
+ {/* </MenuItem> */}
<Link to={DONATION_URL} target="_blank">
<MenuItem
maxW="170px"
@@ -195,21 +159,8 @@ export const Header: FC<HeaderProps> = () => {
>
<CoreSettingsIcon />
</IconButton>
-
<Language />
-
- <IconButton
- size="sm"
- variant="outline"
- aria-label="switch theme"
- onClick={() => {
- updateThemeColor(colorMode == 'dark' ? 'light' : 'dark');
- toggleColorMode();
- }}
- >
- {colorMode === 'light' ? <DarkIcon /> : <LightIcon />}
- </IconButton>
-
+ <ThemeToggle />
<Box
css={{ direction: 'ltr' }}
display="flex"
@@ -226,7 +177,7 @@ export const Header: FC<HeaderProps> = () => {
data-color-scheme={`no-preference: ${gBtnColor}; light: ${gBtnColor}; dark: ${gBtnColor};`}
data-size="large"
data-show-count="true"
- aria-label="Star Marzban on GitHub"
+ aria-label="Star Marzneshin on GitHub"
>
Star
</GitHubButton>
diff --git a/app/dashboard/src/components/Icon.tsx b/app/dashboard/src/components/icon/index.tsx
similarity index 100%
rename from app/dashboard/src/components/Icon.tsx
rename to app/dashboard/src/components/icon/index.tsx
diff --git a/app/dashboard/src/components/JsonEditor/index.tsx b/app/dashboard/src/components/json-editor/index.tsx
similarity index 100%
rename from app/dashboard/src/components/JsonEditor/index.tsx
rename to app/dashboard/src/components/json-editor/index.tsx
diff --git a/app/dashboard/src/components/JsonEditor/styles.css b/app/dashboard/src/components/json-editor/styles.css
similarity index 100%
rename from app/dashboard/src/components/JsonEditor/styles.css
rename to app/dashboard/src/components/json-editor/styles.css
diff --git a/app/dashboard/src/components/JsonEditor/themes.js b/app/dashboard/src/components/json-editor/themes.js
similarity index 100%
rename from app/dashboard/src/components/JsonEditor/themes.js
rename to app/dashboard/src/components/json-editor/themes.js
diff --git a/app/dashboard/src/components/ListCheckBox/index.tsx b/app/dashboard/src/components/list-check-box/index.tsx
similarity index 100%
rename from app/dashboard/src/components/ListCheckBox/index.tsx
rename to app/dashboard/src/components/list-check-box/index.tsx
diff --git a/app/dashboard/src/components/modules/Router.tsx b/app/dashboard/src/components/modules/Router.tsx
deleted file mode 100644
index e80b50f..0000000
--- a/app/dashboard/src/components/modules/Router.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { fetch } from '../../service/http';
-import { getAuthToken } from '../../utils/authStorage';
-
-export const fetchAdminLoader = () => {
- return fetch('/admin', {
- headers: {
- Authorization: `Bearer ${getAuthToken()}`,
- },
- });
-};
diff --git a/app/dashboard/src/components/page/index.tsx b/app/dashboard/src/components/page/index.tsx
new file mode 100644
index 0000000..fb665d9
--- /dev/null
+++ b/app/dashboard/src/components/page/index.tsx
@@ -0,0 +1,14 @@
+import { Box, VStack } from '@chakra-ui/react';
+import { FC, PropsWithChildren } from 'react';
+
+export const Page: FC<PropsWithChildren> = ({ children }) => {
+ return (
+ <VStack justifyContent="space-between" mt={4} rowGap={4}>
+ <Box w="full">
+ {children}
+ </Box>
+ </VStack>
+ );
+};
+
+export default Page;
diff --git a/app/dashboard/src/components/sidebar/index.tsx b/app/dashboard/src/components/sidebar/index.tsx
new file mode 100644
index 0000000..22f0879
--- /dev/null
+++ b/app/dashboard/src/components/sidebar/index.tsx
@@ -0,0 +1,39 @@
+import { HamburgerIcon } from '@chakra-ui/icons';
+import { chakra, Flex } from '@chakra-ui/react'
+import { pages } from 'constants/Pages';
+import { FC } from 'react';
+import { Logo } from './logo';
+import { SidebarGroup } from './sidebar-group';
+import { SidebarItem, SideBarItemProps } from './sidebar-item';
+
+export const SidebarMenuIcon = chakra(HamburgerIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+
+export const Sidebar: FC = () => {
+ return (
+ <aside>
+ <Flex
+ pos="sticky"
+ borderRightWidth="4px"
+ borderRightColor='gray.200'
+ borderRightStyle="solid"
+ flexDir="column"
+ pt={4}
+ h="100vh"
+ bg="gray.50"
+ _dark={{ bg: 'gray.800', borderRightColor: 'gray.700' }}
+ >
+ <Logo />
+ <SidebarGroup titleT={'sidebar.groups.management'}>
+ {pages.map((item: SideBarItemProps, index: number) =>
+ <SidebarItem path={item.path} name={item.name} itemIcon={item.itemIcon} index={index} key={index} />)}
+ </SidebarGroup>
+ </Flex>
+ </aside>
+ )
+}
diff --git a/app/dashboard/src/components/sidebar/logo.tsx b/app/dashboard/src/components/sidebar/logo.tsx
new file mode 100644
index 0000000..cf94fbc
--- /dev/null
+++ b/app/dashboard/src/components/sidebar/logo.tsx
@@ -0,0 +1,7 @@
+import { Image } from '@chakra-ui/react'
+
+export const Logo = () => {
+ return (
+ <Image src="favicon/logo.svg" alt="marzneshin" boxSize="50px" w="full" />
+ )
+}
diff --git a/app/dashboard/src/components/sidebar/sidebar-group.tsx b/app/dashboard/src/components/sidebar/sidebar-group.tsx
new file mode 100644
index 0000000..60500f4
--- /dev/null
+++ b/app/dashboard/src/components/sidebar/sidebar-group.tsx
@@ -0,0 +1,19 @@
+import { Box, Text, VStack } from '@chakra-ui/react'
+import { FC, PropsWithChildren } from 'react'
+import { useTranslation } from 'react-i18next'
+
+type SidebarGroupProps = {
+ titleT: string
+}
+
+export const SidebarGroup: FC<PropsWithChildren<SidebarGroupProps>> = ({ titleT, children }) => {
+ const { t } = useTranslation();
+ return (
+ <Box>
+ <VStack gap="2">
+ <Text w="85%" fontWeight="bold" textTransform="uppercase" fontSize="xs" color="gray.500" >{t(titleT)}</Text>
+ {children}
+ </VStack>
+ </Box>
+ );
+}
diff --git a/app/dashboard/src/components/sidebar/sidebar-item.tsx b/app/dashboard/src/components/sidebar/sidebar-item.tsx
new file mode 100644
index 0000000..5722c8b
--- /dev/null
+++ b/app/dashboard/src/components/sidebar/sidebar-item.tsx
@@ -0,0 +1,64 @@
+
+import { Flex, Text } from '@chakra-ui/react'
+import { useDashboard } from 'contexts/DashboardContext';
+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';
+
+export type SideBarItemProps = Pick<
+ PageType,
+ | 'name'
+ | 'path'
+ | 'itemIcon'
+>
+
+export const SidebarItem: FC<SideBarItemProps & { index: number }> = ({ itemIcon: ItemIcon, name, path, index }) => {
+ const { t } = useTranslation();
+ const { activePage, activatePage, isCollapsed } = useDashboard();
+ const [active, activate] = useState(index === activePage);
+
+ useEffect(() => { activate(index === activePage) }, [active, activePage]);
+
+ const darkStyle = { bg: active ? 'primary.900' : 'transparent' };
+ const hoverStyle = {
+ borderRadius: 10,
+ bg: active ? undefined : 'gray.200',
+ _dark: {
+ bg: 'gray.700'
+ },
+ transition: 'background-color 0.9s',
+ };
+
+ return (
+ <ChakraLink
+ as={ReactRouterLink}
+ borderRadius={10}
+ bg={active ? 'primary.500' : 'transparent'}
+ border="4px"
+ // ps="4px"
+ borderColor="transparent"
+ _dark={darkStyle}
+ to={path}
+ w="85%"
+ _hover={hoverStyle}
+ onClick={() => {
+ activatePage(index);
+ }}
+ >
+ <Flex flexDir="row" alignItems="center" justifyContent={isCollapsed ? 'center' : undefined} >
+ <Icon color="">
+ <ItemIcon color={active ? 'gray.100' : undefined} _dark={{ color: active ? 'gray.100' : undefined }} />
+ </Icon>
+ <Text
+ color={active ? 'gray.100' : 'gray.700'}
+ _dark={{ color: 'gray.100' }} fontSize="xl"
+ display={isCollapsed ? 'none' : 'block'}>
+ {t(name)}
+ </Text>
+ </Flex>
+ </ChakraLink>
+ );
+}
diff --git a/app/dashboard/src/components/statistics-card/index.tsx b/app/dashboard/src/components/statistics-card/index.tsx
new file mode 100644
index 0000000..ac7776d
--- /dev/null
+++ b/app/dashboard/src/components/statistics-card/index.tsx
@@ -0,0 +1,3 @@
+export { StatisticsCardInLine } from './inline-card';
+export { StatisticsCardWide } from './wide-card';
+export const StatisticsQueryKey = 'statistics-query-key';
diff --git a/app/dashboard/src/components/statistics-card/inline-card.tsx b/app/dashboard/src/components/statistics-card/inline-card.tsx
new file mode 100644
index 0000000..a56f6ef
--- /dev/null
+++ b/app/dashboard/src/components/statistics-card/inline-card.tsx
@@ -0,0 +1,19 @@
+import { StatisticsWidget } from './widget';
+import { FC, PropsWithChildren } from 'react';
+import { StatisticCardProps } from './statistics-props';
+
+export const StatisticsCardInLine: FC<PropsWithChildren<StatisticCardProps>> = ({
+ title,
+ content,
+ icon }) => {
+ return (
+ <StatisticsWidget
+ title={title}
+ icon={icon}
+ content={content}
+ display="flex"
+ justifyContent="space-between"
+ flexDirection="row"
+ />
+ );
+};
diff --git a/app/dashboard/src/components/statistics-card/statistics-props.tsx b/app/dashboard/src/components/statistics-card/statistics-props.tsx
new file mode 100644
index 0000000..5f0229c
--- /dev/null
+++ b/app/dashboard/src/components/statistics-card/statistics-props.tsx
@@ -0,0 +1,7 @@
+import { ReactElement, ReactNode } from 'react';
+
+export type StatisticCardProps = {
+ title: string;
+ content: ReactNode;
+ icon: ReactElement;
+};
diff --git a/app/dashboard/src/components/statistics-card/wide-card.tsx b/app/dashboard/src/components/statistics-card/wide-card.tsx
new file mode 100644
index 0000000..81a037f
--- /dev/null
+++ b/app/dashboard/src/components/statistics-card/wide-card.tsx
@@ -0,0 +1,17 @@
+import { StatisticsWidget } from './widget';
+import { FC, PropsWithChildren } from 'react';
+import { StatisticCardProps } from './statistics-props';
+
+export const StatisticsCardWide: FC<PropsWithChildren<StatisticCardProps>> = ({
+ title,
+ content,
+ icon }) => {
+ return (
+ <StatisticsWidget
+ title={title}
+ icon={icon}
+ content={content}
+ flexDirection="column"
+ />
+ );
+};
diff --git a/app/dashboard/src/components/statistics-card/widget.tsx b/app/dashboard/src/components/statistics-card/widget.tsx
new file mode 100644
index 0000000..35fec1d
--- /dev/null
+++ b/app/dashboard/src/components/statistics-card/widget.tsx
@@ -0,0 +1,73 @@
+
+import { Box, Text, Card, ChakraProps, HStack } from '@chakra-ui/react';
+import { FC } from 'react';
+import { StatisticCardProps } from './statistics-props';
+
+export const StatisticsWidget: FC<ChakraProps & StatisticCardProps> = ({ icon, content, title, ...chakraProps }) => {
+ return (
+ <Card
+ p={6}
+ borderWidth="1px"
+ borderColor="light-border"
+ bg="#F9FAFB"
+ _dark={{ borderColor: 'gray.600', bg: 'gray.750' }}
+ borderStyle="solid"
+ boxShadow="none"
+ borderRadius="12px"
+ width="full"
+ display="flex"
+ justifyContent="space-between"
+ {...chakraProps}
+ >
+ <HStack alignItems="center" columnGap="4">
+ <Box
+ p="2"
+ position="relative"
+ color="white"
+ _before={{
+ content: '""',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ bg: 'primary.400',
+ display: 'block',
+ w: 'full',
+ h: 'full',
+ borderRadius: '5px',
+ opacity: '.5',
+ z: '1',
+ }}
+ _after={{
+ content: '""',
+ position: 'absolute',
+ top: '-5px',
+ left: '-5px',
+ bg: 'primary.400',
+ display: 'block',
+ w: 'calc(100% + 10px)',
+ h: 'calc(100% + 10px)',
+ borderRadius: '8px',
+ opacity: '.4',
+ z: '1',
+ }}
+ >
+ {icon}
+ </Box>
+ <Text
+ color="gray.600"
+ _dark={{
+ color: 'gray.300',
+ }}
+ fontWeight="medium"
+ textTransform="capitalize"
+ fontSize="sm"
+ >
+ {title}
+ </Text>
+ </HStack>
+ <Box fontSize="3xl" fontWeight="semibold" mt="2">
+ {content}
+ </Box>
+ </Card>
+ );
+};
diff --git a/app/dashboard/src/components/theme-toggler/index.tsx b/app/dashboard/src/components/theme-toggler/index.tsx
new file mode 100644
index 0000000..f00e1b6
--- /dev/null
+++ b/app/dashboard/src/components/theme-toggler/index.tsx
@@ -0,0 +1,32 @@
+
+import { IconButton, chakra, useColorMode } from '@chakra-ui/react';
+import { MoonIcon, SunIcon } from '@heroicons/react/24/outline';
+import { updateThemeColor } from 'utils/themeColor';
+
+const iconProps = {
+ baseStyle: {
+ w: 4,
+ h: 4,
+ },
+};
+
+const DarkThemeIcon = chakra(MoonIcon, iconProps);
+const LightThemeIcon = chakra(SunIcon, iconProps);
+
+export const ThemeToggle = () => {
+ const { colorMode, toggleColorMode } = useColorMode();
+ return (
+ <IconButton
+ size="sm"
+ variant="solid"
+ colorScheme="gray"
+ aria-label="switch theme"
+ onClick={() => {
+ updateThemeColor(colorMode == 'dark' ? 'light' : 'dark');
+ toggleColorMode();
+ }}
+ >
+ {colorMode === 'light' ? <DarkThemeIcon /> : <LightThemeIcon />}
+ </IconButton>
+ );
+};
diff --git a/app/dashboard/src/constants/Pages.tsx b/app/dashboard/src/constants/Pages.tsx
index d9d201e..ddef501 100644
--- a/app/dashboard/src/constants/Pages.tsx
+++ b/app/dashboard/src/constants/Pages.tsx
@@ -2,11 +2,6 @@ import { PageType } from 'types/Page';
import { chakra } from '@chakra-ui/react';
import { CubeIcon, HomeIcon, ServerIcon, ServerStackIcon, UsersIcon } from '@heroicons/react/24/outline';
import { IconBaseStyle } from 'constants/IconBaseStyle';
-import { UsersPage } from 'pages/Users';
-import { NodesPage } from 'pages/Nodes';
-import { ServicesPage } from 'pages/Services';
-import { HostPage } from 'pages/Host';
-import { Dashboard } from 'pages/Dashboard';
const UsersSidebarItemIcon = chakra(UsersIcon, IconBaseStyle);
const ServicesSidebarItemIcon = chakra(ServerStackIcon, IconBaseStyle);
@@ -19,34 +14,25 @@ export const pages: PageType[] = [
name: 'pageHeaderTitle.Home',
path: '/',
itemIcon: HomeSidebarItemIcon,
- element: <Dashboard />,
},
{
name: 'pageHeaderTitle.Users',
path: '/users',
itemIcon: UsersSidebarItemIcon,
- element: <UsersPage />,
},
{
name: 'pageHeaderTitle.Services',
path: '/services',
itemIcon: ServicesSidebarItemIcon,
- element: <ServicesPage />,
},
{
name: 'pageHeaderTitle.Nodes',
path: '/nodes',
itemIcon: NodesSidebarItemIcon,
- element: <NodesPage />,
},
{
name: 'pageHeaderTitle.Host',
path: '/host',
itemIcon: HostSidebarItemIcon,
- element: <HostPage />,
},
];
-
-export type PageRoute = Pick<PageType,
- | 'path'
- | 'element'>
diff --git a/app/dashboard/src/contexts/DashboardContext.ts b/app/dashboard/src/contexts/DashboardContext.ts
index 5cd3e07..5ba839e 100644
--- a/app/dashboard/src/contexts/DashboardContext.ts
+++ b/app/dashboard/src/contexts/DashboardContext.ts
@@ -7,6 +7,9 @@ type DashboardStateType = {
version: string | null;
loading: boolean;
activePage: number;
+ isCollapsed: boolean;
+ collapseSidebar: () => void;
+ expandSidebar: () => void;
activatePage: (pageId: PageId) => void;
};
@@ -15,6 +18,13 @@ export const useDashboard = create(
version: null,
loading: true,
activePage: 0,
+ isCollapsed: false,
+ expandSidebar: () => {
+ set({ isCollapsed: false })
+ },
+ collapseSidebar: () => {
+ set({ isCollapsed: true })
+ },
activatePage: (pageId: number) => {
set({ activePage: pageId });
},
diff --git a/app/dashboard/src/contexts/ServicesContext.ts b/app/dashboard/src/contexts/ServicesContext.ts
index 763152c..1bf45ba 100644
--- a/app/dashboard/src/contexts/ServicesContext.ts
+++ b/app/dashboard/src/contexts/ServicesContext.ts
@@ -1,6 +1,5 @@
-
-import { queryClient } from 'utils/react-query';
-import { StatisticsQueryKey } from 'components/Statistics';
+import { queryClient } from 'service/react-query';
+import { StatisticsQueryKey } from 'components/statistics-card';
import { Service, ServiceCreate } from 'types/Service';
import { getServicesPerPageLimitSize } from 'utils/userPreferenceStorage';
import { create } from 'zustand';
diff --git a/app/dashboard/src/contexts/UsersContext.ts b/app/dashboard/src/contexts/UsersContext.ts
index 45d51a7..4bd43d0 100644
--- a/app/dashboard/src/contexts/UsersContext.ts
+++ b/app/dashboard/src/contexts/UsersContext.ts
@@ -1,12 +1,11 @@
-
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 { StatisticsQueryKey } from 'components/statistics-card';
+import { queryClient } from 'service/react-query';
import { useServices } from './ServicesContext';
import { UsersFilterType, FilterUsageType } from 'types/Filter';
@@ -111,14 +110,14 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
},
deleteUser: async (user: User) => {
set({ editingUser: null });
- return fetch(`/user/${user.username}`, { method: 'DELETE' }).then(() => {
+ return fetch(`/users/${user.username}`, { method: 'DELETE' }).then(() => {
set({ deletingUser: null });
get().refetchUsers();
queryClient.invalidateQueries(StatisticsQueryKey);
});
},
createUser: async (body: UserCreate) => {
- return fetch('/user', { method: 'POST', body }).then(() => {
+ return fetch('/users', { method: 'POST', body }).then(() => {
set({ editingUser: null });
get().refetchUsers();
useServices.getState().refetchServices();
@@ -126,7 +125,7 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
});
},
editUser: async (body: UserCreate) => {
- return fetch(`/user/${body.username}`, { method: 'PUT', body }).then(
+ return fetch(`/users/${body.username}`, { method: 'PUT', body }).then(
() => {
get().onEditingUser(null);
get().refetchUsers();
@@ -139,10 +138,10 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
if (!query[key as keyof FilterUsageType])
delete query[key as keyof FilterUsageType];
}
- return fetch(`/user/${body.username}/usage`, { method: 'GET', query });
+ return fetch(`/users/${body.username}/usage`, { method: 'GET', query });
},
resetDataUsage: async (user) => {
- return fetch(`/user/${user.username}/reset`, { method: 'POST' }).then(
+ return fetch(`/users/${user.username}/reset`, { method: 'POST' }).then(
() => {
set({ resetUsageUser: null });
get().refetchUsers();
@@ -150,7 +149,7 @@ export const useUsers = create(subscribeWithSelector<UsersStateType>((set, get)
);
},
revokeSubscription: async (user) => {
- return fetch(`/user/${user.username}/revoke_sub`, {
+ return fetch(`/users/${user.username}/revoke_sub`, {
method: 'POST',
}).then((user) => {
set({ revokeSubscriptionUser: null, editingUser: user });
diff --git a/app/dashboard/src/index.scss b/app/dashboard/src/index.scss
index b78a457..782a579 100644
--- a/app/dashboard/src/index.scss
+++ b/app/dashboard/src/index.scss
@@ -1,5 +1,9 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap");
+body {
+ overflow: hidden;
+}
+
.bg-blue-animate {
animation: blur-animate 200ms ease-in;
backdrop-filter: blur(10px);
@@ -227,13 +231,16 @@ table thead {
.pulse {
animation: pulse-animation 3s infinite;
}
+
.pulse.green {
animation: green-pulse-animation 3s infinite;
}
-.pulse.red{
+
+.pulse.red {
animation: red-pulse-animation 3s infinite;
}
-.pulse.orange{
+
+.pulse.orange {
animation: orange-pulse-animation 3s infinite;
}
@@ -246,6 +253,7 @@ table thead {
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
}
+
@keyframes green-pulse-animation {
0% {
box-shadow: 0 0 0 0px #66ff9975;
@@ -255,6 +263,7 @@ table thead {
box-shadow: 0 0 0 10px #66ff9900;
}
}
+
@keyframes red-pulse-animation {
0% {
box-shadow: 0 0 0 0px #e53e3e8c;
@@ -264,6 +273,7 @@ table thead {
box-shadow: 0 0 0 10px #e53e3e00;
}
}
+
@keyframes orange-pulse-animation {
0% {
box-shadow: 0 0 0 0px #fbd38d85;
@@ -279,4 +289,4 @@ table thead {
align-items: center;
justify-content: start;
gap: .7rem;
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/src/index.tsx b/app/dashboard/src/index.tsx
index 28bcf5d..4a8667e 100644
--- a/app/dashboard/src/index.tsx
+++ b/app/dashboard/src/index.tsx
@@ -9,12 +9,11 @@ import 'locales/i18n';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClientProvider } from 'react-query';
-import { queryClient } from 'utils/react-query';
+import { queryClient } from 'service/react-query';
import { updateThemeColor } from 'utils/themeColor';
import { theme } from '../chakra.config';
import App from './App';
import 'index.scss';
-import { BrowserRouter } from 'react-router-dom';
dayjs.extend(Timezone);
dayjs.extend(LocalizedFormat);
@@ -26,12 +25,10 @@ updateThemeColor(localStorageManager.get() || 'light');
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
- <BrowserRouter basename={import.meta.env.BASE_URL}>
- <ChakraProvider theme={theme}>
- <QueryClientProvider client={queryClient}>
- <App />
- </QueryClientProvider>
- </ChakraProvider>
- </BrowserRouter>
+ <ChakraProvider theme={theme}>
+ <QueryClientProvider client={queryClient}>
+ <App />
+ </QueryClientProvider>
+ </ChakraProvider>
</React.StrictMode>
);
diff --git a/app/dashboard/src/layouts/console-layout.tsx b/app/dashboard/src/layouts/console-layout.tsx
new file mode 100644
index 0000000..1f768f8
--- /dev/null
+++ b/app/dashboard/src/layouts/console-layout.tsx
@@ -0,0 +1,35 @@
+import { Box } from '@chakra-ui/react';
+import { PanelGroup, Panel, PanelResizeHandle, ImperativePanelHandle } from 'react-resizable-panels';
+import { Footer } from 'components/Footer';
+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';
+
+export const ConsoleLayout = () => {
+ const ref = useRef<ImperativePanelHandle>(null);
+ const { collapseSidebar, expandSidebar } = useDashboard();
+
+ return (
+ <PanelGroup autoSaveId="sidebar-ratio" direction="horizontal">
+ <Panel collapsible onExpand={expandSidebar} onCollapse={collapseSidebar} collapsedSize={5} defaultSize={20} maxSize={20} minSize={10} ref={ref}>
+ <Sidebar />
+ </Panel>
+ <PanelResizeHandle />
+ <Panel minSize={30} >
+ <Box overflowY="auto" p="6" >
+ <Header />
+ <Box >
+ <Suspense>
+ <Outlet />
+ </Suspense>
+ <Footer />
+ </Box>
+ </Box>
+ </Panel>
+ </PanelGroup>
+ );
+};
+
+export default ConsoleLayout;
diff --git a/app/dashboard/src/pages/Dashboard.tsx b/app/dashboard/src/pages/Dashboard.tsx
deleted file mode 100644
index 9df076e..0000000
--- a/app/dashboard/src/pages/Dashboard.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Box, VStack } from '@chakra-ui/react';
-import { FC } from 'react';
-import { Statistics } from '../components/Statistics';
-
-export const Dashboard: FC = () => {
- return (
- <VStack justifyContent="space-between" p="6" rowGap={4}>
- <Box w="full">
- <Statistics mt="4" />
- </Box>
- </VStack>
- );
-};
-
-export default Dashboard;
diff --git a/app/dashboard/src/pages/Host.tsx b/app/dashboard/src/pages/Host.tsx
deleted file mode 100644
index af5d828..0000000
--- a/app/dashboard/src/pages/Host.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Box, VStack } from '@chakra-ui/react';
-import { HostsDialog } from 'components/HostsDialog';
-
-export const HostPage = () => {
- return (
- <VStack justifyContent="space-between" p="6" rowGap={4} >
- <Box w="full" >
- <HostsDialog />
- </Box>
- </VStack >
- )
-}
diff --git a/app/dashboard/src/pages/Nodes.tsx b/app/dashboard/src/pages/Nodes.tsx
deleted file mode 100644
index a3be313..0000000
--- a/app/dashboard/src/pages/Nodes.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-
-import { FC, useEffect } from 'react';
-import { Box, VStack } from '@chakra-ui/react';
-import { CoreSettingsModal } from 'components/CoreSettingsModal';
-import { NodesDialog } from 'components/NodesModal';
-import { NodesUsage } from 'components/NodesUsage';
-import { NodesTable } from 'components/NodesTable';
-import { NodesFilters } from 'components/NodesFilters';
-import { DeleteNodeModal } from 'components/DeleteNodeModal';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useNodes } from 'contexts/NodesContext';
-import { pages } from 'constants/Pages';
-
-export const NodesPage: FC = () => {
- useEffect(() => {
- useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/nodes'));
- useNodes.getState().refetchNodes();
- useNodes.getState().refetchCertificate();
- }, []);
- return (
- <VStack justifyContent="space-between" p="6" rowGap={4}>
- <Box w="full">
- <NodesFilters />
- <NodesTable />
- <NodesDialog />
- <DeleteNodeModal />
- <NodesUsage />
- <CoreSettingsModal />
- </Box>
- </VStack>
- )
-}
diff --git a/app/dashboard/src/pages/Services.tsx b/app/dashboard/src/pages/Services.tsx
deleted file mode 100644
index 2c1fd28..0000000
--- a/app/dashboard/src/pages/Services.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { FC, useEffect } from 'react';
-import { Box, VStack } from '@chakra-ui/react';
-import { ServicesFilters } from 'components/ServicesFilters';
-import { ServicesTable } from 'components/ServicesTable';
-import { pages } from 'constants/Pages';
-import { ServiceDialog } from 'components/ServiceDialog';
-import { useServices } from 'contexts/ServicesContext';
-import { useDashboard } from 'contexts/DashboardContext';
-import { DeleteServiceModal } from 'components/DeleteServiceModal';
-
-
-export const ServicesPage: FC = () => {
- useEffect(() => {
- useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/services'));
- useServices.getState().refetchServices();
- }, []);
- return (
- <VStack justifyContent="space-between" maxH="100vh" p="2rem" rowGap={4}>
- <Box w="full" >
- <ServiceDialog />
- <DeleteServiceModal />
- <ServicesFilters />
- <ServicesTable />
- </Box>
- </VStack>
- )
-}
diff --git a/app/dashboard/src/pages/Users.tsx b/app/dashboard/src/pages/Users.tsx
deleted file mode 100644
index 3a47727..0000000
--- a/app/dashboard/src/pages/Users.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-
-import { ResetAllUsageModal } from 'components/ResetAllUsageModal';
-import { Box, VStack } from '@chakra-ui/react';
-import { DeleteUserModal } from 'components/DeleteUserModal';
-import { UserDialog } from 'components/UserDialog';
-import { UsersTable } from 'components/UsersTable/';
-import { useDashboard } from 'contexts/DashboardContext';
-import { useEffect } from 'react';
-import { UsersFilters } from 'components/UsersFilters';
-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'));
- useUsers.getState().refetchUsers();
- }, []);
- return (
- <VStack justifyContent="space-between" maxH="100vh" p="2rem" rowGap={4}>
- <Box w="full" >
- <UsersFilters />
- <UsersTable />
- <UserDialog />
- <QRCodeDialog />
- <DeleteUserModal />
- <RevokeSubscriptionModal />
- <ResetAllUsageModal />
- <ResetUserUsageModal />
- </Box>
- </VStack>
- )
-}
diff --git a/app/dashboard/src/pages/home/index.tsx b/app/dashboard/src/pages/home/index.tsx
new file mode 100644
index 0000000..c8b5634
--- /dev/null
+++ b/app/dashboard/src/pages/home/index.tsx
@@ -0,0 +1,14 @@
+
+import Page from 'components/page';
+import { FC } from 'react';
+import { Statistics } from './statistics';
+
+export const Home: FC = () => {
+ return (
+ <Page>
+ <Statistics mt="4" />
+ </Page>
+ );
+};
+
+export default Home;
diff --git a/app/dashboard/src/components/Statistics.tsx b/app/dashboard/src/pages/home/statistics.tsx
similarity index 54%
rename from app/dashboard/src/components/Statistics.tsx
rename to app/dashboard/src/pages/home/statistics.tsx
index da5d203..ae7c6b6 100644
--- a/app/dashboard/src/components/Statistics.tsx
+++ b/app/dashboard/src/pages/home/statistics.tsx
@@ -1,15 +1,16 @@
-import { Box, BoxProps, Card, chakra, HStack, Text } from '@chakra-ui/react';
+import { BoxProps, chakra, HStack, Text } from '@chakra-ui/react';
import {
ChartBarIcon,
ChartPieIcon,
UsersIcon,
} from '@heroicons/react/24/outline';
import { useDashboard } from 'contexts/DashboardContext';
-import { FC, PropsWithChildren, ReactElement, ReactNode } from 'react';
+import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { fetch } from 'service/http';
import { formatBytes, numberWithCommas } from 'utils/formatByte';
+import { StatisticsCardInLine, StatisticsQueryKey } from 'components/statistics-card';
const TotalUsersIcon = chakra(UsersIcon, {
baseStyle: {
@@ -38,85 +39,7 @@ const MemoryIcon = chakra(ChartPieIcon, {
},
});
-type StatisticCardProps = {
- title: string;
- content: ReactNode;
- icon: ReactElement;
-};
-const StatisticCard: FC<PropsWithChildren<StatisticCardProps>> = ({
- title,
- content,
- icon,
-}) => {
- return (
- <Card
- p={6}
- borderWidth="1px"
- borderColor="light-border"
- bg="#F9FAFB"
- _dark={{ borderColor: 'gray.600', bg: 'gray.750' }}
- borderStyle="solid"
- boxShadow="none"
- borderRadius="12px"
- width="full"
- display="flex"
- justifyContent="space-between"
- flexDirection="row"
- >
- <HStack alignItems="center" columnGap="4">
- <Box
- p="2"
- position="relative"
- color="white"
- _before={{
- content: '""',
- position: 'absolute',
- top: 0,
- left: 0,
- bg: 'primary.400',
- display: 'block',
- w: 'full',
- h: 'full',
- borderRadius: '5px',
- opacity: '.5',
- z: '1',
- }}
- _after={{
- content: '""',
- position: 'absolute',
- top: '-5px',
- left: '-5px',
- bg: 'primary.400',
- display: 'block',
- w: 'calc(100% + 10px)',
- h: 'calc(100% + 10px)',
- borderRadius: '8px',
- opacity: '.4',
- z: '1',
- }}
- >
- {icon}
- </Box>
- <Text
- color="gray.600"
- _dark={{
- color: 'gray.300',
- }}
- fontWeight="medium"
- textTransform="capitalize"
- fontSize="sm"
- >
- {title}
- </Text>
- </HStack>
- <Box fontSize="3xl" fontWeight="semibold" mt="2">
- {content}
- </Box>
- </Card>
- );
-};
-export const StatisticsQueryKey = 'statistics-query-key';
export const Statistics: FC<BoxProps> = (props) => {
const { version } = useDashboard();
const { data: systemData } = useQuery({
@@ -139,7 +62,7 @@ export const Statistics: FC<BoxProps> = (props) => {
flexDirection={{ lg: 'row', base: 'column' }}
{...props}
>
- <StatisticCard
+ <StatisticsCardInLine
title={t('activeUsers')}
content={
systemData && (
@@ -152,24 +75,24 @@ export const Statistics: FC<BoxProps> = (props) => {
display="inline-block"
pb="5px"
>
- / {numberWithCommas(systemData.total_user)}
+ / {numberWithCommas(systemData.total_user)}
</Text>
</HStack>
)
}
icon={<TotalUsersIcon />}
/>
- <StatisticCard
+ <StatisticsCardInLine
title={t('dataUsage')}
content={
systemData &&
- formatBytes(
- systemData.incoming_bandwidth + systemData.outgoing_bandwidth
- )
+ formatBytes(
+ systemData.incoming_bandwidth + systemData.outgoing_bandwidth
+ )
}
icon={<NetworkIcon />}
/>
- <StatisticCard
+ <StatisticsCardInLine
title={t('memoryUsage')}
content={
systemData && (
diff --git a/app/dashboard/src/components/HostsDialog.tsx b/app/dashboard/src/pages/host/dialog.tsx
similarity index 99%
rename from app/dashboard/src/components/HostsDialog.tsx
rename to app/dashboard/src/pages/host/dialog.tsx
index 48c226e..51357c0 100644
--- a/app/dashboard/src/components/HostsDialog.tsx
+++ b/app/dashboard/src/pages/host/dialog.tsx
@@ -55,9 +55,9 @@ 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 './Dialog/Icons';
-import { Icon } from './Icon';
-import { Input as CustomInput } from './Input';
+import { DeleteIcon } from 'components/dialog/Icons';
+import { Icon } from 'components/icon';
+import { Input as CustomInput } from 'components/Input';
const Select = chakra(ChakraSelect, {
baseStyle: {
diff --git a/app/dashboard/src/pages/host/index.tsx b/app/dashboard/src/pages/host/index.tsx
new file mode 100644
index 0000000..16efd76
--- /dev/null
+++ b/app/dashboard/src/pages/host/index.tsx
@@ -0,0 +1,12 @@
+import { HostsDialog } from './dialog';
+import Page from 'components/page';
+
+const HostPage = () => {
+ return (
+ <Page>
+ <HostsDialog />
+ </Page>
+ )
+}
+
+export default HostPage;
diff --git a/app/dashboard/src/pages/Login.tsx b/app/dashboard/src/pages/login/index.tsx
similarity index 96%
rename from app/dashboard/src/pages/Login.tsx
rename to app/dashboard/src/pages/login/index.tsx
index 56ea69b..b5f1cfa 100644
--- a/app/dashboard/src/pages/Login.tsx
+++ b/app/dashboard/src/pages/login/index.tsx
@@ -10,7 +10,7 @@ import {
Text,
VStack,
} from '@chakra-ui/react';
-import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/outline';
+import { ArrowRightStartOnRectangleIcon } from '@heroicons/react/24/outline';
import { zodResolver } from '@hookform/resolvers/zod';
import { FC, useEffect, useState } from 'react';
import { FieldValues, useForm } from 'react-hook-form';
@@ -37,7 +37,7 @@ export const LogoIcon = chakra(Logo, {
},
});
-const LoginIcon = chakra(ArrowRightOnRectangleIcon, {
+const LoginIcon = chakra(ArrowRightStartOnRectangleIcon, {
baseStyle: {
w: 5,
h: 5,
diff --git a/app/dashboard/src/components/NodesModal.tsx b/app/dashboard/src/pages/nodes/dialog.tsx
similarity index 97%
rename from app/dashboard/src/components/NodesModal.tsx
rename to app/dashboard/src/pages/nodes/dialog.tsx
index 0ecbcf5..c3d7af2 100644
--- a/app/dashboard/src/components/NodesModal.tsx
+++ b/app/dashboard/src/pages/nodes/dialog.tsx
@@ -28,9 +28,9 @@ import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import 'slick-carousel/slick/slick-theme.css';
import 'slick-carousel/slick/slick.css';
-import { DeleteIcon, AddIcon, EditIcon } from './Dialog/Icons';
-import { Input } from './Input';
-import { DialogModalHeader } from './Dialog/ModalHeader';
+import { DeleteIcon, AddIcon, EditIcon } from 'components/dialog/Icons';
+import { Input } from 'components/Input';
+import { DialogModalHeader } from 'components/dialog/ModalHeader';
const CustomInput = chakra(Input, {
baseStyle: {
diff --git a/app/dashboard/src/components/NodesFilters.tsx b/app/dashboard/src/pages/nodes/filters.tsx
similarity index 98%
rename from app/dashboard/src/components/NodesFilters.tsx
rename to app/dashboard/src/pages/nodes/filters.tsx
index 79538be..8bb9a9b 100644
--- a/app/dashboard/src/components/NodesFilters.tsx
+++ b/app/dashboard/src/pages/nodes/filters.tsx
@@ -25,7 +25,7 @@ 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 './Table/Icons';
+import { CopiedIcon, SubscriptionLinkIcon } from 'components/Table/Icons';
const iconProps = {
baseStyle: {
diff --git a/app/dashboard/src/pages/nodes/index.tsx b/app/dashboard/src/pages/nodes/index.tsx
new file mode 100644
index 0000000..64cd905
--- /dev/null
+++ b/app/dashboard/src/pages/nodes/index.tsx
@@ -0,0 +1,32 @@
+
+import { FC, useEffect } from 'react';
+import { CoreSettingsModal } from './modals/core-settings-modal';
+import { NodesDialog } from './dialog';
+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 { pages } from 'constants/Pages';
+import Page from 'components/page';
+
+const NodesPage: FC = () => {
+ useEffect(() => {
+ useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/nodes'));
+ useNodes.getState().refetchNodes();
+ useNodes.getState().refetchCertificate();
+ }, []);
+ return (
+ <Page>
+ <NodesUsage />
+ <NodesFilters />
+ <NodesTable />
+ <NodesDialog />
+ <DeleteNodeModal />
+ <CoreSettingsModal />
+ </Page>
+ );
+}
+
+export default NodesPage;
diff --git a/app/dashboard/src/components/CoreSettingsModal.tsx b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
similarity index 97%
rename from app/dashboard/src/components/CoreSettingsModal.tsx
rename to app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
index beac875..64f7831 100644
--- a/app/dashboard/src/components/CoreSettingsModal.tsx
+++ b/app/dashboard/src/pages/nodes/modals/core-settings-modal.tsx
@@ -36,10 +36,10 @@ import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { ReadyState } from 'react-use-websocket';
import { useWebSocket } from 'react-use-websocket/dist/lib/use-websocket';
-import { getAuthToken } from 'utils/authStorage';
-import { Icon } from './Icon';
-import { JsonEditor } from './JsonEditor';
-import './JsonEditor/themes.js';
+import { getAuthToken } from 'service/auth-storage';
+import { Icon } from 'components/icon';
+import { JsonEditor } from 'components/json-editor';
+import 'components/json-editor/themes.js';
export const MAX_NUMBER_OF_LOGS = 500;
diff --git a/app/dashboard/src/pages/nodes/modals/delete-modal.tsx b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx
new file mode 100644
index 0000000..c44e774
--- /dev/null
+++ b/app/dashboard/src/pages/nodes/modals/delete-modal.tsx
@@ -0,0 +1,48 @@
+import {
+ useToast,
+} from '@chakra-ui/react';
+import { useNodes } from 'contexts/NodesContext';
+import { FC, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { DeleteModal } from 'components/delete-modal';
+
+export const DeleteNodeModal: FC = () => {
+ const { deleteNode, refetchNodes, deletingNode, onDeletingNode } = useNodes();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onClose = () => {
+ onDeletingNode(undefined);
+ };
+
+ const onDelete = () => {
+ if (deletingNode) {
+ setLoading(true);
+ deleteNode(deletingNode)
+ .then(() => {
+ refetchNodes();
+ toast({
+ title: t('deleteNode.deleteSuccess', { name: deletingNode.name }),
+ status: 'success',
+ isClosable: true,
+ position: 'top',
+ duration: 3000,
+ });
+ })
+ .then(onClose)
+ .finally(setLoading.bind(null, false));
+ }
+ };
+
+ return (
+ <DeleteModal
+ onDelete={onDelete}
+ loading={loading}
+ isOpen={!!deletingNode}
+ onClose={onClose}
+ deletingObjectName={deletingNode?.name}
+ deleteObjectTitleT={'deleteNode.title'}
+ deleteObjectPromptT={'deleteNode.prompt'}
+ />
+ );
+};
diff --git a/app/dashboard/src/components/NodesTable/index.tsx b/app/dashboard/src/pages/nodes/table.tsx
similarity index 99%
rename from app/dashboard/src/components/NodesTable/index.tsx
rename to app/dashboard/src/pages/nodes/table.tsx
index db5b08a..4688da3 100644
--- a/app/dashboard/src/components/NodesTable/index.tsx
+++ b/app/dashboard/src/pages/nodes/table.tsx
@@ -26,7 +26,7 @@ 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 '../StatusBadge';
+import { StatusBadge } from 'components/StatusBadge';
import { EmptySection } from 'components/Table/EmptySection';
import {
AccordionArrowIcon,
diff --git a/app/dashboard/src/pages/nodes/usage.tsx b/app/dashboard/src/pages/nodes/usage.tsx
new file mode 100644
index 0000000..ba4fdd5
--- /dev/null
+++ b/app/dashboard/src/pages/nodes/usage.tsx
@@ -0,0 +1,86 @@
+import {
+ Box,
+ CircularProgress,
+ VStack,
+ chakra,
+ useColorMode,
+} from '@chakra-ui/react';
+import { ChartPieIcon } from '@heroicons/react/24/outline';
+import { FilterUsageType } from 'types/Filter';
+import { useNodes } from 'contexts/NodesContext';
+import dayjs from 'dayjs';
+import { FC, Suspense, useEffect, useState } from 'react';
+import ReactApexChart from 'react-apexcharts';
+import { useTranslation } from 'react-i18next';
+import { Icon } from 'components/icon';
+import { UsageFilter, createUsageConfig } from 'components/UsageFilter';
+import { StatisticsCardWide } from 'components/statistics-card';
+
+const UsageIcon = chakra(ChartPieIcon, {
+ baseStyle: {
+ w: 5,
+ h: 5,
+ },
+});
+
+export type NodesUsageProps = {};
+
+export const NodesUsage: FC<NodesUsageProps> = () => {
+ const { isShowingNodesUsage } = useNodes();
+ const { fetchNodesUsage } = useNodes();
+ const { t } = useTranslation();
+ const { colorMode } = useColorMode();
+
+ const usageTitle = t('userDialog.total');
+ const [usage, setUsage] = useState(createUsageConfig(colorMode, usageTitle));
+ const [usageFilter, setUsageFilter] = useState('1m');
+ const fetchUsageWithFilter = (query: FilterUsageType) => {
+ fetchNodesUsage(query).then((data: any) => {
+ const labels = [];
+ const series = [];
+ for (const key in data.usages) {
+ const entry = data.usages[key];
+ series.push(entry.uplink + entry.downlink);
+ labels.push(entry.node_name);
+ }
+ setUsage(createUsageConfig(colorMode, usageTitle, series, labels));
+ });
+ };
+
+ useEffect(() => {
+ if (isShowingNodesUsage) {
+ fetchUsageWithFilter({
+ start: dayjs().utc().subtract(30, 'day').format('YYYY-MM-DDTHH:00:00'),
+ });
+ }
+ }, [isShowingNodesUsage]);
+
+ return (
+ <StatisticsCardWide
+ title={t('header.nodesUsage')}
+ icon={<Icon color="primary">
+ <UsageIcon color="white" /> </Icon>}
+ content={
+ <VStack gap={4}>
+ <UsageFilter
+ defaultValue={usageFilter}
+ onChange={(filter, query) => {
+ setUsageFilter(filter);
+ fetchUsageWithFilter(query);
+ }}
+ />
+ <Box justifySelf="center" w="full" maxW="300px" mt="4">
+ <Suspense fallback={<CircularProgress isIndeterminate />}>
+ <ReactApexChart
+ options={usage.options}
+ series={usage.series}
+ type="donut"
+ height="500px"
+ />
+ </Suspense>
+ </Box>
+ </VStack>
+
+ } />
+ );
+};
diff --git a/app/dashboard/src/pages/routes.tsx b/app/dashboard/src/pages/routes.tsx
new file mode 100644
index 0000000..db1637c
--- /dev/null
+++ b/app/dashboard/src/pages/routes.tsx
@@ -0,0 +1,46 @@
+import { lazy } from 'react';
+import { createHashRouter } from 'react-router-dom';
+import { fetchAdminLoader } from 'service/http';
+
+const ConsoleLayout = lazy(() => import('layouts/console-layout'));
+const Home = lazy(() => import('./home'));
+const Host = lazy(() => import('./host'));
+const Users = lazy(() => import('./users'));
+const Login = lazy(() => import('./login'));
+const Nodes = lazy(() => import('./nodes'));
+const Services = lazy(() => import('./services'));
+
+export const router = createHashRouter([
+ {
+ path: '/',
+ errorElement: <Login />,
+ loader: fetchAdminLoader,
+ element: <ConsoleLayout />,
+ children: [
+ {
+ path: '/',
+ element: <Home />,
+ },
+ {
+ path: '/users',
+ element: <Users />,
+ },
+ {
+ path: '/services',
+ element: <Services />,
+ },
+ {
+ path: '/nodes',
+ element: <Nodes />,
+ },
+ {
+ path: '/host',
+ element: <Host />,
+ },
+ ],
+ },
+ {
+ path: '/login/',
+ element: <Login />,
+ },
+]);
diff --git a/app/dashboard/src/components/ServiceDialog/DefaultValues.ts b/app/dashboard/src/pages/services/dialog/default-values.ts
similarity index 71%
rename from app/dashboard/src/components/ServiceDialog/DefaultValues.ts
rename to app/dashboard/src/pages/services/dialog/default-values.ts
index 20a8a6c..371860c 100644
--- a/app/dashboard/src/components/ServiceDialog/DefaultValues.ts
+++ b/app/dashboard/src/pages/services/dialog/default-values.ts
@@ -1,6 +1,6 @@
-import { FormType } from './FormSchema';
+import { FormType } from './form-schema';
export const getDefaultValues = (): FormType => {
return {
diff --git a/app/dashboard/src/components/ServiceDialog/FormSchema.ts b/app/dashboard/src/pages/services/dialog/form-schema.ts
similarity index 100%
rename from app/dashboard/src/components/ServiceDialog/FormSchema.ts
rename to app/dashboard/src/pages/services/dialog/form-schema.ts
diff --git a/app/dashboard/src/components/ServiceDialog/InboundsField.tsx b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx
similarity index 96%
rename from app/dashboard/src/components/ServiceDialog/InboundsField.tsx
rename to app/dashboard/src/pages/services/dialog/inbounds-field.tsx
index fcb5942..d9b46eb 100644
--- a/app/dashboard/src/components/ServiceDialog/InboundsField.tsx
+++ b/app/dashboard/src/pages/services/dialog/inbounds-field.tsx
@@ -2,7 +2,7 @@ import { FormControl, FormLabel, FormErrorMessage, Center, Spinner, Text, Spacer
import { Inbounds, InboundType } from 'types/Inbounds';
import { TFunction } from 'i18next';
import { Controller } from 'react-hook-form';
-import { InboundsSelectList } from './InboundsSelectList';
+import { InboundsSelectList } from './inbounds-select-list';
interface InboundsFieldProps {
diff --git a/app/dashboard/src/components/ServiceDialog/InboundsSelectList.tsx b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
similarity index 96%
rename from app/dashboard/src/components/ServiceDialog/InboundsSelectList.tsx
rename to app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
index 9aadb0f..5bcb5ea 100644
--- a/app/dashboard/src/components/ServiceDialog/InboundsSelectList.tsx
+++ b/app/dashboard/src/pages/services/dialog/inbounds-select-list.tsx
@@ -4,7 +4,7 @@ import {
Flex,
Stack,
} from '@chakra-ui/react';
-import { ListCheckBox } from 'components/ListCheckBox';
+import { ListCheckBox } from 'components/list-check-box';
import { Inbounds, InboundType } from 'types/Inbounds';
import { forwardRef, ReactNode } from 'react';
import {
diff --git a/app/dashboard/src/components/ServiceDialog/index.tsx b/app/dashboard/src/pages/services/dialog/index.tsx
similarity index 94%
rename from app/dashboard/src/components/ServiceDialog/index.tsx
rename to app/dashboard/src/pages/services/dialog/index.tsx
index 4a79cf3..850019a 100644
--- a/app/dashboard/src/components/ServiceDialog/index.tsx
+++ b/app/dashboard/src/pages/services/dialog/index.tsx
@@ -21,13 +21,13 @@ import { InboundType } from 'types/Inbounds';
import { FC, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
-import { AddIcon, DeleteIcon, EditIcon } from 'components/Dialog/Icons';
-import { DialogModalHeader } from 'components/Dialog/ModalHeader';
-import { DialogModalFooter } from 'components/Dialog/ModalFooter';
-import { getDefaultValues } from './DefaultValues';
-import { schema, FormType } from './FormSchema';
-import { InboundsField } from './InboundsField';
-import { NameField } from './NameField';
+import { AddIcon, DeleteIcon, EditIcon } from 'components/dialog/Icons';
+import { DialogModalHeader } from 'components/dialog/ModalHeader';
+import { DialogModalFooter } from 'components/dialog/ModalFooter';
+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';
diff --git a/app/dashboard/src/components/ServiceDialog/NameField.tsx b/app/dashboard/src/pages/services/dialog/name-field.tsx
similarity index 100%
rename from app/dashboard/src/components/ServiceDialog/NameField.tsx
rename to app/dashboard/src/pages/services/dialog/name-field.tsx
diff --git a/app/dashboard/src/components/ServicesFilters.tsx b/app/dashboard/src/pages/services/filters.tsx
similarity index 100%
rename from app/dashboard/src/components/ServicesFilters.tsx
rename to app/dashboard/src/pages/services/filters.tsx
diff --git a/app/dashboard/src/pages/services/index.tsx b/app/dashboard/src/pages/services/index.tsx
new file mode 100644
index 0000000..ed826a0
--- /dev/null
+++ b/app/dashboard/src/pages/services/index.tsx
@@ -0,0 +1,28 @@
+
+import { FC, useEffect } from 'react';
+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 { DeleteServiceModal } from './modals/delete-modal';
+import Page from 'components/page';
+
+
+const ServicesPage: FC = () => {
+ useEffect(() => {
+ useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/services'));
+ useServices.getState().refetchServices();
+ }, []);
+ return (
+ <Page>
+ <ServiceDialog />
+ <DeleteServiceModal />
+ <ServicesFilters />
+ <ServicesTable />
+ </Page>
+ )
+}
+
+export default ServicesPage;
diff --git a/app/dashboard/src/pages/services/modals/delete-modal.tsx b/app/dashboard/src/pages/services/modals/delete-modal.tsx
new file mode 100644
index 0000000..3bb8133
--- /dev/null
+++ b/app/dashboard/src/pages/services/modals/delete-modal.tsx
@@ -0,0 +1,48 @@
+import {
+ useToast,
+} from '@chakra-ui/react';
+import { useServices } from 'contexts/ServicesContext';
+import { FC, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { DeleteModal } from 'components/delete-modal';
+
+export const DeleteServiceModal: FC = () => {
+ const { deleteService, refetchServices, deletingService, onDeletingService } = useServices();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onClose = () => {
+ onDeletingService(null);
+ };
+
+ const onDelete = () => {
+ if (deletingService) {
+ setLoading(true);
+ deleteService(deletingService)
+ .then(() => {
+ refetchServices();
+ toast({
+ title: t('deleteService.deleteSuccess', { name: deletingService.name }),
+ status: 'success',
+ isClosable: true,
+ position: 'top',
+ duration: 3000,
+ });
+ })
+ .then(onClose)
+ .finally(setLoading.bind(null, false));
+ }
+ };
+
+ return (
+ <DeleteModal
+ deleteObjectTitleT={'deleteService.title'}
+ deleteObjectPromptT={'deleteService.prompt'}
+ onDelete={onDelete}
+ loading={loading}
+ isOpen={!!deletingService}
+ onClose={onClose}
+ deletingObjectName={deletingService?.name}
+ />
+ );
+};
diff --git a/app/dashboard/src/components/ServicesTable/index.tsx b/app/dashboard/src/pages/services/table.tsx
similarity index 100%
rename from app/dashboard/src/components/ServicesTable/index.tsx
rename to app/dashboard/src/pages/services/table.tsx
diff --git a/app/dashboard/src/components/UserDialog/DataLimitField.tsx b/app/dashboard/src/pages/users/dialog/data-limit-field.tsx
similarity index 95%
rename from app/dashboard/src/components/UserDialog/DataLimitField.tsx
rename to app/dashboard/src/pages/users/dialog/data-limit-field.tsx
index 431eb49..cd261b3 100644
--- a/app/dashboard/src/components/UserDialog/DataLimitField.tsx
+++ b/app/dashboard/src/pages/users/dialog/data-limit-field.tsx
@@ -1,5 +1,5 @@
import { FormControl, FormLabel } from '@chakra-ui/react';
-import { Input } from '../Input';
+import { Input } from 'components/Input';
import { TFunction } from 'i18next';
import { Controller } from 'react-hook-form';
@@ -36,4 +36,4 @@ export const DataLimitField = ({ form, disabled, t }: DataLimitFieldProps) => {
/>
</FormControl>
)
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/src/components/UserDialog/DefaultValues.ts b/app/dashboard/src/pages/users/dialog/default-values.ts
similarity index 84%
rename from app/dashboard/src/components/UserDialog/DefaultValues.ts
rename to app/dashboard/src/pages/users/dialog/default-values.ts
index c86ca51..3c516d3 100644
--- a/app/dashboard/src/components/UserDialog/DefaultValues.ts
+++ b/app/dashboard/src/pages/users/dialog/default-values.ts
@@ -1,5 +1,5 @@
-import { FormType } from './FormSchema';
+import { FormType } from './form-schema';
export const getDefaultValues = (): FormType => {
diff --git a/app/dashboard/src/components/UserDialog/ExpireDateField.tsx b/app/dashboard/src/pages/users/dialog/expire-date-field.tsx
similarity index 94%
rename from app/dashboard/src/components/UserDialog/ExpireDateField.tsx
rename to app/dashboard/src/pages/users/dialog/expire-date-field.tsx
index eca499c..3175a74 100644
--- a/app/dashboard/src/components/UserDialog/ExpireDateField.tsx
+++ b/app/dashboard/src/pages/users/dialog/expire-date-field.tsx
@@ -8,13 +8,13 @@ import { i18n, TFunction } from 'i18next';
import ReactDatePicker from 'react-datepicker';
import { Controller } from 'react-hook-form';
import { relativeExpiryDate } from 'utils/dateFormatter';
-import { Input } from '../Input';
+import { Input } from 'components/Input';
interface ExpireDateFieldProps {
- form: any;
- disabled: boolean;
- t: TFunction<'translation', undefined, 'translation'>;
- i18n: i18n;
+ form: any;
+ disabled: boolean;
+ t: TFunction<'translation', undefined, 'translation'>;
+ i18n: i18n;
}
export const ExpireDateField = ({ form, t, i18n, disabled }: ExpireDateFieldProps) => {
diff --git a/app/dashboard/src/components/UserDialog/FormSchema.ts b/app/dashboard/src/pages/users/dialog/form-schema.ts
similarity index 100%
rename from app/dashboard/src/components/UserDialog/FormSchema.ts
rename to app/dashboard/src/pages/users/dialog/form-schema.ts
diff --git a/app/dashboard/src/components/UserDialog/UserDialogIcons.ts b/app/dashboard/src/pages/users/dialog/icons.ts
similarity index 100%
rename from app/dashboard/src/components/UserDialog/UserDialogIcons.ts
rename to app/dashboard/src/pages/users/dialog/icons.ts
diff --git a/app/dashboard/src/components/UserDialog/index.tsx b/app/dashboard/src/pages/users/dialog/index.tsx
similarity index 93%
rename from app/dashboard/src/components/UserDialog/index.tsx
rename to app/dashboard/src/pages/users/dialog/index.tsx
index eb98f20..af6c13e 100644
--- a/app/dashboard/src/components/UserDialog/index.tsx
+++ b/app/dashboard/src/pages/users/dialog/index.tsx
@@ -31,20 +31,20 @@ import {
User,
UserCreate,
} from 'types/User';
-import { UsageFilter, createUsageConfig } from '../UsageFilter';
-import { AddIcon, EditIcon } from 'components/Dialog/Icons';
-import { DialogModalHeader } from 'components/Dialog/ModalHeader';
-import { DialogModalFooter } from 'components/Dialog/ModalFooter';
-import { DeleteIcon } from 'components/Dialog/Icons';
-import { UserUsageIcon } from './UserDialogIcons';
-import { getDefaultValues } from './DefaultValues';
-import { schema, FormType } from './FormSchema';
-import { ServicesField } from './ServicesField';
-import { UsernameField } from './UsernameField';
-import { DataLimitField } from './DataLimitField';
-import { PeriodicUsageReset as PeriodicUsageReset } from './PeriodicUsageReset';
-import { ExpireDateField } from './ExpireDateField';
-import { NoteField } from './NoteField';
+import { UsageFilter, createUsageConfig } from 'components/UsageFilter';
+import { AddIcon, EditIcon } from 'components/dialog/Icons';
+import { DialogModalHeader } from 'components/dialog/ModalHeader';
+import { DialogModalFooter } from 'components/dialog/ModalFooter';
+import { DeleteIcon } from 'components/dialog/Icons';
+import { UserUsageIcon } from './icons';
+import { getDefaultValues } from './default-values';
+import { schema, FormType } from './form-schema';
+import { ServicesField } from './services-field';
+import { UsernameField } from './username-field';
+import { DataLimitField } from './data-limit-field';
+import { PeriodicUsageReset } from './periodic-usage-reset';
+import { ExpireDateField } from './expire-date-field';
+import { NoteField } from './note-field';
import { Service } from 'types/Service';
import { useUsers } from 'contexts/UsersContext';
import { useServices } from 'contexts/ServicesContext';
diff --git a/app/dashboard/src/components/UserDialog/NoteField.tsx b/app/dashboard/src/pages/users/dialog/note-field.tsx
similarity index 100%
rename from app/dashboard/src/components/UserDialog/NoteField.tsx
rename to app/dashboard/src/pages/users/dialog/note-field.tsx
diff --git a/app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx b/app/dashboard/src/pages/users/dialog/periodic-usage-reset.tsx
similarity index 100%
rename from app/dashboard/src/components/UserDialog/PeriodicUsageReset.tsx
rename to app/dashboard/src/pages/users/dialog/periodic-usage-reset.tsx
diff --git a/app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx b/app/dashboard/src/pages/users/dialog/service-property-status.tsx
similarity index 100%
rename from app/dashboard/src/components/UserDialog/ServicePropertyStatus.tsx
rename to app/dashboard/src/pages/users/dialog/service-property-status.tsx
diff --git a/app/dashboard/src/components/UserDialog/ServicesField.tsx b/app/dashboard/src/pages/users/dialog/services-field.tsx
similarity index 85%
rename from app/dashboard/src/components/UserDialog/ServicesField.tsx
rename to app/dashboard/src/pages/users/dialog/services-field.tsx
index 56a6746..30dbb66 100644
--- a/app/dashboard/src/components/UserDialog/ServicesField.tsx
+++ b/app/dashboard/src/pages/users/dialog/services-field.tsx
@@ -4,16 +4,16 @@ import { IconBaseStyle } from 'constants/IconBaseStyle';
import { TFunction } from 'i18next';
import { Controller } from 'react-hook-form';
import { Service } from 'types/Service';
-import { ServicePropertyStatus } from './ServicePropertyStatus';
-import { ServicesSelectList } from './ServicesSelectList';
+import { ServicePropertyStatus } from './service-property-status';
+import { ServicesSelectList } from './services-select-list';
const UsersServiceIcon = chakra(UsersIcon, IconBaseStyle);
const InboundsServiceIcon = chakra(ServerStackIcon, IconBaseStyle);
interface ServicesFieldProps {
- form: any;
- services: Service[];
- t: TFunction<'translation', undefined, 'translation'>;
+ form: any;
+ services: Service[];
+ t: TFunction<'translation', undefined, 'translation'>;
}
export const ServicesField = ({ form, t, services }: ServicesFieldProps) => {
@@ -51,8 +51,8 @@ export const ServicesField = ({ form, t, services }: ServicesFieldProps) => {
/>
<FormErrorMessage>
{t(
- form.formState.errors.services
- ?.message as string
+ form.formState.errors.services
+ ?.message as string
)}
</FormErrorMessage>
</FormControl>
diff --git a/app/dashboard/src/components/UserDialog/ServicesSelectList.tsx b/app/dashboard/src/pages/users/dialog/services-select-list.tsx
similarity index 91%
rename from app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
rename to app/dashboard/src/pages/users/dialog/services-select-list.tsx
index 336b39a..f320714 100644
--- a/app/dashboard/src/components/UserDialog/ServicesSelectList.tsx
+++ b/app/dashboard/src/pages/users/dialog/services-select-list.tsx
@@ -4,7 +4,7 @@ import {
Flex,
Stack,
} from '@chakra-ui/react';
-import { ListCheckBox } from 'components/ListCheckBox';
+import { ListCheckBox } from 'components/list-check-box';
import { forwardRef, ReactNode } from 'react';
import {
useFormContext,
@@ -12,8 +12,8 @@ import {
import { Service } from 'types/Service';
interface ServicesSelectListProps {
- list: Service[];
- renderCheckboxContent: (service: Service) => ReactNode;
+ list: Service[];
+ renderCheckboxContent: (service: Service) => ReactNode;
}
export const ServicesSelectList = forwardRef<HTMLDivElement, ServicesSelectListProps>(
diff --git a/app/dashboard/src/components/UserDialog/UsernameField.tsx b/app/dashboard/src/pages/users/dialog/username-field.tsx
similarity index 100%
rename from app/dashboard/src/components/UserDialog/UsernameField.tsx
rename to app/dashboard/src/pages/users/dialog/username-field.tsx
diff --git a/app/dashboard/src/components/UsersFilters.tsx b/app/dashboard/src/pages/users/filters.tsx
similarity index 92%
rename from app/dashboard/src/components/UsersFilters.tsx
rename to app/dashboard/src/pages/users/filters.tsx
index 59f7585..eb8c0f5 100644
--- a/app/dashboard/src/components/UsersFilters.tsx
+++ b/app/dashboard/src/pages/users/filters.tsx
@@ -45,9 +45,8 @@ const setSearchField = debounce((username: string) => {
}, 300);
export const UsersFilters: FC<FilterProps> = ({ ...props }) => {
- const { loading, } =
- useDashboard();
- const { usersFilters: filters, onFilterChange, refetchUsers, onCreateUser } = useUsers();
+ const { loading, } = useDashboard();
+ const { usersFilters: filters, onFilterChange, refetchUsers, onCreateUser, onResetAllUsage } = useUsers();
const { t } = useTranslation();
const [search, setSearch] = useState('');
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -123,6 +122,14 @@ export const UsersFilters: FC<FilterProps> = ({ ...props }) => {
})}
/>
</IconButton>
+ <Button
+ colorScheme="red"
+ size="sm"
+ onClick={() => onResetAllUsage(true)}
+ px={5}
+ >
+ {t('resetAllUsage')}
+ </Button>
<Button
colorScheme="primary"
size="sm"
diff --git a/app/dashboard/src/pages/users/index.tsx b/app/dashboard/src/pages/users/index.tsx
new file mode 100644
index 0000000..6fa5d9a
--- /dev/null
+++ b/app/dashboard/src/pages/users/index.tsx
@@ -0,0 +1,37 @@
+import { UserDialog } from './dialog';
+import { UsersTable } from './table';
+import { useDashboard } from 'contexts/DashboardContext';
+import { useEffect } from 'react';
+import { UsersFilters } from './filters';
+import {
+ ResetAllUsageModal,
+ DeleteUserModal,
+ QRCodeDialog,
+ ResetUserUsageModal,
+ RevokeSubscriptionModal
+} from './modals';
+import { pages } from 'constants/Pages';
+import { useUsers } from 'contexts/UsersContext';
+import Page from 'components/page';
+
+
+const UsersPage = () => {
+ useEffect(() => {
+ useDashboard.getState().activatePage(pages.findIndex((page) => page.path === '/users'));
+ useUsers.getState().refetchUsers();
+ }, []);
+ return (
+ <Page>
+ <UsersFilters />
+ <UsersTable />
+ <UserDialog />
+ <QRCodeDialog />
+ <DeleteUserModal />
+ <RevokeSubscriptionModal />
+ <ResetAllUsageModal />
+ <ResetUserUsageModal />
+ </Page>
+ )
+}
+
+export default UsersPage;
diff --git a/app/dashboard/src/pages/users/modals/delete-modal.tsx b/app/dashboard/src/pages/users/modals/delete-modal.tsx
new file mode 100644
index 0000000..2d8e418
--- /dev/null
+++ b/app/dashboard/src/pages/users/modals/delete-modal.tsx
@@ -0,0 +1,49 @@
+import {
+ useToast,
+} from '@chakra-ui/react';
+import { FC, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useUsers } from 'contexts/UsersContext';
+import { DeleteModal } from 'components/delete-modal';
+
+type DeleteUserModalProps = {
+ deleteCallback?: () => void;
+};
+
+export const DeleteUserModal: FC<DeleteUserModalProps> = () => {
+ const [loading, setLoading] = useState(false);
+ const { deletingUser, onDeletingUser, deleteUser } = useUsers();
+ const { t } = useTranslation();
+ const toast = useToast();
+ const onClose = () => {
+ onDeletingUser(null);
+ };
+ const onDelete = () => {
+ if (deletingUser) {
+ setLoading(true);
+ deleteUser(deletingUser)
+ .then(() => {
+ toast({
+ title: t('deleteUser.deleteSuccess', { username: deletingUser.username }),
+ status: 'success',
+ isClosable: true,
+ position: 'top',
+ duration: 3000,
+ });
+ })
+ .then(onClose)
+ .finally(setLoading.bind(null, false));
+ }
+ };
+ return (
+ <DeleteModal
+ onDelete={onDelete}
+ loading={loading}
+ isOpen={!!deletingUser}
+ onClose={onClose}
+ deletingObjectName={deletingUser?.username}
+ deleteObjectPromptT={'deleteUser.prompt'}
+ deleteObjectTitleT={'deleteUser.title'}
+ />
+ );
+};
diff --git a/app/dashboard/src/pages/users/modals/index.tsx b/app/dashboard/src/pages/users/modals/index.tsx
new file mode 100644
index 0000000..b90cacb
--- /dev/null
+++ b/app/dashboard/src/pages/users/modals/index.tsx
@@ -0,0 +1,5 @@
+export * from './reset-all-usage-modal';
+export * from './delete-modal';
+export * from './qrcode-modals';
+export * from './reset-user-usage-modal';
+export * from './revoke-subscription-modal';
diff --git a/app/dashboard/src/components/QRCodeDialog.tsx b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx
similarity index 99%
rename from app/dashboard/src/components/QRCodeDialog.tsx
rename to app/dashboard/src/pages/users/modals/qrcode-modals.tsx
index 1e0ee41..ce43006 100644
--- a/app/dashboard/src/components/QRCodeDialog.tsx
+++ b/app/dashboard/src/pages/users/modals/qrcode-modals.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick-theme.css';
import 'slick-carousel/slick/slick.css';
-import { Icon } from './Icon';
+import { Icon } from 'components/icon';
import { useUsers } from 'contexts/UsersContext';
const QRCode = chakra(QRCodeCanvas);
diff --git a/app/dashboard/src/components/ResetAllUsageModal.tsx b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
similarity index 97%
rename from app/dashboard/src/components/ResetAllUsageModal.tsx
rename to app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
index 6ffd39a..f9b177c 100644
--- a/app/dashboard/src/components/ResetAllUsageModal.tsx
+++ b/app/dashboard/src/pages/users/modals/reset-all-usage-modal.tsx
@@ -14,7 +14,7 @@ import {
} from '@chakra-ui/react';
import { FC, useState } from 'react';
import { DocumentMinusIcon } from '@heroicons/react/24/outline';
-import { Icon } from './Icon';
+import { Icon } from 'components/icon';
import { useTranslation } from 'react-i18next';
import { useUsers } from 'contexts/UsersContext';
@@ -25,7 +25,7 @@ export const ResetIcon = chakra(DocumentMinusIcon, {
},
});
-export type DeleteUserModalProps = {};
+type DeleteUserModalProps = {};
export const ResetAllUsageModal: FC<DeleteUserModalProps> = () => {
const [loading, setLoading] = useState(false);
diff --git a/app/dashboard/src/components/ResetUserUsageModal.tsx b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
similarity index 95%
rename from app/dashboard/src/components/ResetUserUsageModal.tsx
rename to app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
index 1ff5bb1..5191f04 100644
--- a/app/dashboard/src/components/ResetUserUsageModal.tsx
+++ b/app/dashboard/src/pages/users/modals/reset-user-usage-modal.tsx
@@ -14,18 +14,18 @@ import {
} from '@chakra-ui/react';
import { FC, useState } from 'react';
import { ArrowPathIcon } from '@heroicons/react/24/outline';
-import { Icon } from './Icon';
+import { Icon } from 'components/icon';
import { useTranslation, Trans } from 'react-i18next';
import { useUsers } from 'contexts/UsersContext';
-export const ResetIcon = chakra(ArrowPathIcon, {
+const ResetIcon = chakra(ArrowPathIcon, {
baseStyle: {
w: 5,
h: 5,
},
});
-export type DeleteUserModalProps = {};
+type DeleteUserModalProps = {};
export const ResetUserUsageModal: FC<DeleteUserModalProps> = () => {
const [loading, setLoading] = useState(false);
diff --git a/app/dashboard/src/components/RevokeSubscriptionModal.tsx b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
similarity index 97%
rename from app/dashboard/src/components/RevokeSubscriptionModal.tsx
rename to app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
index 3d4c1b4..4c953b5 100644
--- a/app/dashboard/src/components/RevokeSubscriptionModal.tsx
+++ b/app/dashboard/src/pages/users/modals/revoke-subscription-modal.tsx
@@ -16,9 +16,9 @@ import { ArrowPathIcon } from '@heroicons/react/24/outline';
import { useUsers } from 'contexts/UsersContext';
import { FC, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
-import { Icon } from './Icon';
+import { Icon } from 'components/icon';
-export const ResetIcon = chakra(ArrowPathIcon, {
+const ResetIcon = chakra(ArrowPathIcon, {
baseStyle: {
w: 5,
h: 5,
diff --git a/app/dashboard/src/components/UsersTable/index.tsx b/app/dashboard/src/pages/users/table/index.tsx
similarity index 98%
rename from app/dashboard/src/components/UsersTable/index.tsx
rename to app/dashboard/src/pages/users/table/index.tsx
index 43b03b0..db0e18b 100644
--- a/app/dashboard/src/components/UsersTable/index.tsx
+++ b/app/dashboard/src/pages/users/table/index.tsx
@@ -27,10 +27,10 @@ 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 { OnlineBadge } from '../OnlineBadge';
-import { OnlineStatus } from '../OnlineStatus';
+import { OnlineBadge } from 'components/OnlineBadge';
+import { OnlineStatus } from 'components/OnlineStatus';
import { Pagination } from 'components/Table/Pagination';
-import { StatusBadge } from '../StatusBadge';
+import { StatusBadge } from 'components/StatusBadge';
import { EmptySection } from 'components/Table/EmptySection';
import {
AccordionArrowIcon,
@@ -41,7 +41,7 @@ import {
SubscriptionLinkIcon
} from 'components/Table/Icons';
import { Sort } from 'components/Table/Sort';
-import { UsageSlider, UsageSliderCompact } from './UsageSlider';
+import { UsageSlider, UsageSliderCompact } from './usage-slider';
import { StatusSortSelect } from 'components/Table/StatusSortSelect';
import { useUsers } from 'contexts/UsersContext';
diff --git a/app/dashboard/src/components/UsersTable/UsageSlider.tsx b/app/dashboard/src/pages/users/table/usage-slider.tsx
similarity index 100%
rename from app/dashboard/src/components/UsersTable/UsageSlider.tsx
rename to app/dashboard/src/pages/users/table/usage-slider.tsx
diff --git a/app/dashboard/src/utils/authStorage.ts b/app/dashboard/src/service/auth-storage.ts
similarity index 100%
rename from app/dashboard/src/utils/authStorage.ts
rename to app/dashboard/src/service/auth-storage.ts
diff --git a/app/dashboard/src/service/http.ts b/app/dashboard/src/service/http.ts
index bee1d42..f2268f1 100644
--- a/app/dashboard/src/service/http.ts
+++ b/app/dashboard/src/service/http.ts
@@ -1,5 +1,5 @@
import { FetchOptions, $fetch as ohMyFetch } from 'ohmyfetch';
-import { getAuthToken } from 'utils/authStorage';
+import { getAuthToken } from 'service/auth-storage';
export const $fetch = ohMyFetch.create({
baseURL: import.meta.env.VITE_BASE_API,
@@ -20,3 +20,11 @@ export const fetcher = <T = any>(
};
export const fetch = fetcher;
+
+export const fetchAdminLoader = () => {
+ return fetch('/admins', {
+ headers: {
+ Authorization: `Bearer ${getAuthToken()}`,
+ },
+ });
+};
diff --git a/app/dashboard/src/utils/react-query.ts b/app/dashboard/src/service/react-query.ts
similarity index 100%
rename from app/dashboard/src/utils/react-query.ts
rename to app/dashboard/src/service/react-query.ts
diff --git a/app/dashboard/src/types/Page.ts b/app/dashboard/src/types/Page.ts
index f054a70..608b210 100644
--- a/app/dashboard/src/types/Page.ts
+++ b/app/dashboard/src/types/Page.ts
@@ -1,9 +1,7 @@
-import { ReactElement } from 'react';
import { ChakraIcon } from 'types/ChakraIcon'
export interface PageType {
path: string;
name: string;
- element: ReactElement;
itemIcon: ChakraIcon;
}