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 (
- - } - /> - - - - -
- - - - - - - {pages.map((route: PageRoute, i) => - } loader={fetchAdminLoader} key={i} /> - - )} - - - -
); } 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 ( - - - - - - - - - - - - {t('deleteService.title')} - - {deletingService && ( - - }}> - {t('deleteService.prompt', { name: deletingService.name })} - - - )} - - - - - - - - ); -}; 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 = () => { - 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 ( - - - - - - - - - - - - {t('deleteUser.title')} - - {user && ( - - }}> - {t('deleteUser.prompt', { username: user.username })} - - - )} - - - - - - - - ); -}; 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 = (props) => { {version ? ` (v${version}), ` : ', '} Made in{' '} - khodedawosh + khodedawsh 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 = () => { - 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 ( - - - - - - - - - - {t('header.nodesUsage')} - - - - - - - { - setUsageFilter(filter); - fetchUsageWithFilter(query); - }} - /> - - }> - - - - - - - - - ); -}; 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 = ({ 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 ( - { - activatePage(index); - }} - > - - - - - - {t(name)} - - - - ); -} - -export const Sidebar = () => { - return ( - - ) -} 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 = ({ + onDelete, + isOpen, + onClose, + deletingObjectName, + loading, + deleteObjectTitleT, + deleteObjectPromptT +}) => { + const { t } = useTranslation(); return ( - + @@ -57,9 +50,9 @@ export const DeleteNodeModal: FC = () => { - {t('deleteNode.title')} + {t(deleteObjectTitleT)} - {deletingNode && ( + {deletingObjectName && ( { color="gray.600" > }}> - {t('deleteNode.prompt', { name: deletingNode.name })} + {t(deleteObjectPromptT, { name: deletingObjectName })} )} 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 = () => { 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 = () => { position="relative" > - } - onClick={onEditingHosts.bind(null, true)} - > - {t('header.hostSettings')} - - } - onClick={onShowingNodesUsage.bind(null, true)} - > - {t('header.nodesUsage')} - - } - onClick={onResetAllUsage.bind(null, true)} - > - {t('resetAllUsage')} - + {/* NOTE: Host settings trigger */} + {/* } */} + {/* onClick={onEditingHosts.bind(null, true)} */} + {/* > */} + {/* {t('header.hostSettings')} */} + {/* */} = () => { > - - - { - updateThemeColor(colorMode == 'dark' ? 'light' : 'dark'); - toggleColorMode(); - }} - > - {colorMode === 'light' ? : } - - + = () => { 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 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 = ({ children }) => { + return ( + + + {children} + + + ); +}; + +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 ( + + ) +} 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 ( + marzneshin + ) +} 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> = ({ titleT, children }) => { + const { t } = useTranslation(); + return ( + + + {t(titleT)} + {children} + + + ); +} 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 = ({ 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 ( + { + activatePage(index); + }} + > + + + + + + {t(name)} + + + + ); +} 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> = ({ + title, + content, + icon }) => { + return ( + + ); +}; 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> = ({ + title, + content, + icon }) => { + return ( + + ); +}; 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 = ({ icon, content, title, ...chakraProps }) => { + return ( + + + + {icon} + + + {title} + + + + {content} + + + ); +}; 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 ( + { + updateThemeColor(colorMode == 'dark' ? 'light' : 'dark'); + toggleColorMode(); + }} + > + {colorMode === 'light' ? : } + + ); +}; 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: , }, { name: 'pageHeaderTitle.Users', path: '/users', itemIcon: UsersSidebarItemIcon, - element: , }, { name: 'pageHeaderTitle.Services', path: '/services', itemIcon: ServicesSidebarItemIcon, - element: , }, { name: 'pageHeaderTitle.Nodes', path: '/nodes', itemIcon: NodesSidebarItemIcon, - element: , }, { name: 'pageHeaderTitle.Host', path: '/host', itemIcon: HostSidebarItemIcon, - element: , }, ]; - -export type PageRoute = Pick 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((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((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((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((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( - - - - - - - + + + + + ); 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(null); + const { collapseSidebar, expandSidebar } = useDashboard(); + + return ( + + + + + + + +
+ + + + +