diff --git a/package-lock.json b/package-lock.json index 6759923..39e4c8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "2.10.0(35)", + "version": "2.10.0(36)", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "2.10.0(35)", + "version": "2.10.0(36)", "license": "ISC", "dependencies": { "@chakra-ui/icons": "^2.0.18", diff --git a/package.json b/package.json index c86d05c..a31ca84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.10.0(35)", + "version": "2.10.0(36)", "description": "", "private": true, "main": "index.tsx", diff --git a/src/components/Containers/Card/CardHeader.tsx b/src/components/Containers/Card/CardHeader.tsx index 1d28e2b..ae58e1e 100644 --- a/src/components/Containers/Card/CardHeader.tsx +++ b/src/components/Containers/Card/CardHeader.tsx @@ -1,17 +1,57 @@ -import React from 'react'; -import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react'; +import React, { DOMAttributes } from 'react'; +import { + BackgroundProps, + Box, + EffectProps, + InteractivityProps, + LayoutProps, + PositionProps, + SpaceProps, + useColorModeValue, + useStyleConfig, + useToken, +} from '@chakra-ui/react'; -export interface CardHeaderProps extends LayoutProps, SpaceProps { - variant?: string; +export interface CardHeaderProps + extends LayoutProps, + SpaceProps, + BackgroundProps, + InteractivityProps, + PositionProps, + EffectProps, + DOMAttributes { + variant?: 'panel' | 'unstyled'; children: React.ReactNode; + icon?: React.ReactNode; + headerStyle?: { + color: string; + }; } -const _CardHeader: React.FC = ({ variant, children, ...rest }) => { - // @ts-ignore +const _CardHeader: React.FC = ({ + variant, + children, + icon, + headerStyle = { + color: 'blue', + }, + ...rest +}) => { + const iconBgcolor = useToken('colors', [`${headerStyle?.color}.500`, `${headerStyle?.color}.300`]); + const bgColor = useToken('colors', [`${headerStyle?.color}.50`, `${headerStyle?.color}.700`]); + const iconColor = useColorModeValue(iconBgcolor[0], iconBgcolor[1]); + const headerBgColor = useColorModeValue(bgColor[0], bgColor[1]); + const styles = useStyleConfig('CardHeader', { variant }); + // Pass the computed styles into the `__css` prop return ( - + + {icon ? ( + + {icon} + + ) : null} {children} ); diff --git a/src/components/Containers/Card/index.tsx b/src/components/Containers/Card/index.tsx index ab0c3ff..45dfdb1 100644 --- a/src/components/Containers/Card/index.tsx +++ b/src/components/Containers/Card/index.tsx @@ -1,22 +1,7 @@ import React from 'react'; -import { - BackgroundProps, - Box, - EffectProps, - InteractivityProps, - LayoutProps, - PositionProps, - SpaceProps, - useStyleConfig, -} from '@chakra-ui/react'; +import { BackgroundProps, Box, InteractivityProps, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react'; -export interface CardProps - extends LayoutProps, - SpaceProps, - BackgroundProps, - InteractivityProps, - PositionProps, - EffectProps { +export interface CardProps extends LayoutProps, SpaceProps, BackgroundProps, InteractivityProps { variant?: string; onClick?: () => void; className?: string; diff --git a/src/components/Containers/GraphStatDisplay/index.tsx b/src/components/Containers/GraphStatDisplay/index.tsx index 76a4b52..7cd431d 100644 --- a/src/components/Containers/GraphStatDisplay/index.tsx +++ b/src/components/Containers/GraphStatDisplay/index.tsx @@ -18,7 +18,7 @@ const GraphStatDisplay = ({ chart, title, explanation }: Props) => { return ( <> - + {title} diff --git a/src/components/Containers/Modal/ModalHeader/index.tsx b/src/components/Containers/Modal/ModalHeader/index.tsx index 0eb54c4..0eea853 100644 --- a/src/components/Containers/Modal/ModalHeader/index.tsx +++ b/src/components/Containers/Modal/ModalHeader/index.tsx @@ -1,17 +1,26 @@ import React from 'react'; -import { Flex, LayoutProps, ModalHeader as Header, SpaceProps, Spacer } from '@chakra-ui/react'; +import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react'; -export interface ModalHeaderProps extends LayoutProps, SpaceProps { +export interface ModalHeaderProps { title: string; - right?: React.ReactNode; + left?: React.ReactNode; + right: React.ReactNode; } -export const ModalHeader = ({ title, right }: ModalHeaderProps) => ( -
- +const _ModalHeader: React.FC = ({ title, left, right }) => { + const bg = useColorModeValue('blue.50', 'blue.700'); + + return ( +
{title} + {left ? ( + + {left} + + ) : null} {right} - -
-); +
+ ); +}; +export const ModalHeader = React.memo(_ModalHeader); diff --git a/src/components/Containers/SimpleIconStatDisplay/index.tsx b/src/components/Containers/SimpleIconStatDisplay/index.tsx index 06d4522..cccfbc8 100644 --- a/src/components/Containers/SimpleIconStatDisplay/index.tsx +++ b/src/components/Containers/SimpleIconStatDisplay/index.tsx @@ -15,19 +15,19 @@ const SimpleIconStatDisplay = ({ title, description, icon, value, color }: Props const bgColor = useColorModeValue(color[0], color[1]); return ( - - + + {value} - {title} + {title} - + ); diff --git a/src/components/GlobalSearchBar/index.tsx b/src/components/GlobalSearchBar/index.tsx index 3cb9fc3..74d7eb1 100644 --- a/src/components/GlobalSearchBar/index.tsx +++ b/src/components/GlobalSearchBar/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Tooltip, useColorModeValue } from '@chakra-ui/react'; +import { Tooltip, useColorMode, useColorModeValue } from '@chakra-ui/react'; import { AsyncSelect, ChakraStylesConfig, @@ -14,7 +14,9 @@ import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore'; import debounce from 'helpers/debounce'; import { getUsernameRadiusSessions } from 'hooks/Network/Radius'; -const chakraStyles: ChakraStylesConfig> = { +const chakraStyles: ( + colorMode: 'light' | 'dark', +) => ChakraStylesConfig> = (colorMode) => ({ dropdownIndicator: (provided) => ({ ...provided, width: '32px', @@ -26,8 +28,10 @@ const chakraStyles: ChakraStylesConfig ({ ...provided, width: '320px', + backgroundColor: colorMode === 'light' ? 'white' : 'gray.600', + borderRadius: '15px', }), -}; +}); interface SearchOption extends OptionBase { label: string; @@ -62,6 +66,7 @@ const asyncComponents = { }; const GlobalSearchBar = () => { + const { colorMode } = useColorMode(); const navigate = useNavigate(); const store = useControllerStore((state) => ({ searchSerialNumber: state.searchSerialNumber, @@ -125,6 +130,8 @@ const GlobalSearchBar = () => { [], ); + const styles = React.useMemo(() => chakraStyles(colorMode), [colorMode]); + return ( { > > name="global_search" - chakraStyles={chakraStyles} + chakraStyles={styles} closeMenuOnSelect placeholder="Search MACs or radius clients" components={asyncComponents} diff --git a/src/components/LanguageSwitcher/index.tsx b/src/components/LanguageSwitcher/index.tsx index 7c0d7d4..67b17ae 100644 --- a/src/components/LanguageSwitcher/index.tsx +++ b/src/components/LanguageSwitcher/index.tsx @@ -33,7 +33,14 @@ const LanguageSwitcher = () => { return ( - + Deutsche diff --git a/src/components/Modals/GenericModal/ModalHeader/index.tsx b/src/components/Modals/GenericModal/ModalHeader/index.tsx index 269abdd..0eea853 100644 --- a/src/components/Modals/GenericModal/ModalHeader/index.tsx +++ b/src/components/Modals/GenericModal/ModalHeader/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react'; +import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react'; export interface ModalHeaderProps { title: string; @@ -7,17 +7,20 @@ export interface ModalHeaderProps { right: React.ReactNode; } -const _ModalHeader: React.FC = ({ title, left, right }) => ( -
- +const _ModalHeader: React.FC = ({ title, left, right }) => { + const bg = useColorModeValue('blue.50', 'blue.700'); + + return ( +
{title} - - {left ?? null} - + {left ? ( + + {left} + + ) : null} {right} - -
-); - +
+ ); +}; export const ModalHeader = React.memo(_ModalHeader); diff --git a/src/hooks/Network/Radius.ts b/src/hooks/Network/Radius.ts index 0f0ad9b..28df1ba 100644 --- a/src/hooks/Network/Radius.ts +++ b/src/hooks/Network/Radius.ts @@ -1,4 +1,4 @@ -import { QueryFunctionContext, useQuery } from '@tanstack/react-query'; +import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { axiosGw } from 'constants/axiosInstances'; export type RadiusSession = { @@ -46,3 +46,22 @@ export const useGetStationRadiusSessions = ({ station }: { station: string }) => useQuery(['radius-sessions', 'station', station], getStationSessions, { staleTime: 1000 * 60, }); + +const disconnectRadiusSession = async (session: RadiusSession) => + axiosGw + .put(`radiusSessions/${session.serialNumber}?operation=coadm`, { + accountingSessionId: session.accountingSessionId, + accountingMultiSessionId: session.accountingMultiSessionId, + callingStationId: session.callingStationId, + }) + .then(() => session); + +export const useDisconnectRadiusSession = () => { + const queryClient = useQueryClient(); + + return useMutation(disconnectRadiusSession, { + onSuccess: (session) => { + queryClient.invalidateQueries(['radius-sessions', session.serialNumber]); + }, + }); +}; diff --git a/src/layout/Devices.tsx b/src/layout/Devices.tsx index 237204c..f26aec6 100644 --- a/src/layout/Devices.tsx +++ b/src/layout/Devices.tsx @@ -54,7 +54,7 @@ const SidebarDevices = () => { if (!getStats.data) return null; return ( - + - + {isCompact && } - {activeRoute} + {activeRoute} void; }; @@ -39,42 +17,64 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => { const { t } = useTranslation(); const activeTextColor = useColorModeValue('gray.700', 'white'); const inactiveTextColor = useColorModeValue('gray.600', 'gray.200'); - const inactiveIconColor = useColorModeValue('gray.100', 'gray.600'); + const activeBg = useColorModeValue('blue.50', 'blue.900'); + const hoverBg = useColorModeValue('blue.100', 'blue.800'); if (route.navButton) { return route.navButton(isActive, toggleSidebar, route) as JSX.Element; } return ( - + {isActive ? ( - + + + + + {route.icon(false)} + + + {t(route.name)} + + + + ) : ( - + + + + + {route.icon(false)} + + + {t(route.name)} + + + + )} ); diff --git a/src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx b/src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx new file mode 100644 index 0000000..78dc72f --- /dev/null +++ b/src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { Button, useColorModeValue } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { NavLink } from 'react-router-dom'; +import { SubRoute } from 'models/Routes'; + +type Props = { + isActive: (path: string) => boolean; + route: SubRoute; +}; + +const SubNavigationButton = ({ isActive, route }: Props) => { + const { t } = useTranslation(); + const activeTextColor = useColorModeValue('gray.700', 'white'); + const inactiveTextColor = useColorModeValue('gray.600', 'gray.200'); + const activeBg = useColorModeValue('blue.50', 'blue.900'); + const hoverBg = useColorModeValue('blue.100', 'blue.800'); + + const isCurrentlyActive = isActive(route.path); + + return ( + + + + ); +}; + +export default SubNavigationButton; diff --git a/src/layout/Sidebar/NestedNavButton/index.tsx b/src/layout/Sidebar/NestedNavButton/index.tsx new file mode 100644 index 0000000..f003e02 --- /dev/null +++ b/src/layout/Sidebar/NestedNavButton/index.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, + Flex, + Text, + useColorModeValue, +} from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import SubNavigationButton from './SubNavigationButton'; +import IconBox from 'components/Containers/IconBox'; +import { RouteGroup } from 'models/Routes'; + +const variantChange = '0.2s linear'; + +type Props = { + isActive: (path: string) => boolean; + route: RouteGroup; +}; + +const NestedNavButton = ({ isActive, route }: Props) => { + const { t } = useTranslation(); + const inactiveTextColor = useColorModeValue('gray.600', 'gray.200'); + const hoverBg = useColorModeValue('blue.100', 'blue.800'); + + return ( + + + + + {route.icon(false)} + + + {typeof route.name === 'string' ? t(route.name) : route.name(t)} + + + + + + + {route.children.map((subRoute) => ( + + ))} + + + + ); +}; + +export default NestedNavButton; diff --git a/src/layout/Sidebar/index.tsx b/src/layout/Sidebar/index.tsx index beb941e..c1efe04 100644 --- a/src/layout/Sidebar/index.tsx +++ b/src/layout/Sidebar/index.tsx @@ -12,11 +12,12 @@ import { Spacer, useBreakpoint, VStack, + Accordion, } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { v4 as uuid } from 'uuid'; import { NavLinkButton } from './NavLinkButton'; +import NestedNavButton from './NestedNavButton'; import { useAuth } from 'contexts/AuthProvider'; import { Route } from 'models/Routes'; @@ -60,14 +61,25 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre const sidebarContent = React.useMemo( () => ( <> - - {topNav ? topNav(isRouteActive, toggle) : null} - {routes - .filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? '')) - .map((route) => ( - - ))} - + + + {topNav ? topNav(isRouteActive, toggle) : null} + {routes + .filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? '')) + .map((route) => + route.children ? ( + + ) : ( + + ), + )} + + {children} @@ -117,7 +129,8 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre h="calc(100vh - 32px)" my="16px" ml="16px" - borderRadius="16px" + borderRadius="15px" + border="0.5px solid" > {brand} diff --git a/src/layout/index.tsx b/src/layout/index.tsx index 2fdb9d6..b00a7e7 100644 --- a/src/layout/index.tsx +++ b/src/layout/index.tsx @@ -10,7 +10,7 @@ import { Sidebar } from './Sidebar'; import darkLogo from 'assets/Logo_Dark_Mode.svg'; import lightLogo from 'assets/Logo_Light_Mode.svg'; import LanguageSwitcher from 'components/LanguageSwitcher'; -import { Route as RouteProps } from 'models/Routes'; +import { RouteName } from 'models/Routes'; import NotFoundPage from 'pages/NotFound'; import routes from 'router/routes'; @@ -22,18 +22,57 @@ const Layout = () => { document.documentElement.dir = 'ltr'; const activeRoute = React.useMemo(() => { - const route = routes.find( - (r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1], - ); + let name: RouteName = ''; + for (const route of routes) { + if (!route.children && route.path === location.pathname) { + name = route.navName ?? route.name; + break; + } + if (route.path?.includes('/:')) { + const routePath = route.path.split('/:')[0]; + const currPath = location.pathname.split('/'); + if (routePath && location.pathname.startsWith(routePath) && currPath.length === 3) { + name = route.navName ?? route.name; + break; + } + } + if (route.children) { + for (const child of route.children) { + if (child.path === location.pathname) { + name = child.navName ?? child.name; + break; + } + } + } + } - if (route) return route.navName ? t(route.navName) : t(route.name); + if (typeof name === 'function') return name(t); - return ''; + if (name.includes('PATH')) { + name = location.pathname.split('/')[location.pathname.split('/').length - 1] ?? ''; + } + + if (name.includes('RAW-')) name.replace('RAW-', ''); + + return t(name); }, [t, location.pathname]); - const getRoutes = (r: RouteProps[]) => - // @ts-ignore - r.map((route: RouteProps) => } key={uuid()} />); + const routeInstances = React.useMemo(() => { + const instances = []; + + for (const route of routes) { + // @ts-ignore + if (!route.children) instances.push(} key={route.id} />); + else { + for (const child of route.children) { + // @ts-ignore + instances.push(} key={child.id} />); + } + } + } + + return instances; + }, []); return ( <> @@ -59,9 +98,7 @@ const Layout = () => { } activeRoute={activeRoute} /> - - {[...getRoutes(routes as RouteProps[]), } key={uuid()} />]} - + {[...routeInstances, } key={uuid()} />]} ); diff --git a/src/models/Routes.ts b/src/models/Routes.ts index 15dda4d..b6b12e5 100644 --- a/src/models/Routes.ts +++ b/src/models/Routes.ts @@ -1,14 +1,53 @@ -import { ReactNode } from 'react'; +import React, { LazyExoticComponent } from 'react'; -export interface Route { +export type RouteName = string | ((t: (s: string) => string) => string); + +export type SubRoute = { + id: string; authorized: string[]; path: string; - name: string; - navName?: string; - icon: (active: boolean) => ReactNode; - navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode; + name: RouteName; + component: React.ReactElement | LazyExoticComponent>; + navName?: RouteName; + hidden?: boolean; + icon?: undefined; + navButton?: undefined; + isEntity?: undefined; + isCustom?: undefined; + children?: undefined; +}; + +export type RouteGroup = { + id: string; + authorized: string[]; + name: RouteName; + icon: (active: boolean) => React.ReactElement; + children: SubRoute[]; + hidden?: boolean; + path?: undefined; + navName?: undefined; + navButton?: undefined; + isEntity?: undefined; + isCustom?: undefined; +}; + +export type SingleRoute = { + id: string; + authorized: string[]; + path: string; + name: RouteName; + navName?: RouteName; + icon: (active: boolean) => React.ReactElement; + navButton?: ( + isActive: boolean, + toggleSidebar: () => void, + route: Route, + ) => React.ReactElement | LazyExoticComponent>; isEntity?: boolean; - component: unknown; + component: React.ReactElement | LazyExoticComponent>; hidden?: boolean; isCustom?: boolean; -} + children?: undefined; +}; + +export type Route = SingleRoute | RouteGroup; diff --git a/src/pages/Device/Details.tsx b/src/pages/Device/Details.tsx index 060244d..1a90648 100644 --- a/src/pages/Device/Details.tsx +++ b/src/pages/Device/Details.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Button, Grid, GridItem, Heading, Link, Spacer, useClipboard, useDisclosure } from '@chakra-ui/react'; -import { Eye, EyeSlash } from '@phosphor-icons/react'; +import { Eye, EyeSlash, ListBullets } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import ViewCapabilitiesModal from './ViewCapabilitiesModal'; import ViewConfigurationModal from './ViewConfigurationModal'; @@ -50,7 +50,13 @@ const DeviceDetails = ({ serialNumber }: Props) => { return ( - + } + > {t('common.details')} diff --git a/src/pages/Device/Notes.tsx b/src/pages/Device/Notes.tsx index 9577735..aa6908c 100644 --- a/src/pages/Device/Notes.tsx +++ b/src/pages/Device/Notes.tsx @@ -18,7 +18,7 @@ import { useToast, useBreakpoint, } from '@chakra-ui/react'; -import { Plus } from '@phosphor-icons/react'; +import { Note, Plus } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; @@ -26,7 +26,7 @@ import { CardHeader } from 'components/Containers/Card/CardHeader'; import { DataTable } from 'components/DataTables/DataTable'; import FormattedDate from 'components/InformationDisplays/FormattedDate'; import { useGetDevice, useUpdateDevice } from 'hooks/Network/Devices'; -import { Note } from 'models/Note'; +import { Note as TNote } from 'models/Note'; import { Column } from 'models/Table'; type Props = { @@ -87,7 +87,7 @@ const DeviceNotes = ({ serialNumber }: Props) => { [], ); - const columns: Column[] = React.useMemo( + const columns: Column[] = React.useMemo( () => [ { id: 'created', @@ -116,8 +116,8 @@ const DeviceNotes = ({ serialNumber }: Props) => { ); return ( - - + + }> {t('common.notes')} diff --git a/src/pages/Device/RadiusClients/ActionCell.tsx b/src/pages/Device/RadiusClients/ActionCell.tsx new file mode 100644 index 0000000..ea29a24 --- /dev/null +++ b/src/pages/Device/RadiusClients/ActionCell.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { HStack, IconButton, Tooltip, useToast } from '@chakra-ui/react'; +import { Eject, MagnifyingGlass } from '@phosphor-icons/react'; +import { useTranslation } from 'react-i18next'; +import { RadiusSession, useDisconnectRadiusSession } from 'hooks/Network/Radius'; +import { AxiosError } from 'models/Axios'; + +type Props = { + session: RadiusSession; + onAnalysisOpen: (mac: string) => (() => void) | undefined; +}; + +const DeviceRadiusActions = ({ session, onAnalysisOpen }: Props) => { + const { t } = useTranslation(); + const toast = useToast(); + const disconnectSession = useDisconnectRadiusSession(); + + const handleDisconnect = () => { + disconnectSession.mutate(session, { + onSuccess: () => { + toast({ + id: `radius-disconnect-success`, + title: t('common.success'), + description: t('controller.radius.disconnect_success'), + status: 'success', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + }, + onError: (e) => { + toast({ + id: `radius-disconnect-error`, + title: t('common.error'), + description: (e as AxiosError)?.response?.data?.ErrorDescription, + status: 'error', + duration: 5000, + isClosable: true, + position: 'top-right', + }); + }, + }); + }; + + const onOpen = onAnalysisOpen(session.callingStationId); + + return ( + + + } + onClick={handleDisconnect} + isLoading={disconnectSession.isLoading} + /> + + + } + onClick={onOpen} + isDisabled={!onOpen} + /> + + + ); +}; + +export default DeviceRadiusActions; diff --git a/src/pages/Device/RadiusClients/Modal.tsx b/src/pages/Device/RadiusClients/Modal.tsx new file mode 100644 index 0000000..483b741 --- /dev/null +++ b/src/pages/Device/RadiusClients/Modal.tsx @@ -0,0 +1,117 @@ +import * as React from 'react'; +import { Box, Grid, GridItem, Heading, Text, UseDisclosureReturn } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { ParsedAssociation } from '../WifiAnalysis/AssocationsTable'; +import { ParsedRadio } from '../WifiAnalysis/RadiosTable'; +import { RefreshButton } from 'components/Buttons/RefreshButton'; +import { Modal } from 'components/Modals/Modal'; +import { bytesString } from 'helpers/stringHelper'; +import { useGetMacOuis } from 'hooks/Network/Statistics'; + +type Props = { + data: { + radios: ParsedRadio[]; + associations: ParsedAssociation[]; + }; + modalProps: UseDisclosureReturn; + selectedClient?: string; + refresh: () => void; + isFetching: boolean; +}; + +const RadiusClientModal = ({ data, modalProps, selectedClient, refresh, isFetching }: Props) => { + const { t } = useTranslation(); + const getOuis = useGetMacOuis({ macs: data.associations.map((d) => d.station) }); + const ouiKeyValue = React.useMemo(() => { + if (!getOuis.data) return undefined; + const obj: Record = {}; + for (const oui of getOuis.data.tagList) { + obj[oui.tag] = oui.value; + } + return obj; + }, [data.associations, getOuis.data]); + + if (!selectedClient) return null; + + const correspondingAssociation = data.associations.find((a) => a.station === selectedClient); + + const vendor = correspondingAssociation?.station ? ouiKeyValue?.[correspondingAssociation?.station] : ''; + + return ( + } + > + + + + {t('analytics.band')} + + + {correspondingAssociation?.radio?.band} + + + {t('controller.wifi.vendor')} + + + {vendor && vendor.length > 0 ? vendor : t('common.unknown')} + + + {t('controller.wifi.mode')} + + + {correspondingAssociation?.mode.toUpperCase()} + + + RSSI + + + {correspondingAssociation?.rssi} db + + + {t('controller.wifi.rx_rate')} + + + {correspondingAssociation?.rxRate.toLocaleString()} + + + Rx + + + {correspondingAssociation?.rxBytes ? bytesString(correspondingAssociation.rxBytes) : 0} + + + Rx MCS + + + {correspondingAssociation?.rxMcs} + + + Rx NSS + + + {correspondingAssociation?.rxNss} + + + {t('controller.wifi.tx_rate')} + + + {correspondingAssociation?.txRate.toLocaleString()} + + + Tx + + + {correspondingAssociation?.txBytes ? bytesString(correspondingAssociation.txBytes) : 0} + + + + + ); +}; + +export default RadiusClientModal; diff --git a/src/pages/Device/RadiusClients/Table.tsx b/src/pages/Device/RadiusClients/Table.tsx index 9753292..1e1db49 100644 --- a/src/pages/Device/RadiusClients/Table.tsx +++ b/src/pages/Device/RadiusClients/Table.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Box } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; +import DeviceRadiusActions from './ActionCell'; import { DataGrid } from 'components/DataTables/DataGrid'; import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; import DataCell from 'components/TableCells/DataCell'; @@ -12,9 +13,10 @@ type Props = { sessions: RadiusSession[]; refetch: () => void; isFetching: boolean; + onAnalysisOpen: (mac: string) => (() => void) | undefined; }; -const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => { +const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching, onAnalysisOpen }: Props) => { const { t } = useTranslation(); const tableController = useDataGrid({ tableSettingsId: 'gateway.device_radius_sessions.table', @@ -24,6 +26,7 @@ const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => { desc: false, }, ], + defaultOrder: ['callingStationId', 'userName', 'sessionTime', 'inputOctets', 'outputOctets', 'actions'], }); const columns: DataGridColumn[] = React.useMemo( @@ -107,6 +110,17 @@ const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => { }, }, }, + { + id: 'actions', + header: '', + cell: ({ cell }) => , + meta: { + alwaysShow: true, + columnSelectorOptions: { + label: t('common.actions'), + }, + }, + }, ], [t], ); @@ -124,6 +138,8 @@ const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => { options={{ refetch, minimumHeight: '200px', + onRowClick: (session) => onAnalysisOpen(session.callingStationId), + showAsCard: true, }} /> ); diff --git a/src/pages/Device/RadiusClients/index.tsx b/src/pages/Device/RadiusClients/index.tsx index 1e01823..7b02182 100644 --- a/src/pages/Device/RadiusClients/index.tsx +++ b/src/pages/Device/RadiusClients/index.tsx @@ -1,31 +1,145 @@ import * as React from 'react'; -import { Box } from '@chakra-ui/react'; +import { Box, useDisclosure } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { ParsedAssociation } from '../WifiAnalysis/AssocationsTable'; +import { ParsedRadio } from '../WifiAnalysis/RadiosTable'; +import RadiusClientModal from './Modal'; import DeviceRadiusClientsTable from './Table'; -import { Card } from 'components/Containers/Card'; -import { CardBody } from 'components/Containers/Card/CardBody'; +import { compactSecondsToDetailed } from 'helpers/dateFormatting'; +import { parseDbm } from 'helpers/stringHelper'; import { useGetDeviceRadiusSessions } from 'hooks/Network/Radius'; +import { DeviceStatistics, useGetDeviceLastStats } from 'hooks/Network/Statistics'; + +const parseRadios = (t: (str: string) => string, data: DeviceStatistics) => { + const radios: ParsedRadio[] = []; + if (data.radios) { + for (let i = 0; i < data.radios.length; i += 1) { + const radio = data.radios[i]; + if (radio) { + radios.push({ + recorded: 0, + index: i, + band: radio.band?.[0], + deductedBand: radio.channel && radio.channel > 16 ? '5G' : '2G', + channel: radio.channel, + channelWidth: radio.channel_width, + noise: radio.noise ? parseDbm(radio.noise) : '-', + txPower: radio.tx_power ?? '-', + activeMs: compactSecondsToDetailed(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0, t), + busyMs: compactSecondsToDetailed(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0, t), + receiveMs: compactSecondsToDetailed(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0, t), + phy: radio.phy, + }); + } + } + } + return radios; +}; + +const parseAssociations = (data: DeviceStatistics, radios: ParsedRadio[]) => { + const associations: ParsedAssociation[] = []; + + for (const interfaceObj of data.interfaces ?? []) { + for (const ssid of interfaceObj.ssids ?? []) { + let radio: ParsedRadio | undefined; + + if (ssid.phy) { + const foundRadio = radios.find((r) => r.phy === ssid.phy); + if (foundRadio) radio = foundRadio; + } + if (!radio) { + const potentialIndex = ssid.radio?.$ref?.split('/').pop(); + if (potentialIndex) { + const foundRadio = radios[parseInt(potentialIndex, 10)]; + if (foundRadio) radio = foundRadio; + } + } + + for (const association of ssid.associations ?? []) { + const ips = interfaceObj.clients?.find(({ mac }) => mac === association.station); + + associations.push({ + radio, + ips: { + ipv4: ips?.ipv4_addresses ?? [], + ipv6: ips?.ipv6_addresses ?? [], + }, + station: association.station, + ssid: ssid.ssid, + rssi: association.rssi ? parseDbm(association.rssi) : '-', + mode: ssid.mode, + rxBytes: association.rx_bytes, + rxRate: association.rx_rate.bitrate, + rxMcs: association.rx_rate.mcs ?? '-', + rxNss: association.rx_rate.nss ?? '-', + txBytes: association.tx_bytes, + txRate: association.tx_rate.bitrate, + txMcs: association.tx_rate.mcs ?? '-', + txNss: association.tx_rate.nss ?? '-', + recorded: 0, + }); + } + } + } + return associations; +}; type Props = { serialNumber: string; }; const RadiusClientsCard = ({ serialNumber }: Props) => { + const { t } = useTranslation(); + const [selectedClient, setSelectedClient] = React.useState('22:07:ff:11:84:6f'); const getRadiusClients = useGetDeviceRadiusSessions({ serialNumber }); + const getStats = useGetDeviceLastStats({ serialNumber }); + const modalProps = useDisclosure(); + + const wifiAnalysisData = React.useMemo(() => { + if (!getRadiusClients.data || getRadiusClients.data.length === 0 || !getStats.data) + return { radios: [], associations: [] }; + + const data: { radios: ParsedRadio[]; associations: ParsedAssociation[] } = { radios: [], associations: [] }; + + data.radios = parseRadios(t, getStats.data); + data.associations = parseAssociations(getStats.data, data.radios).filter((a) => { + const found = getRadiusClients.data.find( + (c) => c.callingStationId.replaceAll('-', ':').toLowerCase() === a.station, + ); + return !!found; + }); + + return { ...data }; + }, [getStats.dataUpdatedAt, getRadiusClients.data]); + + const onOpen = (mac: string) => + wifiAnalysisData.associations.find((a) => a.station === mac.replaceAll('-', ':').toLowerCase()) + ? () => { + setSelectedClient(mac.replaceAll('-', ':').toLowerCase()); + modalProps.onOpen(); + } + : undefined; if (!getRadiusClients.data || getRadiusClients.data.length === 0) return null; return ( - - - - - - - + + + + + + ); }; diff --git a/src/pages/Device/StatisticsCard/index.tsx b/src/pages/Device/StatisticsCard/index.tsx index c8d321e..a30785b 100644 --- a/src/pages/Device/StatisticsCard/index.tsx +++ b/src/pages/Device/StatisticsCard/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Box, Center, Flex, Heading, HStack, Select, Spacer, Spinner } from '@chakra-ui/react'; +import { ChartLine } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { v4 as uuid } from 'uuid'; import StatisticsCardDatePickers from './DatePickers'; @@ -47,33 +48,36 @@ const DeviceStatisticsCard = ({ serialNumber }: Props) => { return ( - - - {t('configurations.statistics')} - - - - - - - - + } + headerStyle={{ + color: 'green', + }} + > + {t('configurations.statistics')} + + + + + + + {time && ( diff --git a/src/pages/Device/Summary.tsx b/src/pages/Device/Summary.tsx index 384fd34..99d22d6 100644 --- a/src/pages/Device/Summary.tsx +++ b/src/pages/Device/Summary.tsx @@ -1,10 +1,25 @@ import * as React from 'react'; -import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react'; +import { + Alert, + AlertDescription, + AlertIcon, + Box, + Center, + Flex, + Grid, + GridItem, + Heading, + Image, + Tag, + useColorMode, +} from '@chakra-ui/react'; +import { Cloud } from '@phosphor-icons/react'; import ReactCountryFlag from 'react-country-flag'; import { useTranslation } from 'react-i18next'; import LocationDisplayButton from './LocationDisplayButton'; import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; +import { CardHeader } from 'components/Containers/Card/CardHeader'; import FormattedDate from 'components/InformationDisplays/FormattedDate'; import COUNTRY_LIST from 'constants/countryList'; import { compactDate, compactSecondsToDetailed } from 'helpers/dateFormatting'; @@ -20,6 +35,7 @@ type Props = { const DeviceSummary = ({ serialNumber }: Props) => { const { t } = useTranslation(); + const { colorMode } = useColorMode(); const getDevice = useGetDevice({ serialNumber }); const getStatus = useGetDeviceStatus({ serialNumber }); const getStats = useGetDeviceLastStats({ serialNumber }); @@ -47,11 +63,37 @@ const DeviceSummary = ({ serialNumber }: Props) => { }; return ( + } + > + {t('common.status')} + {getDevice?.data?.compatible} + {getDevice?.data?.compatible} +
+ + + {t('devices.no_model_image')} + +
+
+ } boxSize="220px" mr={4} /> diff --git a/src/pages/Device/WifiAnalysis/index.tsx b/src/pages/Device/WifiAnalysis/index.tsx index 7a5b3aa..2387aa5 100644 --- a/src/pages/Device/WifiAnalysis/index.tsx +++ b/src/pages/Device/WifiAnalysis/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Box, Flex, Heading, Slider, SliderFilledTrack, SliderThumb, SliderTrack } from '@chakra-ui/react'; +import { Box, Heading, Slider, SliderFilledTrack, SliderThumb, SliderTrack, Text } from '@chakra-ui/react'; +import { WifiHigh } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import WifiAnalysisAssocationsTable, { ParsedAssociation } from './AssocationsTable'; import WifiAnalysisRadioTable, { ParsedRadio } from './RadiosTable'; @@ -129,36 +130,41 @@ const WifiAnalysisCard = ({ serialNumber }: Props) => { return ( - - - - {t('controller.wifi.wifi_analysis')} - - {parsedData && ( - - - - - - - )} - + } + > + + {t('controller.wifi.wifi_analysis')} + - + + When:{' '} {parsedData && parsedData[sliderIndex]?.radios[0]?.recorded !== undefined ? ( // @ts-ignore ) : ( '-' )} - + + {parsedData && ( + + + + + + + )} + diff --git a/src/pages/Device/Wrapper.tsx b/src/pages/Device/Wrapper.tsx index f78f2de..06b3ace 100644 --- a/src/pages/Device/Wrapper.tsx +++ b/src/pages/Device/Wrapper.tsx @@ -8,7 +8,6 @@ import { AlertDialogOverlay, Box, Button, - Heading, HStack, Portal, Spacer, @@ -171,20 +170,19 @@ const DevicePageWrapper = ({ serialNumber }: Props) => { return ( <> {isCompact ? ( - - + + - {serialNumber} {getDevice.data?.simulated ? ( ) : null} {connectedTag} {healthTag} {restrictedTag} + - {getDevice?.data && ( { ) : ( - + - + - {serialNumber} {getDevice.data?.simulated ? ( ) : null} {connectedTag} {healthTag} {restrictedTag} + - {getDevice?.data && ( { const { t } = useTranslation(); const navigate = useNavigate(); - const [pageInfo, setPageInfo] = React.useState(undefined); const [device, setDevice] = React.useState(); - const [hiddenColumns, setHiddenColumns] = React.useState([]); const editModalProps = useDisclosure(); + const tableController = useDataGrid({ tableSettingsId: 'gateway.blacklist.table', defaultOrder: [] }); const getCount = useGetBlacklistCount({ enabled: true }); const getDevices = useGetBlacklistDevices({ - pageInfo, + pageInfo: { + limit: tableController.pageInfo.pageSize, + index: tableController.pageInfo.pageIndex, + }, enabled: true, }); @@ -52,111 +50,86 @@ const DeviceListCard = () => { [], ); - const columns: Column[] = React.useMemo( - (): Column[] => [ + const columns: DataGridColumn[] = React.useMemo( + (): DataGridColumn[] => [ { id: 'serialNumber', - Header: t('inventory.serial_number'), - Footer: '', - accessor: 'serialNumber', - Cell: (v) => serialCell(v.cell.row.original), - alwaysShow: true, - customMaxWidth: '200px', - customWidth: '130px', - customMinWidth: '130px', - disableSortBy: true, + header: t('inventory.serial_number'), + accessorKey: 'serialNumber', + cell: (v) => serialCell(v.cell.row.original), + enableSorting: false, + meta: { + alwaysShow: true, + customMaxWidth: '200px', + customWidth: '130px', + customMinWidth: '130px', + }, }, { id: 'created', - Header: t('controller.devices.added'), - Footer: '', - accessor: 'created', - Cell: (v) => dateCell(v.cell.row.original), - customMaxWidth: '200px', - customWidth: '130px', - customMinWidth: '130px', - disableSortBy: true, + header: t('controller.devices.added'), + accessorKey: 'created', + cell: (v) => dateCell(v.cell.row.original), + enableSorting: false, + meta: { + customMaxWidth: '200px', + customWidth: '130px', + customMinWidth: '130px', + }, }, { id: 'author', - Header: t('controller.devices.by'), - Footer: '', - accessor: 'author', - customMaxWidth: '200px', - customWidth: '130px', - customMinWidth: '130px', - disableSortBy: true, + header: t('controller.devices.by'), + accessorKey: 'author', + enableSorting: false, + meta: { + customMaxWidth: '200px', + customWidth: '130px', + customMinWidth: '130px', + }, }, { id: 'reason', - Header: t('controller.devices.reason'), - Footer: '', - accessor: 'reason', - disableSortBy: true, + header: t('controller.devices.reason'), + accessorKey: 'reason', + enableSorting: false, }, { id: 'actions', - Header: t('common.actions'), - Footer: '', - accessor: 'actions', - Cell: (v) => actionCell(v.cell.row.original), - customWidth: '50px', - alwaysShow: true, - disableSortBy: true, + header: t('common.actions'), + accessorKey: 'actions', + cell: (v) => actionCell(v.cell.row.original), + enableSorting: false, + meta: { + customWidth: '50px', + alwaysShow: true, + }, }, ], [t], ); return ( - <> - - - {getCount.data?.count} {t('devices.title')} - - - []} - hiddenColumns={hiddenColumns} - setHiddenColumns={setHiddenColumns} - preference="gateway.blacklist.table.hiddenColumns" - /> - - { - getDevices.refetch(); - getCount.refetch(); - }} - isFetching={getDevices.isFetching || getCount.isFetching} - isCompact - /> - - - - - - + + + controller={tableController} + header={{ + title: `${getCount.data?.count} ${t('devices.title')}`, + objectListed: t('devices.title'), + addButton: , + }} + columns={columns} + data={getDevices.data?.devices} + isLoading={getCount.isFetching || getDevices.isFetching} + options={{ + count: getCount.data?.count, + isManual: true, + onRowClick: (dev) => goToSerial(dev.serialNumber), + refetch: getDevices.refetch, + showAsCard: true, + }} + /> - + ); }; diff --git a/src/pages/Devices/Dashboard/MemorySimpleChart.tsx b/src/pages/Devices/Dashboard/MemorySimpleChart.tsx new file mode 100644 index 0000000..15eb0c6 --- /dev/null +++ b/src/pages/Devices/Dashboard/MemorySimpleChart.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Circuitry } from '@phosphor-icons/react'; +import { useTranslation } from 'react-i18next'; +import SimpleIconStatDisplay from 'components/Containers/SimpleIconStatDisplay'; +import { ControllerDashboardMemoryUsed } from 'hooks/Network/Controller'; + +type Props = { + data: ControllerDashboardMemoryUsed[]; +}; + +const MemorySimpleChart = ({ data }: Props) => { + const { t } = useTranslation(); + + const highMemoryDevices = React.useMemo( + () => + data.reduce((acc, curr) => { + if (curr.tag === '> 75%') return acc + curr.value; + return acc; + }, 0), + [data], + ); + + return ( + + ); +}; + +export default MemorySimpleChart; diff --git a/src/pages/Devices/Dashboard/OverallHealth.tsx b/src/pages/Devices/Dashboard/OverallHealth.tsx index 39064cd..43c03d8 100644 --- a/src/pages/Devices/Dashboard/OverallHealth.tsx +++ b/src/pages/Devices/Dashboard/OverallHealth.tsx @@ -34,7 +34,7 @@ const OverallHealthSimple = ({ data }: Props) => { let color: [string, string] = ['green.300', 'green.300']; let icon = Heart; - if (avg >= 80) { + if (avg >= 80 && avg < 100) { icon = Warning; color = ['yellow.300', 'yellow.300']; } else if (avg < 80) { diff --git a/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx b/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx index a641ef6..154347f 100644 --- a/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx +++ b/src/pages/Devices/Dashboard/OverallHealthPieChart.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { CopyIcon } from '@chakra-ui/icons'; import { - useColorMode, Alert, AlertDescription, AlertTitle, @@ -15,6 +14,7 @@ import { UnorderedList, useClipboard, Tooltip as ChakraTooltip, + useColorMode, useDisclosure, } from '@chakra-ui/react'; import { diff --git a/src/pages/Devices/Dashboard/index.tsx b/src/pages/Devices/Dashboard/index.tsx index ba3a16c..94363de 100644 --- a/src/pages/Devices/Dashboard/index.tsx +++ b/src/pages/Devices/Dashboard/index.tsx @@ -1,6 +1,19 @@ import * as React from 'react'; -import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Center, Spacer, Spinner } from '@chakra-ui/react'; -import { Clock, WifiHigh } from '@phosphor-icons/react'; +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, + Box, + Center, + Flex, + Heading, + Spacer, + Spinner, + Text, + Tooltip, +} from '@chakra-ui/react'; +import { Info, WifiHigh } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import Masonry from 'react-masonry-css'; import AssociationsPieChart from './AssociationsPieChart'; @@ -9,12 +22,13 @@ import CommandsBarChart from './CommandsBarChart'; import ConnectedPieChart from './ConnectedPieChart'; import DeviceTypesPieChart from './DeviceTypesPieChart'; import MemoryBarChart from './MemoryBarChart'; +import MemorySimpleChart from './MemorySimpleChart'; import OverallHealthSimple from './OverallHealth'; import OverallHealthPieChart from './OverallHealthPieChart'; import UptimesBarChart from './UptimesBarChart'; import VendorBarChart from './VendorBarChart'; import { RefreshButton } from 'components/Buttons/RefreshButton'; -import { CardBody } from 'components/Containers/Card/CardBody'; +import { Card } from 'components/Containers/Card'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import SimpleIconStatDisplay from 'components/Containers/SimpleIconStatDisplay'; import FormattedDate from 'components/InformationDisplays/FormattedDate'; @@ -25,75 +39,75 @@ const DevicesDashboard = () => { const getDashboard = useGetControllerDashboard(); return ( <> - - - - - - - <> - {getDashboard.isLoading && ( -
- -
- )} - {getDashboard.error && ( -
- - - - {t('controller.dashboard.error_fetching')} - { - // @ts-ignore - {getDashboard.error?.response?.data?.ErrorDescription} - } - - -
- )} - {getDashboard.data && ( - - } - description={t('controller.dashboard.last_ping_explanation')} - icon={Clock} - color={['blue.300', 'blue.300']} - /> - - - - - - - - - - - - - )} - -
-
+ + + + {t('analytics.last_ping')} + + + + + + + + + + + + + <> + {getDashboard.isLoading && ( +
+ +
+ )} + {getDashboard.error && ( +
+ + + + {t('controller.dashboard.error_fetching')} + { + // @ts-ignore + {getDashboard.error?.response?.data?.ErrorDescription} + } + + +
+ )} + {getDashboard.data && ( + + + + + + + + + + + + + + + )} + +
); }; diff --git a/src/pages/Devices/ListCard/index.tsx b/src/pages/Devices/ListCard/index.tsx index 24fa4a6..3f225c6 100644 --- a/src/pages/Devices/ListCard/index.tsx +++ b/src/pages/Devices/ListCard/index.tsx @@ -22,7 +22,6 @@ import MESH from './icons/MESH.png'; import SWITCH from './icons/SWITCH.png'; import ProvisioningStatusCell from './ProvisioningStatusCell'; import DeviceUptimeCell from './Uptime'; -import { CardBody } from 'components/Containers/Card/CardBody'; import { DataGrid } from 'components/DataTables/DataGrid'; import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid'; import GlobalSearchBar from 'components/GlobalSearchBar'; @@ -690,7 +689,7 @@ const DeviceListCard = () => { }, [getAges, getDevices.data, getDevices.dataUpdatedAt]); return ( - + <> controller={tableController} header={{ @@ -712,6 +711,7 @@ const DeviceListCard = () => { getDevices.refetch(); getCount.refetch(); }, + showAsCard: true, }} /> @@ -723,7 +723,7 @@ const DeviceListCard = () => { {scriptModal.modal} - + ); }; diff --git a/src/pages/SystemPage/SystemTile/LoggingButton/Modal.tsx b/src/pages/EndpointsPage/SystemTile/LoggingButton/Modal.tsx similarity index 91% rename from src/pages/SystemPage/SystemTile/LoggingButton/Modal.tsx rename to src/pages/EndpointsPage/SystemTile/LoggingButton/Modal.tsx index b072936..1b77cfb 100644 --- a/src/pages/SystemPage/SystemTile/LoggingButton/Modal.tsx +++ b/src/pages/EndpointsPage/SystemTile/LoggingButton/Modal.tsx @@ -4,7 +4,6 @@ import { AlertDescription, AlertIcon, Box, - Button, Center, Heading, Select, @@ -18,11 +17,11 @@ import { useToast, } from '@chakra-ui/react'; import axios from 'axios'; -import { FloppyDisk } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import { v4 as uuid } from 'uuid'; -import { Modal } from '../../../../components/Modals/Modal'; +import { SaveButton } from 'components/Buttons/SaveButton'; import { LoadingOverlay } from 'components/LoadingOverlay'; +import { Modal } from 'components/Modals/Modal'; import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System'; type Props = { @@ -138,15 +137,7 @@ const SystemLoggingModal = ({ modalProps, endpoint, token }: Props) => { maxWidth: { sm: '600px', md: '600px', lg: '600px', xl: '600px' }, }} topRightButtons={ - + } > <> diff --git a/src/pages/SystemPage/SystemTile/LoggingButton/index.tsx b/src/pages/EndpointsPage/SystemTile/LoggingButton/index.tsx similarity index 100% rename from src/pages/SystemPage/SystemTile/LoggingButton/index.tsx rename to src/pages/EndpointsPage/SystemTile/LoggingButton/index.tsx diff --git a/src/pages/SystemPage/SystemTile/SystemCertificatesTable.tsx b/src/pages/EndpointsPage/SystemTile/SystemCertificatesTable.tsx similarity index 100% rename from src/pages/SystemPage/SystemTile/SystemCertificatesTable.tsx rename to src/pages/EndpointsPage/SystemTile/SystemCertificatesTable.tsx diff --git a/src/pages/SystemPage/SystemTile/index.tsx b/src/pages/EndpointsPage/SystemTile/index.tsx similarity index 89% rename from src/pages/SystemPage/SystemTile/index.tsx rename to src/pages/EndpointsPage/SystemTile/index.tsx index 7c84ee0..f8c56be 100644 --- a/src/pages/SystemPage/SystemTile/index.tsx +++ b/src/pages/EndpointsPage/SystemTile/index.tsx @@ -1,10 +1,5 @@ import React, { useState } from 'react'; import { - AlertDialog, - AlertDialogBody, - AlertDialogContent, - AlertDialogHeader, - AlertDialogOverlay, Box, Button, Flex, @@ -16,15 +11,17 @@ import { useDisclosure, VStack, } from '@chakra-ui/react'; -import { MultiValue, Select } from 'chakra-react-select'; import { ArrowsClockwise } from '@phosphor-icons/react'; +import { MultiValue, Select } from 'chakra-react-select'; import { useTranslation } from 'react-i18next'; +import { RefreshButton } from '../../../components/Buttons/RefreshButton'; import FormattedDate from '../../../components/InformationDisplays/FormattedDate'; import SystemLoggingButton from './LoggingButton'; import SystemCertificatesTable from './SystemCertificatesTable'; -import { RefreshButton } from 'components/Buttons/RefreshButton'; import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; +import { CardHeader } from 'components/Containers/Card/CardHeader'; +import { Modal } from 'components/Modals/Modal'; import { compactSecondsToDetailed } from 'helpers/dateFormatting'; import { EndpointApiResponse } from 'hooks/Network/Endpoints'; import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System'; @@ -66,13 +63,13 @@ const SystemTile = ({ endpoint, token }: Props) => { return ( <> - - + + {endpoint.type} - +
@@ -180,16 +177,16 @@ const SystemTile = ({ endpoint, token }: Props) => {
- - - - {t('certificates.title')} - - - - - - + + + ); }; diff --git a/src/pages/EndpointsPage/index.tsx b/src/pages/EndpointsPage/index.tsx new file mode 100644 index 0000000..844e42b --- /dev/null +++ b/src/pages/EndpointsPage/index.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { SimpleGrid, Spacer } from '@chakra-ui/react'; +import { v4 as uuid } from 'uuid'; +import { RefreshButton } from '../../components/Buttons/RefreshButton'; +import SystemTile from './SystemTile'; +import { Card } from 'components/Containers/Card'; +import { CardHeader } from 'components/Containers/Card/CardHeader'; +import { axiosSec } from 'constants/axiosInstances'; +import { useAuth } from 'contexts/AuthProvider'; +import { useGetEndpoints } from 'hooks/Network/Endpoints'; + +type EndpointsPageProps = { + isOnlySec?: boolean; +}; + +const EndpointsPage = ({ isOnlySec }: EndpointsPageProps) => { + const { token } = useAuth(); + const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} }); + + const endpointsList = React.useMemo(() => { + if (!token || (!isOnlySec && !endpoints)) return null; + + const endpointList = endpoints ? [...endpoints] : []; + endpointList.push({ + uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', + type: isOnlySec ? '' : 'owsec', + id: 0, + vendor: 'owsec', + authenticationType: '', + }); + + return endpointList + .sort((a, b) => { + if (a.type < b.type) return -1; + if (a.type > b.type) return 1; + return 0; + }) + .map((endpoint) => ); + }, [endpoints, token]); + + return ( + <> + + + + + + + + {endpointsList} + + + ); +}; + +export default EndpointsPage; diff --git a/src/pages/Firmware/Dashboard/EndpointsDisplay.tsx b/src/pages/Firmware/Dashboard/EndpointsDisplay.tsx index f143a07..ffa1ed2 100644 --- a/src/pages/Firmware/Dashboard/EndpointsDisplay.tsx +++ b/src/pages/Firmware/Dashboard/EndpointsDisplay.tsx @@ -15,7 +15,7 @@ const FirmwareDashboardEndpointDisplay = ({ data }: Props) => { const { t } = useTranslation(); return ( - + {t('controller.firmware.endpoints')} diff --git a/src/pages/Firmware/Dashboard/index.tsx b/src/pages/Firmware/Dashboard/index.tsx index 7470c57..eac2328 100644 --- a/src/pages/Firmware/Dashboard/index.tsx +++ b/src/pages/Firmware/Dashboard/index.tsx @@ -1,6 +1,19 @@ import * as React from 'react'; -import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Center, Spacer, Spinner } from '@chakra-ui/react'; -import { Clock, WifiHigh } from '@phosphor-icons/react'; +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, + Box, + Center, + Flex, + Heading, + Spacer, + Spinner, + Text, + Tooltip, +} from '@chakra-ui/react'; +import { Info, WifiHigh } from '@phosphor-icons/react'; import { useTranslation } from 'react-i18next'; import Masonry from 'react-masonry-css'; import AverageFirmwareAge from './AverageFirmwareAge'; @@ -12,7 +25,7 @@ import OuisBarChart from './OuisBarChart'; import UnknownFirmwareBarChart from './UnknownFirmwareBarChart'; import UpToDateDevicesSimple from './UpToDateDevices'; import { RefreshButton } from 'components/Buttons/RefreshButton'; -import { CardBody } from 'components/Containers/Card/CardBody'; +import { Card } from 'components/Containers/Card'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import SimpleIconStatDisplay from 'components/Containers/SimpleIconStatDisplay'; import FormattedDate from 'components/InformationDisplays/FormattedDate'; @@ -23,73 +36,72 @@ const FirmwareDashboard = () => { const getDashboard = useGetFirmwareDashboard(); return ( <> - - - - - - - <> - {getDashboard.isLoading && ( -
- -
- )} - {getDashboard.error && ( -
- - - - {t('controller.dashboard.error_fetching')} - { - // @ts-ignore - {getDashboard.error?.response?.data?.ErrorDescription} - } - - -
- )} - {getDashboard.data && ( - - } - description={t('controller.dashboard.last_ping_explanation')} - icon={Clock} - color={['blue.300', 'blue.300']} - /> - - - - - - - - - - - )} - -
-
+ + + + {t('analytics.last_ping')} + + + + + + + + + + + + + <> + {getDashboard.isLoading && ( +
+ +
+ )} + {getDashboard.error && ( +
+ + + + {t('controller.dashboard.error_fetching')} + { + // @ts-ignore + {getDashboard.error?.response?.data?.ErrorDescription} + } + + +
+ )} + {getDashboard.data && ( + + + + + + + + + + + + )} + +
); }; diff --git a/src/pages/Firmware/List/index.tsx b/src/pages/Firmware/List/index.tsx index bbcbc82..2b25ce9 100644 --- a/src/pages/Firmware/List/index.tsx +++ b/src/pages/Firmware/List/index.tsx @@ -19,6 +19,7 @@ import FirmwareDetailsModal from './Modal'; import UpdateDbButton from './UpdateDbButton'; import UriCell from './UriCell'; import { RefreshButton } from 'components/Buttons/RefreshButton'; +import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import { DataTable } from 'components/DataTables/DataTable'; @@ -126,8 +127,8 @@ const FirmwareListTable = () => { }, [deviceType, getDeviceTypes]); return ( - <> - + + {t('analytics.firmware')} {getFirmware.data ? `(${getFirmware.data.length})` : ''} @@ -155,7 +156,7 @@ const FirmwareListTable = () => { /> - + @@ -171,7 +172,7 @@ const FirmwareListTable = () => { - + ); }; diff --git a/src/pages/Notifications/DeviceLogs/index.tsx b/src/pages/Notifications/DeviceLogs/index.tsx index 2c12261..ae1a7ef 100644 --- a/src/pages/Notifications/DeviceLogs/index.tsx +++ b/src/pages/Notifications/DeviceLogs/index.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import DeviceLogsSearchBar from './DeviceLogsSearchBar'; +import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import ShownLogsDropdown from 'components/ShownLogsDropdown'; @@ -106,8 +107,8 @@ const LogsCard = () => { ); return ( - <> - + + @@ -133,7 +134,7 @@ const LogsCard = () => { - + @@ -158,7 +159,7 @@ const LogsCard = () => { - + ); }; diff --git a/src/pages/Notifications/FmsLogs/index.tsx b/src/pages/Notifications/FmsLogs/index.tsx index e946de8..67bb5dc 100644 --- a/src/pages/Notifications/FmsLogs/index.tsx +++ b/src/pages/Notifications/FmsLogs/index.tsx @@ -20,6 +20,7 @@ import { useTranslation } from 'react-i18next'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { v4 as uuid } from 'uuid'; +import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import ShownLogsDropdown from 'components/ShownLogsDropdown'; @@ -121,8 +122,8 @@ const FmsLogsCard = () => { ); return ( - <> - + + { - +
@@ -182,7 +183,7 @@ const FmsLogsCard = () => { - + ); }; diff --git a/src/pages/Notifications/GeneralLogs/index.tsx b/src/pages/Notifications/GeneralLogs/index.tsx index ab90a11..db03209 100644 --- a/src/pages/Notifications/GeneralLogs/index.tsx +++ b/src/pages/Notifications/GeneralLogs/index.tsx @@ -20,6 +20,7 @@ import { useTranslation } from 'react-i18next'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { v4 as uuid } from 'uuid'; +import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import ShownLogsDropdown from 'components/ShownLogsDropdown'; @@ -121,8 +122,8 @@ const GeneralLogsCard = () => { ); return ( - <> - + + { - +
@@ -182,7 +183,7 @@ const GeneralLogsCard = () => { - + ); }; diff --git a/src/pages/Notifications/SecLogs/index.tsx b/src/pages/Notifications/SecLogs/index.tsx index 394c245..e87072b 100644 --- a/src/pages/Notifications/SecLogs/index.tsx +++ b/src/pages/Notifications/SecLogs/index.tsx @@ -20,6 +20,7 @@ import { useTranslation } from 'react-i18next'; import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { v4 as uuid } from 'uuid'; +import { Card } from 'components/Containers/Card'; import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import ShownLogsDropdown from 'components/ShownLogsDropdown'; @@ -121,8 +122,8 @@ const SecLogsCard = () => { ); return ( - <> - + + { - +
@@ -182,7 +183,7 @@ const SecLogsCard = () => { - + ); }; diff --git a/src/pages/Profile/ApiKeys/Table/index.tsx b/src/pages/Profile/ApiKeys/Table/index.tsx index 8cd1bff..3380be2 100644 --- a/src/pages/Profile/ApiKeys/Table/index.tsx +++ b/src/pages/Profile/ApiKeys/Table/index.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { Box, Flex, Heading, HStack, Spacer } from '@chakra-ui/react'; +import { Heading, HStack, Spacer } from '@chakra-ui/react'; import { useTranslation } from 'react-i18next'; import CreateApiKeyButton from './AddButton'; import useApiKeyTable from './useApiKeyTable'; import { RefreshButton } from 'components/Buttons/RefreshButton'; +import { CardBody } from 'components/Containers/Card/CardBody'; +import { CardHeader } from 'components/Containers/Card/CardHeader'; import { ColumnPicker } from 'components/DataTables/ColumnPicker'; import { DataTable } from 'components/DataTables/DataTable'; import { Column } from 'models/Table'; @@ -17,8 +19,8 @@ const ApiKeyTable = ({ userId }: Props) => { const { query, columns, hiddenColumns } = useApiKeyTable({ userId }); return ( - - + <> + {t('keys.other')} ({query.data?.apiKeys.length}) @@ -33,8 +35,8 @@ const ApiKeyTable = ({ userId }: Props) => { /> - - + + []} saveSettingsId="apiKeys.profile.table" @@ -46,8 +48,8 @@ const ApiKeyTable = ({ userId }: Props) => { showAllRows hideControls /> - - + + ); }; diff --git a/src/pages/Profile/ApiKeys/index.tsx b/src/pages/Profile/ApiKeys/index.tsx index fac9261..e4d0602 100644 --- a/src/pages/Profile/ApiKeys/index.tsx +++ b/src/pages/Profile/ApiKeys/index.tsx @@ -1,20 +1,14 @@ import * as React from 'react'; -import { Box } from '@chakra-ui/react'; import ApiKeyTable from './Table'; import { Card } from 'components/Containers/Card'; -import { CardBody } from 'components/Containers/Card/CardBody'; import { useAuth } from 'contexts/AuthProvider'; const ApiKeysCard = () => { const { user } = useAuth(); return ( - - - - - - + + ); }; diff --git a/src/pages/Profile/GeneralInformation.tsx b/src/pages/Profile/GeneralInformation.tsx index 901d817..1b5ad97 100644 --- a/src/pages/Profile/GeneralInformation.tsx +++ b/src/pages/Profile/GeneralInformation.tsx @@ -70,8 +70,8 @@ const GeneralInformationProfile = () => { }, [isEditing]); return ( - - + + {t('profile.your_profile')} diff --git a/src/pages/Profile/MultiFactorAuth/index.tsx b/src/pages/Profile/MultiFactorAuth/index.tsx index d4b94cb..222b66f 100644 --- a/src/pages/Profile/MultiFactorAuth/index.tsx +++ b/src/pages/Profile/MultiFactorAuth/index.tsx @@ -41,8 +41,8 @@ const MultiFactorAuthProfile = () => { }; return ( - - + + {t('account.mfa')} {currentMfaMethod ? t('profile.enabled').toUpperCase() : t('profile.disabled').toUpperCase()} diff --git a/src/pages/Profile/Notes.tsx b/src/pages/Profile/Notes.tsx index edfddc0..eb3973c 100644 --- a/src/pages/Profile/Notes.tsx +++ b/src/pages/Profile/Notes.tsx @@ -115,8 +115,8 @@ const ProfileNotes = () => { ); return ( - - + + {t('common.notes')} diff --git a/src/pages/Profile/Summary.tsx b/src/pages/Profile/Summary.tsx index 878e433..e1eaa97 100644 --- a/src/pages/Profile/Summary.tsx +++ b/src/pages/Profile/Summary.tsx @@ -12,7 +12,7 @@ const SummaryInformationProfile = () => { const { user, avatar } = useAuth(); return ( - + { columns={columns as Column[]} saveSettingsId="system.secrets.table" data={getSecrets.data ?? []} - obj={t('keys.other')} + obj={t('system.secrets')} sortBy={[{ id: 'key', desc: false }]} showAllRows hideControls diff --git a/src/pages/SystemPage/SystemSecrets/index.tsx b/src/pages/SystemConfigurationPage/index.tsx similarity index 63% rename from src/pages/SystemPage/SystemSecrets/index.tsx rename to src/pages/SystemConfigurationPage/index.tsx index 7a91006..9a94243 100644 --- a/src/pages/SystemPage/SystemSecrets/index.tsx +++ b/src/pages/SystemConfigurationPage/index.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { BackgroundProps, - Box, EffectProps, Heading, InteractivityProps, @@ -18,7 +17,7 @@ import { CardBody } from 'components/Containers/Card/CardBody'; import { CardHeader } from 'components/Containers/Card/CardHeader'; import { useAuth } from 'contexts/AuthProvider'; -export interface SystemSecretsCardProps +interface SystemConfigurationPageProps extends LayoutProps, SpaceProps, BackgroundProps, @@ -26,7 +25,7 @@ export interface SystemSecretsCardProps PositionProps, EffectProps {} -export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => { +const SystemConfigurationPage = ({ ...props }: SystemConfigurationPageProps) => { const { t } = useTranslation(); const { user } = useAuth(); @@ -35,19 +34,19 @@ export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => { } return ( - - - - - {t('system.secrets')} - - - - - - - - - + + + + {t('system.secrets')} + + + + + + + + ); }; + +export default SystemConfigurationPage; diff --git a/src/pages/SystemPage/index.tsx b/src/pages/SystemPage/index.tsx deleted file mode 100644 index 7c81f0e..0000000 --- a/src/pages/SystemPage/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react'; -import { useTranslation } from 'react-i18next'; -import { v4 as uuid } from 'uuid'; -import { RefreshButton } from '../../components/Buttons/RefreshButton'; -import { SystemSecretsCard } from './SystemSecrets'; -import SystemTile from './SystemTile'; -import { Card } from 'components/Containers/Card'; -import { CardHeader } from 'components/Containers/Card/CardHeader'; -import { axiosSec } from 'constants/axiosInstances'; -import { useAuth } from 'contexts/AuthProvider'; -import { useGetEndpoints } from 'hooks/Network/Endpoints'; - -const getDefaultTabIndex = () => { - const index = localStorage.getItem('system-tab-index') || '0'; - try { - return parseInt(index, 10); - } catch { - return 0; - } -}; -type Props = { - isOnlySec?: boolean; -}; - -const SystemPage = ({ isOnlySec }: Props) => { - const { t } = useTranslation(); - const { token, user } = useAuth(); - const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} }); - const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex()); - const handleTabChange = (index: number) => { - setTabIndex(index); - localStorage.setItem('system-tab-index', index.toString()); - }; - - const isRoot = user && user.userRole === 'root'; - - const endpointsList = React.useMemo(() => { - if (!token || (!isOnlySec && !endpoints)) return null; - - const endpointList = endpoints ? [...endpoints] : []; - endpointList.push({ - uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '', - type: isOnlySec ? '' : 'owsec', - id: 0, - vendor: 'owsec', - authenticationType: '', - }); - - return endpointList - .sort((a, b) => { - if (a.type < b.type) return -1; - if (a.type > b.type) return 1; - return 0; - }) - .map((endpoint) => ); - }, [endpoints, token]); - - return ( - - - - - {t('system.services')} - - - - - - - {!isOnlySec && ( - - - - - )} - - {endpointsList} - - - - - - - - - - - - ); -}; - -export default SystemPage; diff --git a/src/router/routes.tsx b/src/router/routes.tsx index fd1dbf7..f4f344b 100644 --- a/src/router/routes.tsx +++ b/src/router/routes.tsx @@ -1,101 +1,182 @@ import React from 'react'; -import { Icon } from '@chakra-ui/react'; import { Barcode, FloppyDisk, Info, ListBullets, TerminalWindow, UsersThree, WifiHigh } from '@phosphor-icons/react'; import { Route } from 'models/Routes'; const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations')); const DevicePage = React.lazy(() => import('pages/Device')); -const DevicesPage = React.lazy(() => import('pages/Devices')); -const FirmwarePage = React.lazy(() => import('pages/Firmware')); -const NotificationsPage = React.lazy(() => import('pages/Notifications')); +const DashboardPage = React.lazy(() => import('pages/Devices/Dashboard')); +const AllDevicesPage = React.lazy(() => import('pages/Devices/ListCard')); +const BlacklistPage = React.lazy(() => import('pages/Devices/Blacklist')); +const ControllerLogsPage = React.lazy(() => import('pages/Notifications/GeneralLogs')); +const DeviceLogsPage = React.lazy(() => import('pages/Notifications/DeviceLogs')); +const FmsLogsPage = React.lazy(() => import('pages/Notifications/FmsLogs')); +const SecLogsPage = React.lazy(() => import('pages/Notifications/SecLogs')); +const FirmwarePage = React.lazy(() => import('pages/Firmware/List')); +const FirmwareDashboard = React.lazy(() => import('pages/Firmware/Dashboard')); const ProfilePage = React.lazy(() => import('pages/Profile')); const ScriptsPage = React.lazy(() => import('pages/Scripts')); -const SystemPage = React.lazy(() => import('pages/SystemPage')); const UsersPage = React.lazy(() => import('pages/UsersPage')); +const EndpointsPage = React.lazy(() => import('pages/EndpointsPage')); +const SystemConfigurationPage = React.lazy(() => import('pages/SystemConfigurationPage')); const routes: Route[] = [ { + id: 'devices-group', authorized: ['root', 'partner', 'admin', 'csr', 'system'], - path: '/', name: 'devices.title', - icon: (active: boolean) => ( - - ), - component: DevicesPage, + icon: () => , + children: [ + { + id: 'devices-table', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/', + name: 'devices.all', + navName: 'devices.title', + component: AllDevicesPage, + }, + { + id: 'devices-dashboard', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/devices_dashboard', + name: 'analytics.dashboard', + component: DashboardPage, + }, + { + id: 'devices-blacklist', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/devices_blacklist', + name: 'controller.devices.blacklist', + component: BlacklistPage, + }, + ], }, { + id: 'firmware-group', authorized: ['root', 'partner', 'admin', 'csr', 'system'], - path: '/firmware', name: 'analytics.firmware', - icon: (active: boolean) => ( - - ), - component: FirmwarePage, + icon: () => , + children: [ + { + id: 'firmware-table', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/firmware', + name: 'devices.all', + navName: 'analytics.firmware', + component: FirmwarePage, + }, + { + id: 'firmware-dashboard', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/firmware/dashboard', + name: 'analytics.dashboard', + component: FirmwareDashboard, + }, + ], }, { + id: 'scripts', authorized: ['root'], path: '/scripts/:id', name: 'script.other', - icon: (active: boolean) => ( - - ), + icon: () => , component: ScriptsPage, }, { + id: 'configurations', authorized: ['root', 'partner', 'admin', 'csr', 'system'], path: '/configurations', name: 'configurations.title', - icon: (active: boolean) => ( - - ), + icon: () => , component: DefaultConfigurationsPage, }, { + id: 'logs-group', authorized: ['root', 'partner', 'admin', 'csr', 'system'], - path: '/logs', name: 'controller.devices.logs', - icon: (active: boolean) => ( - - ), - component: NotificationsPage, + icon: () => , + children: [ + { + id: 'logs-devices', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/logs/devices', + name: 'devices.title', + navName: (t) => `${t('devices.one')} ${t('controller.devices.logs')}`, + component: DeviceLogsPage, + }, + { + id: 'logs-controller', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/logs/controller', + name: 'simulation.controller', + navName: (t) => `${t('simulation.controller')} ${t('controller.devices.logs')}`, + component: ControllerLogsPage, + }, + { + id: 'logs-security', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/logs/security', + name: 'logs.security', + navName: (t) => `${t('logs.security')} ${t('controller.devices.logs')}`, + component: SecLogsPage, + }, + { + id: 'logs-firmware', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/logs/firmware', + name: 'logs.firmware', + navName: (t) => `${t('logs.firmware')} ${t('controller.devices.logs')}`, + component: FmsLogsPage, + }, + ], }, { + id: 'device-page', hidden: true, authorized: ['root', 'partner', 'admin', 'csr', 'system'], path: '/devices/:id', name: 'devices.one', - icon: (active: boolean) => ( - - ), + navName: 'PATH', + icon: () => , component: DevicePage, }, { + id: 'account-page', hidden: true, authorized: ['root', 'partner', 'admin', 'csr', 'system'], path: '/account', name: 'account.title', - icon: (active: boolean) => ( - - ), + icon: () => , component: ProfilePage, }, { + id: 'users-page', authorized: ['root', 'partner', 'admin', 'csr', 'system'], path: '/users', name: 'users.title', - icon: (active: boolean) => ( - - ), + icon: () => , component: UsersPage, }, { + id: 'system-group', authorized: ['root', 'partner', 'admin'], - path: '/system', name: 'system.title', - icon: (active: boolean) => ( - - ), - component: SystemPage, + icon: () => , + children: [ + { + id: 'system-services', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/services', + name: 'system.services', + component: EndpointsPage, + }, + { + id: 'system-configuration', + authorized: ['root', 'partner', 'admin', 'csr', 'system'], + path: '/systemConfiguration', + name: 'system.configuration', + component: SystemConfigurationPage, + }, + ], }, ]; diff --git a/src/theme/additions/card/Card.ts b/src/theme/additions/card/Card.ts index a506c39..5016b9e 100644 --- a/src/theme/additions/card/Card.ts +++ b/src/theme/additions/card/Card.ts @@ -1,6 +1,5 @@ const Card = { baseStyle: { - p: '22px', display: 'flex', flexDirection: 'column', width: '100%', @@ -15,12 +14,14 @@ const Card = { width: '100%', boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.05)', borderRadius: '15px', + border: '0.5px solid', }), widget: (props: { colorMode: string }) => ({ bg: props.colorMode === 'dark' ? 'gray.800' : 'gray.100', width: '100%', boxShadow: '0px 4px 6px rgba(0, 0, 0, 0.1)', borderRadius: '15px', + border: '0.5px solid', }), }, defaultProps: { diff --git a/src/theme/additions/card/CardBody.ts b/src/theme/additions/card/CardBody.ts index d89fe31..dcb570a 100644 --- a/src/theme/additions/card/CardBody.ts +++ b/src/theme/additions/card/CardBody.ts @@ -2,6 +2,8 @@ const CardBody = { baseStyle: { display: 'flex', width: '100%', + py: '12px', + px: '12px', }, }; diff --git a/src/theme/additions/card/CardHeader.ts b/src/theme/additions/card/CardHeader.ts index cad9830..7bebc7b 100644 --- a/src/theme/additions/card/CardHeader.ts +++ b/src/theme/additions/card/CardHeader.ts @@ -1,7 +1,23 @@ -const CardHeader = { +import { StyleConfig } from '@chakra-ui/theme-tools'; + +const CardHeader: StyleConfig = { baseStyle: { display: 'flex', width: '100%', + alignItems: 'center', + }, + variants: { + panel: () => ({ + borderBottom: '0.5px solid', + px: '12px', + minH: '58px', + borderTopLeftRadius: '15px', + borderTopRightRadius: '15px', + }), + unstyled: () => ({}), + }, + defaultProps: { + variant: 'panel', }, }; diff --git a/src/theme/additions/layout/MainPanel.ts b/src/theme/additions/layout/MainPanel.ts deleted file mode 100644 index 12535a1..0000000 --- a/src/theme/additions/layout/MainPanel.ts +++ /dev/null @@ -1,27 +0,0 @@ -const MainPanel = { - baseStyle: { - float: 'right', - maxWidth: '100%', - overflow: 'auto', - position: 'relative', - maxHeight: '100%', - transition: 'all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1)', - transitionDuration: '.2s, .2s, .35s', - transitionProperty: 'top, bottom, width', - transitionTimingFunction: 'linear, linear, ease', - }, - variants: { - main: () => ({ - float: 'right', - }), - }, - defaultProps: { - variant: 'main', - }, -}; - -export default { - components: { - MainPanel, - }, -}; diff --git a/src/theme/additions/layout/PanelContainer.ts b/src/theme/additions/layout/PanelContainer.ts deleted file mode 100644 index aea81e6..0000000 --- a/src/theme/additions/layout/PanelContainer.ts +++ /dev/null @@ -1,12 +0,0 @@ -const PanelContainer = { - baseStyle: { - p: '30px 10px 0px', - minHeight: 'calc(100vh - 123px)', - }, -}; - -export default { - components: { - PanelContainer, - }, -}; diff --git a/src/theme/additions/layout/PanelContent.ts b/src/theme/additions/layout/PanelContent.ts deleted file mode 100644 index 8f1acb4..0000000 --- a/src/theme/additions/layout/PanelContent.ts +++ /dev/null @@ -1,14 +0,0 @@ -const PanelContent = { - baseStyle: { - ms: 'auto', - me: 'auto', - ps: '15px', - pe: '15px', - }, -}; - -export default { - components: { - PanelContent, - }, -}; diff --git a/src/theme/components/button.ts b/src/theme/components/button.ts index 7602b9e..b876e0d 100644 --- a/src/theme/components/button.ts +++ b/src/theme/components/button.ts @@ -22,6 +22,9 @@ export default { boxShadow: 'none', }, }, + solid: { + border: '0.5px solid', + }, }, baseStyle: { borderRadius: '15px', diff --git a/src/theme/components/modal.ts b/src/theme/components/modal.ts new file mode 100644 index 0000000..c01ef28 --- /dev/null +++ b/src/theme/components/modal.ts @@ -0,0 +1,27 @@ +import { modalAnatomy as parts } from '@chakra-ui/anatomy'; +import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(parts.keys); + +const baseStyle = definePartsStyle({ + header: { + mb: 2, + borderBottom: '0.5px solid', + borderTopRadius: '15px', + py: 0, + minH: '58px', + justifyContent: 'center', + alignItems: 'center', + display: 'flex', + // A little bit of shadow under + boxShadow: 'sm', + }, + dialog: { + borderRadius: '15px', + border: '1px solid', + }, +}); + +export const modalTheme = defineMultiStyleConfig({ + baseStyle, +}); diff --git a/src/theme/components/tabs.ts b/src/theme/components/tabs.ts new file mode 100644 index 0000000..c42270b --- /dev/null +++ b/src/theme/components/tabs.ts @@ -0,0 +1,46 @@ +import { tabsAnatomy } from '@chakra-ui/anatomy'; +import { createMultiStyleConfigHelpers } from '@chakra-ui/react'; + +const { definePartsStyle, defineMultiStyleConfig } = createMultiStyleConfigHelpers(tabsAnatomy.keys); + +// define custom sizes +const sizes = { + md: definePartsStyle(({ colorMode }) => { + const isLight = colorMode === 'light'; + + return { + // define the parts that will change for each size + tab: { + fontSize: 'lg', + py: '2', + px: '4', + fontWeight: 'normal', + color: isLight ? 'gray.500' : 'gray.400', + borderWidth: '0.5px', + _selected: { + borderColor: 'unset', + textColor: isLight ? 'gray.800' : 'white', + fontWeight: 'semibold', + // borderTopRadius: '15px', + borderTopColor: isLight ? 'black' : 'white', + borderLeftColor: isLight ? 'black' : 'white', + borderRightColor: isLight ? 'black' : 'white', + marginTop: '-0.5px', + marginLeft: '-0.5px', + borderWidth: '0.5px', + borderBottom: '2px solid', + }, + }, + tablist: { + borderColor: 'black', + borderBottom: '0.5px solid black !important', + borderBottomWidth: '0.5px', + marginTop: '10px', + paddingLeft: '10px', + }, + }; + }), +}; + +// export the component theme +export const tabsTheme = defineMultiStyleConfig({ sizes }); diff --git a/src/theme/theme.ts b/src/theme/theme.ts index d2c5fc6..421b9c3 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -1,13 +1,12 @@ -import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react'; +import { extendTheme, type ThemeConfig, Tooltip } from '@chakra-ui/react'; import CardComponent from './additions/card/Card'; import CardBodyComponent from './additions/card/CardBody'; import CardHeaderComponent from './additions/card/CardHeader'; -import MainPanelComponent from './additions/layout/MainPanel'; -import PanelContainerComponent from './additions/layout/PanelContainer'; -import PanelContentComponent from './additions/layout/PanelContent'; import { Alert, Badge } from './components'; import buttonStyles from './components/button'; import drawerStyles from './components/drawer'; +import { modalTheme } from './components/modal'; +import { tabsTheme } from './components/tabs'; import breakpoints from './foundations/breakpoints'; import font from './foundations/fonts'; import globalStyles from './styles'; @@ -31,9 +30,8 @@ const theme = extendTheme({ Card: CardComponent.components.Card, CardBody: CardBodyComponent.components.CardBody, CardHeader: CardHeaderComponent.components.CardHeader, - MainPanel: MainPanelComponent.components.MainPanel, - PanelContainer: PanelContainerComponent.components.PanelContainer, - PanelContent: PanelContentComponent.components.PanelContent, + Modal: modalTheme, + Tabs: tabsTheme, }, });