[WIFI-12574] Theme improvements

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-05-03 09:57:44 +02:00
parent 5947f3362d
commit 130d71d5a0
70 changed files with 1514 additions and 778 deletions

4
package-lock.json generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.10.0(35)",
"version": "2.10.0(36)",
"description": "",
"private": true,
"main": "index.tsx",

View File

@@ -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<HTMLDivElement> {
variant?: 'panel' | 'unstyled';
children: React.ReactNode;
icon?: React.ReactNode;
headerStyle?: {
color: string;
};
}
const _CardHeader: React.FC<CardHeaderProps> = ({ variant, children, ...rest }) => {
// @ts-ignore
const _CardHeader: React.FC<CardHeaderProps> = ({
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 (
<Box __css={styles} {...rest}>
<Box __css={styles} bgColor={variant === 'unstyled' ? undefined : headerBgColor} {...rest}>
{icon ? (
<Box mr={2} color={headerStyle ? iconColor : undefined} bgColor="unset">
{icon}
</Box>
) : null}
{children}
</Box>
);

View File

@@ -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;

View File

@@ -18,7 +18,7 @@ const GraphStatDisplay = ({ chart, title, explanation }: Props) => {
return (
<>
<Card variant="widget" w="100%">
<Card>
<CardHeader>
<Heading mr={2} my="auto" size="md">
{title}

View File

@@ -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) => (
<Header>
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
const bg = useColorModeValue('blue.50', 'blue.700');
return (
<Header bg={bg}>
{title}
{left ? (
<HStack spacing={2} ml={2}>
{left}
</HStack>
) : null}
<Spacer />
{right}
</Flex>
</Header>
);
</Header>
);
};
export const ModalHeader = React.memo(_ModalHeader);

View File

@@ -15,19 +15,19 @@ const SimpleIconStatDisplay = ({ title, description, icon, value, color }: Props
const bgColor = useColorModeValue(color[0], color[1]);
return (
<Card variant="widget" w="100%" p={3}>
<Flex h="70px" w="100%">
<Card p={3} bgColor={bgColor}>
<Flex h="70px" w="100%" color="white">
<Flex direction="column" justifyContent="center">
<Heading size="lg">{value}</Heading>
<Heading size="sm" display="flex">
<Text opacity={0.8}>{title}</Text>
<Text>{title}</Text>
<Tooltip label={description} hasArrow>
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
</Tooltip>
</Heading>
</Flex>
<Spacer />
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" bgColor={bgColor} color="white" />
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" color="white" />
</Flex>
</Card>
);

View File

@@ -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<SearchOption, false, GroupBase<SearchOption>> = {
const chakraStyles: (
colorMode: 'light' | 'dark',
) => ChakraStylesConfig<SearchOption, false, GroupBase<SearchOption>> = (colorMode) => ({
dropdownIndicator: (provided) => ({
...provided,
width: '32px',
@@ -26,8 +28,10 @@ const chakraStyles: ChakraStylesConfig<SearchOption, false, GroupBase<SearchOpti
container: (provided) => ({
...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 (
<Tooltip
label={`Search serial numbers and radius clients. For radius clients you can either use the client's username (rad:client@client.com)
@@ -134,7 +141,7 @@ const GlobalSearchBar = () => {
>
<AsyncSelect<SearchOption, false, GroupBase<SearchOption>>
name="global_search"
chakraStyles={chakraStyles}
chakraStyles={styles}
closeMenuOnSelect
placeholder="Search MACs or radius clients"
components={asyncComponents}

View File

@@ -33,7 +33,14 @@ const LanguageSwitcher = () => {
return (
<Menu>
<Tooltip label={t('common.language')}>
<MenuButton background="transparent" as={IconButton} aria-label="Commands" icon={languageIcon} size="sm" />
<MenuButton
background="transparent"
variant="ghost"
as={IconButton}
aria-label="Commands"
icon={languageIcon}
size="sm"
/>
</Tooltip>
<MenuList>
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>

View File

@@ -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<ModalHeaderProps> = ({ title, left, right }) => (
<Header>
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
const bg = useColorModeValue('blue.50', 'blue.700');
return (
<Header bg={bg}>
{title}
<HStack spacing={2} ml={2}>
{left ?? null}
</HStack>
{left ? (
<HStack spacing={2} ml={2}>
{left}
</HStack>
) : null}
<Spacer />
{right}
</Flex>
</Header>
);
</Header>
);
};
export const ModalHeader = React.memo(_ModalHeader);

View File

@@ -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]);
},
});
};

View File

@@ -54,7 +54,7 @@ const SidebarDevices = () => {
if (!getStats.data) return null;
return (
<Card borderWidth="2px">
<Card p={4}>
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
<CircularProgress
isIndeterminate

View File

@@ -97,14 +97,22 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
ps="12px"
pt="8px"
top="15px"
w={isCompact ? '100%' : 'calc(100vw - 271px)'}
border={scrolled ? '0.5px solid' : undefined}
w={isCompact ? '100%' : 'calc(100% - 254px)'}
>
<Flex w="100%" flexDirection="row" alignItems="center">
<Flex
w="100%"
flexDirection="row"
alignItems="center"
justifyItems="center"
alignContent="center"
justifyContent="center"
>
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
<Heading>{activeRoute}</Heading>
<Heading size="lg">{activeRoute}</Heading>
<Tooltip label={t('common.go_back')}>
<IconButton
mt={2}
mt={1}
ml={4}
colorScheme="blue"
aria-label={t('common.go_back')}

View File

@@ -1,37 +1,15 @@
import React from 'react';
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import { AccordionButton, AccordionItem, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { NavLink } from 'react-router-dom';
import { v4 as uuid } from 'uuid';
import IconBox from 'components/Containers/IconBox';
import { Route } from 'models/Routes';
import { SingleRoute } from 'models/Routes';
const variantChange = '0.2s linear';
const commonStyle = {
boxSize: 'initial',
justifyContent: 'flex-start',
alignItems: 'center',
transition: variantChange,
bg: 'transparent',
ps: '6px',
py: '12px',
pe: '4px',
w: '100%',
borderRadius: '15px',
_active: {
bg: 'inherit',
transform: 'none',
borderColor: 'transparent',
},
_focus: {
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
},
} as const;
type Props = {
isActive: boolean;
route: Route;
route: SingleRoute;
toggleSidebar: () => 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 (
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
{isActive ? (
<Button {...commonStyle} boxShadow="none">
<Flex>
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
{route.icon(true)}
</IconBox>
<Text color={activeTextColor} my="auto" fontSize="md">
{t(route.name)}
</Text>
</Flex>
</Button>
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
<AccordionButton
px={1}
h={{
md: '40px',
lg: '50px',
}}
borderRadius="15px"
w="100%"
bg={activeBg}
_hover={{
bg: hoverBg,
}}
>
<Flex alignItems="center" w="100%">
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
{route.icon(false)}
</IconBox>
<Text color={activeTextColor} fontSize="md" fontWeight="bold">
{t(route.name)}
</Text>
</Flex>
</AccordionButton>
</AccordionItem>
) : (
<Button
{...commonStyle}
ps="6px"
_focus={{
boxShadow: 'none',
}}
>
<Flex>
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="6px" transition={variantChange}>
{route.icon(false)}
</IconBox>
<Text color={inactiveTextColor} my="auto" fontSize="sm">
{t(route.name)}
</Text>
</Flex>
</Button>
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
<AccordionButton
px={1}
h={{
md: '40px',
lg: '50px',
}}
borderRadius="15px"
w="100%"
_hover={{
bg: hoverBg,
}}
>
<Flex alignItems="center" w="100%">
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
{route.icon(false)}
</IconBox>
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold">
{t(route.name)}
</Text>
</Flex>
</AccordionButton>
</AccordionItem>
)}
</NavLink>
);

View File

@@ -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 (
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
<Button
w="100%"
justifyContent="left"
color={isCurrentlyActive ? activeTextColor : inactiveTextColor}
bg={isCurrentlyActive ? activeBg : 'transparent'}
_hover={{
bg: hoverBg,
}}
border="none"
>
{t(route.name)}
</Button>
</NavLink>
);
};
export default SubNavigationButton;

View File

@@ -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 (
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
<AccordionButton
px={1}
h={{
md: '40px',
lg: '50px',
}}
_hover={{
bg: hoverBg,
}}
borderRadius="15px"
w="100%"
>
<Flex alignItems="center" w="100%">
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
{route.icon(false)}
</IconBox>
<Text size="md" fontWeight="bold" color={inactiveTextColor}>
{typeof route.name === 'string' ? t(route.name) : route.name(t)}
</Text>
</Flex>
<AccordionIcon />
</AccordionButton>
<AccordionPanel pl="18px" paddingEnd={0} pr="-18px">
<Box pl={1} pr={-1} borderLeft="1px solid #63b3ed">
{route.children.map((subRoute) => (
<SubNavigationButton key={subRoute.path} route={subRoute} isActive={isActive} />
))}
</Box>
</AccordionPanel>
</AccordionItem>
);
};
export default NestedNavButton;

View File

@@ -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(
() => (
<>
<VStack spacing={2} alignItems="start" w="100%" px={4}>
{topNav ? topNav(isRouteActive, toggle) : null}
{routes
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
.map((route) => (
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
))}
</VStack>
<Accordion allowToggle>
<VStack spacing={2} alignItems="start" w="100%" px={4}>
{topNav ? topNav(isRouteActive, toggle) : null}
{routes
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
.map((route) =>
route.children ? (
<NestedNavButton key={route.id} isActive={isRouteActive} route={route} />
) : (
<NavLinkButton
key={route.id}
isActive={isRouteActive(route.path)}
route={route}
toggleSidebar={toggle}
/>
),
)}
</VStack>
</Accordion>
<Spacer />
<Box mb={2}>{children}</Box>
<Box>
@@ -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}
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">

View File

@@ -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) => <Route path={route.path} element={<route.component />} key={uuid()} />);
const routeInstances = React.useMemo(() => {
const instances = [];
for (const route of routes) {
// @ts-ignore
if (!route.children) instances.push(<Route path={route.path} element={<route.component />} key={route.id} />);
else {
for (const child of route.children) {
// @ts-ignore
instances.push(<Route path={child.path} element={<child.component />} key={child.id} />);
}
}
}
return instances;
}, []);
return (
<>
@@ -59,9 +98,7 @@ const Layout = () => {
</Sidebar>
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
<PageContainer waitForUser>
<Routes>
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
</Routes>
<Routes>{[...routeInstances, <Route path="*" element={<NotFoundPage />} key={uuid()} />]}</Routes>
</PageContainer>
</>
);

View File

@@ -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<React.ComponentType<unknown>>;
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<React.ComponentType<unknown>>;
isEntity?: boolean;
component: unknown;
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
hidden?: boolean;
isCustom?: boolean;
}
children?: undefined;
};
export type Route = SingleRoute | RouteGroup;

View File

@@ -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 (
<Card mb={4}>
<CardHeader mb={2}>
<CardHeader
mb={2}
headerStyle={{
color: 'purple',
}}
icon={<ListBullets weight="bold" size={20} />}
>
<Heading size="md">{t('common.details')}</Heading>
<Spacer />
<ViewCapabilitiesModal serialNumber={serialNumber} />

View File

@@ -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<Note>[] = React.useMemo(
const columns: Column<TNote>[] = React.useMemo(
() => [
{
id: 'created',
@@ -116,8 +116,8 @@ const DeviceNotes = ({ serialNumber }: Props) => {
);
return (
<Card mb={4} p={4}>
<CardHeader mb={2}>
<Card mb={4}>
<CardHeader icon={<Note weight="bold" size={20} />}>
<Heading size="md">{t('common.notes')}</Heading>
<Spacer />
<Popover trigger="click" placement="auto">

View File

@@ -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 (
<HStack>
<Tooltip label={t('controller.radius.disconnect')}>
<IconButton
aria-label={t('controller.radius.disconnect')}
size="sm"
colorScheme="red"
icon={<Eject size={20} />}
onClick={handleDisconnect}
isLoading={disconnectSession.isLoading}
/>
</Tooltip>
<Tooltip label={t('common.view_details')}>
<IconButton
aria-label={t('common.view_details')}
size="sm"
colorScheme="blue"
icon={<MagnifyingGlass size={20} />}
onClick={onOpen}
isDisabled={!onOpen}
/>
</Tooltip>
</HStack>
);
};
export default DeviceRadiusActions;

View File

@@ -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<string, string> = {};
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 (
<Modal
title={`${selectedClient}`}
{...modalProps}
options={{
modalSize: 'sm',
}}
topRightButtons={<RefreshButton onClick={refresh} isFetching={isFetching} />}
>
<Box>
<Grid templateColumns="repeat(3, 1fr)" gap={2}>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">{t('analytics.band')}</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.radio?.band}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">{t('controller.wifi.vendor')}</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{vendor && vendor.length > 0 ? vendor : t('common.unknown')}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">{t('controller.wifi.mode')}</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.mode.toUpperCase()}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">RSSI</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.rssi} db</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">{t('controller.wifi.rx_rate')}</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.rxRate.toLocaleString()}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">Rx</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.rxBytes ? bytesString(correspondingAssociation.rxBytes) : 0}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">Rx MCS</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.rxMcs}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">Rx NSS</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.rxNss}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">{t('controller.wifi.tx_rate')}</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.txRate.toLocaleString()}</Text>
</GridItem>
<GridItem w="100%" colSpan={1}>
<Heading size="sm">Tx</Heading>
</GridItem>
<GridItem w="100%" colSpan={2}>
<Text>{correspondingAssociation?.txBytes ? bytesString(correspondingAssociation.txBytes) : 0}</Text>
</GridItem>
</Grid>
</Box>
</Modal>
);
};
export default RadiusClientModal;

View File

@@ -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<RadiusSession>[] = React.useMemo(
@@ -107,6 +110,17 @@ const DeviceRadiusClientsTable = ({ sessions, refetch, isFetching }: Props) => {
},
},
},
{
id: 'actions',
header: '',
cell: ({ cell }) => <DeviceRadiusActions session={cell.row.original} onAnalysisOpen={onAnalysisOpen} />,
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,
}}
/>
);

View File

@@ -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<string>('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 (
<Card mb={4}>
<CardBody>
<Box w="100%">
<DeviceRadiusClientsTable
sessions={getRadiusClients.data}
refetch={getRadiusClients.refetch}
isFetching={getRadiusClients.isFetching}
/>
</Box>
</CardBody>
</Card>
<Box mb={4}>
<Box w="100%">
<DeviceRadiusClientsTable
sessions={getRadiusClients.data}
refetch={getRadiusClients.refetch}
isFetching={getRadiusClients.isFetching}
onAnalysisOpen={onOpen}
/>
</Box>
<RadiusClientModal
data={wifiAnalysisData}
modalProps={modalProps}
selectedClient={selectedClient}
refresh={getStats.refetch}
isFetching={getStats.isFetching}
/>
</Box>
);
};

View File

@@ -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 (
<Card mb={4}>
<CardHeader display="block">
<Flex>
<Heading size="md">{t('configurations.statistics')}</Heading>
<Spacer />
<HStack>
<Select value={selected} onChange={onSelectInterface}>
{parsedData?.interfaces
? Object.keys(parsedData.interfaces).map((v) => (
<option value={v} key={uuid()}>
{v}
</option>
))
: null}
<option value="memory">{t('statistics.memory')}</option>
</Select>
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ViewLastStatsModal serialNumber={serialNumber} />
<RefreshButton
size="sm"
onClick={refresh}
isCompact
isFetching={isLoading.isLoading}
// @ts-ignore
colorScheme="blue"
/>
</HStack>
</Flex>
<CardHeader
icon={<ChartLine weight="bold" size={20} />}
headerStyle={{
color: 'green',
}}
>
<Heading size="md">{t('configurations.statistics')}</Heading>
<Spacer />
<HStack>
<Select value={selected} onChange={onSelectInterface}>
{parsedData?.interfaces
? Object.keys(parsedData.interfaces).map((v) => (
<option value={v} key={uuid()}>
{v}
</option>
))
: null}
<option value="memory">{t('statistics.memory')}</option>
</Select>
<StatisticsCardDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ViewLastStatsModal serialNumber={serialNumber} />
<RefreshButton
size="sm"
onClick={refresh}
isCompact
isFetching={isLoading.isLoading}
// @ts-ignore
colorScheme="blue"
/>
</HStack>
</CardHeader>
<CardBody display="block" mb={2} minH="230px">
{time && (

View File

@@ -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 (
<Card mb={4}>
<CardHeader
headerStyle={{
color: 'blue',
}}
icon={<Cloud weight="bold" size={20} />}
>
<Heading size="md">{t('common.status')}</Heading>
</CardHeader>
<CardBody>
<Flex w="100%" alignItems="center">
<Image
src={`devices/${getDevice.data?.compatible}.png`}
alt={getDevice?.data?.compatible}
fallback={
<Box minW="220px" w="220px" h="220px" mr={4} display="flex">
<Image
src="devices/generic_ap.png"
alt={getDevice?.data?.compatible}
w="220px"
h="220px"
position="absolute"
filter={colorMode === 'dark' ? 'invert(1)' : undefined}
/>
<Center>
<Alert status="info" opacity={0.95} py={1} variant="solid">
<AlertIcon />
<AlertDescription fontSize="sm">{t('devices.no_model_image')}</AlertDescription>
</Alert>
</Center>
</Box>
}
boxSize="220px"
mr={4}
/>

View File

@@ -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 (
<Card mb={4}>
<CardHeader>
<Flex w="100%">
<Heading size="md" w="180px">
{t('controller.wifi.wifi_analysis')}
</Heading>
{parsedData && (
<Slider
step={1}
value={sliderIndex}
max={parsedData.length === 0 ? 0 : parsedData.length - 1}
onChange={onSliderChange}
focusThumbOnChange={false}
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
)}
</Flex>
<CardHeader
headerStyle={{
color: 'teal',
}}
icon={<WifiHigh size={20} />}
>
<Heading size="md" w="180px">
{t('controller.wifi.wifi_analysis')}
</Heading>
</CardHeader>
<CardBody display="block">
<Box>
<Text>
When:{' '}
{parsedData && parsedData[sliderIndex]?.radios[0]?.recorded !== undefined ? (
// @ts-ignore
<FormattedDate date={parsedData[sliderIndex]?.radios[0]?.recorded} />
) : (
'-'
)}
</Box>
</Text>
{parsedData && (
<Slider
step={1}
value={sliderIndex}
max={parsedData.length === 0 ? 0 : parsedData.length - 1}
onChange={onSliderChange}
focusThumbOnChange={false}
>
<SliderTrack>
<SliderFilledTrack />
</SliderTrack>
<SliderThumb />
</Slider>
)}
<Box />
<Box w="100%">
<WifiAnalysisRadioTable data={parsedData?.[sliderIndex]?.radios} />
<WifiAnalysisAssocationsTable data={parsedData?.[sliderIndex]?.associations} ouis={ouiKeyValue} />

View File

@@ -8,7 +8,6 @@ import {
AlertDialogOverlay,
Box,
Button,
Heading,
HStack,
Portal,
Spacer,
@@ -171,20 +170,19 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
return (
<>
{isCompact ? (
<Card p={2} mb={4}>
<CardHeader>
<Card mb={4}>
<CardHeader variant="unstyled" p="8px">
<HStack spacing={2}>
<Heading size="md">{serialNumber}</Heading>
{getDevice.data?.simulated ? (
<ResponsiveTag label={t('simulation.simulated')} colorScheme="purple" icon={Circuitry} />
) : null}
{connectedTag}
{healthTag}
{restrictedTag}
<GlobalSearchBar />
</HStack>
<Spacer />
<HStack spacing={2}>
<GlobalSearchBar />
<DeleteButton isCompact onClick={onDeleteOpen} />
{getDevice?.data && (
<DeviceActionDropdown
@@ -215,29 +213,28 @@ const DevicePageWrapper = ({ serialNumber }: Props) => {
</CardHeader>
</Card>
) : (
<Portal>
<Portal appendToParentPortal={false}>
<Card
p={2}
mb={4}
top="100px"
position="fixed"
w="calc(100vw - 271px)"
w="calc(100% - 255px)"
right={{ base: '0px', sm: '0px', lg: '20px' }}
boxShadow={boxShadow}
p="8px"
>
<CardHeader>
<CardHeader variant="unstyled">
<HStack spacing={2}>
<Heading size="md">{serialNumber}</Heading>
{getDevice.data?.simulated ? (
<ResponsiveTag label={t('simulation.simulated')} colorScheme="purple" icon={Circuitry} />
) : null}
{connectedTag}
{healthTag}
{restrictedTag}
<GlobalSearchBar />
</HStack>
<Spacer />
<HStack spacing={2}>
<GlobalSearchBar />
<DeleteButton isCompact onClick={onDeleteOpen} />
{getDevice?.data && (
<DeviceActionDropdown

View File

@@ -1,29 +1,27 @@
import * as React from 'react';
import { Box, Button, Heading, Spacer, useDisclosure } from '@chakra-ui/react';
import { Box, Button, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Actions from './Actions';
import CreateBlacklistModal from './CreateModal';
import EditBlacklistModal from './EditModal';
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 { DataGrid } from 'components/DataTables/DataGrid';
import { DataGridColumn, useDataGrid } from 'components/DataTables/DataGrid/useDataGrid';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { BlacklistDevice, useGetBlacklistCount, useGetBlacklistDevices } from 'hooks/Network/Blacklist';
import { Column, PageInfo } from 'models/Table';
const DeviceListCard = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [pageInfo, setPageInfo] = React.useState<PageInfo | undefined>(undefined);
const [device, setDevice] = React.useState<BlacklistDevice | undefined>();
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
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<BlacklistDevice>[] = React.useMemo(
(): Column<BlacklistDevice>[] => [
const columns: DataGridColumn<BlacklistDevice>[] = React.useMemo(
(): DataGridColumn<BlacklistDevice>[] => [
{
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 (
<>
<CardHeader px={4} pt={4}>
<Heading size="md" my="auto" mr={2}>
{getCount.data?.count} {t('devices.title')}
</Heading>
<Spacer />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="gateway.blacklist.table.hiddenColumns"
/>
<CreateBlacklistModal />
<RefreshButton
ml={2}
onClick={() => {
getDevices.refetch();
getCount.refetch();
}}
isFetching={getDevices.isFetching || getCount.isFetching}
isCompact
/>
</CardHeader>
<CardBody p={4}>
<Box overflowX="auto" w="100%">
<DataTable
columns={
columns as {
id: string;
Header: string;
Footer: string;
accessor: string;
}[]
}
data={getDevices.data?.devices ?? []}
isLoading={getCount.isFetching || getDevices.isFetching}
isManual
hiddenColumns={hiddenColumns}
obj={t('devices.title')}
count={getCount.data?.count || 0}
// @ts-ignore
setPageInfo={setPageInfo}
minHeight="300px"
saveSettingsId="gateway.blacklist.table"
/>
</Box>
</CardBody>
<Box>
<DataGrid<BlacklistDevice>
controller={tableController}
header={{
title: `${getCount.data?.count} ${t('devices.title')}`,
objectListed: t('devices.title'),
addButton: <CreateBlacklistModal />,
}}
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,
}}
/>
<EditBlacklistModal device={device} modalProps={editModalProps} />
</>
</Box>
);
};

View File

@@ -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 (
<SimpleIconStatDisplay
title="High Memory Devices (>90%)"
value={highMemoryDevices}
description={t('controller.dashboard.memory_explanation')}
icon={Circuitry}
color={highMemoryDevices === 0 ? ['teal.300', 'teal.300'] : ['red.300', 'red.300']}
/>
);
};
export default MemorySimpleChart;

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Spacer />
<RefreshButton isCompact onClick={getDashboard.refetch} isFetching={getDashboard.isFetching} />
</CardHeader>
<CardBody p={4}>
<Box display="block" w="100%">
<>
{getDashboard.isLoading && (
<Center my="100px">
<Spinner size="xl" />
</Center>
)}
{getDashboard.error && (
<Center my="100px">
<Alert status="error" mb={4} w="unset">
<AlertIcon />
<Box>
<AlertTitle>{t('controller.dashboard.error_fetching')}</AlertTitle>
{
// @ts-ignore
<AlertDescription>{getDashboard.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box>
</Alert>
</Center>
)}
{getDashboard.data && (
<Masonry
breakpointCols={{
default: 3,
1800: 3,
1400: 2,
1100: 1,
}}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
style={{
marginLeft: 'unset',
}}
>
<SimpleIconStatDisplay
title={t('analytics.last_ping')}
value={<FormattedDate date={getDashboard.data.snapshot ?? 0} />}
description={t('controller.dashboard.last_ping_explanation')}
icon={Clock}
color={['blue.300', 'blue.300']}
/>
<SimpleIconStatDisplay
title={t('devices.title')}
value={getDashboard.data?.numberOfDevices ?? 0}
description={t('controller.dashboard.devices_explanation')}
icon={WifiHigh}
color={['green.300', 'green.300']}
/>
<OverallHealthSimple data={getDashboard.data.healths} />
<ConnectedPieChart data={getDashboard.data} />
<OverallHealthPieChart data={getDashboard.data.healths} />
<UptimesBarChart data={getDashboard.data.upTimes} />
<VendorBarChart data={getDashboard.data.vendors} />
<AssociationsPieChart data={getDashboard.data.associations} />
<MemoryBarChart data={getDashboard.data.memoryUsed} />
<DeviceTypesPieChart data={getDashboard.data.deviceType} />
<CommandsBarChart data={getDashboard.data.commands} />
<CertificatesPieChart data={getDashboard.data.certificates} />
</Masonry>
)}
</>
</Box>
</CardBody>
<Card mb="20px">
<CardHeader variant="unstyled" px={4} py={2}>
<Flex alignItems="center">
<Heading size="md">{t('analytics.last_ping')}</Heading>
<Text ml={1} pt={0.5}>
<FormattedDate date={getDashboard.data?.snapshot ?? 0} key={getDashboard.dataUpdatedAt} />
</Text>
<Tooltip label={t('controller.dashboard.last_ping_explanation')} hasArrow>
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
</Tooltip>
</Flex>
<Spacer />
<RefreshButton isCompact onClick={getDashboard.refetch} isFetching={getDashboard.isFetching} />
</CardHeader>
</Card>
<Box display="block" w="100%">
<>
{getDashboard.isLoading && (
<Center my="100px">
<Spinner size="xl" />
</Center>
)}
{getDashboard.error && (
<Center my="100px">
<Alert status="error" mb={4} w="unset">
<AlertIcon />
<Box>
<AlertTitle>{t('controller.dashboard.error_fetching')}</AlertTitle>
{
// @ts-ignore
<AlertDescription>{getDashboard.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box>
</Alert>
</Center>
)}
{getDashboard.data && (
<Masonry
breakpointCols={{
default: 3,
1800: 3,
1400: 2,
800: 1,
}}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
<SimpleIconStatDisplay
title={t('devices.title')}
value={getDashboard.data?.numberOfDevices ?? 0}
description={t('controller.dashboard.devices_explanation')}
icon={WifiHigh}
color={['blue.300', 'blue.300']}
/>
<OverallHealthSimple data={getDashboard.data.healths} />
<MemorySimpleChart data={getDashboard.data.memoryUsed} />
<ConnectedPieChart data={getDashboard.data} />
<OverallHealthPieChart data={getDashboard.data.healths} />
<UptimesBarChart data={getDashboard.data.upTimes} />
<VendorBarChart data={getDashboard.data.vendors} />
<AssociationsPieChart data={getDashboard.data.associations} />
<MemoryBarChart data={getDashboard.data.memoryUsed} />
<DeviceTypesPieChart data={getDashboard.data.deviceType} />
<CommandsBarChart data={getDashboard.data.commands} />
<CertificatesPieChart data={getDashboard.data.certificates} />
</Masonry>
)}
</>
</Box>
</>
);
};

View File

@@ -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 (
<CardBody p={4}>
<>
<DataGrid<DeviceWithStatus>
controller={tableController}
header={{
@@ -712,6 +711,7 @@ const DeviceListCard = () => {
getDevices.refetch();
getCount.refetch();
},
showAsCard: true,
}}
/>
<WifiScanModal modalProps={scanModalProps} serialNumber={serialNumber} />
@@ -723,7 +723,7 @@ const DeviceListCard = () => {
<TelemetryModal modalProps={telemetryModalProps} serialNumber={serialNumber} />
<RebootModal modalProps={rebootModalProps} serialNumber={serialNumber} />
{scriptModal.modal}
</CardBody>
</>
);
};

View File

@@ -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={
<Button
colorScheme="blue"
rightIcon={<FloppyDisk size={20} />}
onClick={onUpdate}
isDisabled={newLevels.length === 0}
isLoading={updateLevels.isLoading}
>
{t('system.update_levels')} {newLevels.length > 0 ? newLevels.length : ''}
</Button>
<SaveButton onClick={onUpdate} isDisabled={newLevels.length === 0} isLoading={updateLevels.isLoading} />
}
>
<>

View File

@@ -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 (
<>
<Card variant="widget">
<Box display="flex" mb={2}>
<Card>
<CardHeader>
<Heading pt={0}>{endpoint.type}</Heading>
<Spacer />
<SystemLoggingButton endpoint={endpoint} token={token} />
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
</Box>
</CardHeader>
<CardBody>
<VStack w="100%">
<SimpleGrid minChildWidth="500px" w="100%">
@@ -180,16 +177,16 @@ const SystemTile = ({ endpoint, token }: Props) => {
</VStack>
</CardBody>
</Card>
<AlertDialog leastDestructiveRef={undefined} isOpen={isOpen} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader>{t('certificates.title')}</AlertDialogHeader>
<AlertDialogBody pb={6}>
<SystemCertificatesTable certificates={system?.certificates} />
</AlertDialogBody>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
<Modal
isOpen={isOpen}
onClose={onClose}
title={t('certificates.title')}
options={{
modalSize: 'sm',
}}
>
<SystemCertificatesTable certificates={system?.certificates} />
</Modal>
</>
);
};

View File

@@ -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) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
}, [endpoints, token]);
return (
<>
<Card mb="20px">
<CardHeader variant="unstyled" px={4} py={2}>
<Spacer />
<RefreshButton onClick={refetch} isFetching={isFetching} />
</CardHeader>
</Card>
<SimpleGrid minChildWidth="500px" spacing="20px">
{endpointsList}
</SimpleGrid>
</>
);
};
export default EndpointsPage;

View File

@@ -15,7 +15,7 @@ const FirmwareDashboardEndpointDisplay = ({ data }: Props) => {
const { t } = useTranslation();
return (
<Card variant="widget" w="100%">
<Card w="100%">
<CardHeader>
<Heading mr={2} my="auto" size="md">
{t('controller.firmware.endpoints')}

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Spacer />
<RefreshButton isCompact onClick={getDashboard.refetch} isFetching={getDashboard.isFetching} />
</CardHeader>
<CardBody p={4}>
<Box display="block" w="100%">
<>
{getDashboard.isLoading && (
<Center my="100px">
<Spinner size="xl" />
</Center>
)}
{getDashboard.error && (
<Center my="100px">
<Alert status="error" mb={4} w="unset">
<AlertIcon />
<Box>
<AlertTitle>{t('controller.dashboard.error_fetching')}</AlertTitle>
{
// @ts-ignore
<AlertDescription>{getDashboard.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box>
</Alert>
</Center>
)}
{getDashboard.data && (
<Masonry
breakpointCols={{
default: 3,
1800: 3,
1400: 2,
1100: 1,
}}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
style={{
marginLeft: 'unset',
}}
>
<SimpleIconStatDisplay
title={t('analytics.last_ping')}
value={<FormattedDate date={getDashboard.data.snapshot ?? 0} />}
description={t('controller.dashboard.last_ping_explanation')}
icon={Clock}
color={['blue.300', 'blue.300']}
/>
<SimpleIconStatDisplay
title={t('devices.title')}
value={getDashboard.data?.numberOfDevices ?? 0}
description={t('controller.firmware.devices_explanation')}
icon={WifiHigh}
color={['green.300', 'green.300']}
/>
<UpToDateDevicesSimple data={getDashboard.data} />
<AverageFirmwareAge data={getDashboard.data} />
<FirmwareLatestPieChart data={getDashboard.data} />
<UnknownFirmwareBarChart data={getDashboard.data.unknownFirmwares} />
<FirmwareDashboardEndpointDisplay data={getDashboard.data.endPoints} />
<OuisBarChart data={getDashboard.data.ouis} />
<ConnectedPieChart data={getDashboard.data} />
<DeviceTypesPieChart data={getDashboard.data.deviceTypes} />
</Masonry>
)}
</>
</Box>
</CardBody>
<Card mb="20px">
<CardHeader variant="unstyled" px={4} py={2}>
<Flex alignItems="center">
<Heading size="md">{t('analytics.last_ping')}</Heading>
<Text ml={1} pt={0.5}>
<FormattedDate date={getDashboard.data?.snapshot ?? 0} key={getDashboard.dataUpdatedAt} />
</Text>
<Tooltip label={t('controller.dashboard.last_ping_explanation')} hasArrow>
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
</Tooltip>
</Flex>
<Spacer />
<RefreshButton isCompact onClick={getDashboard.refetch} isFetching={getDashboard.isFetching} />
</CardHeader>
</Card>
<Box display="block" w="100%">
<>
{getDashboard.isLoading && (
<Center my="100px">
<Spinner size="xl" />
</Center>
)}
{getDashboard.error && (
<Center my="100px">
<Alert status="error" mb={4} w="unset">
<AlertIcon />
<Box>
<AlertTitle>{t('controller.dashboard.error_fetching')}</AlertTitle>
{
// @ts-ignore
<AlertDescription>{getDashboard.error?.response?.data?.ErrorDescription}</AlertDescription>
}
</Box>
</Alert>
</Center>
)}
{getDashboard.data && (
<Masonry
breakpointCols={{
default: 3,
1800: 3,
1400: 2,
1100: 1,
}}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
<SimpleIconStatDisplay
title={t('devices.title')}
value={getDashboard.data?.numberOfDevices ?? 0}
description={t('controller.firmware.devices_explanation')}
icon={WifiHigh}
color={['green.300', 'green.300']}
/>
<UpToDateDevicesSimple data={getDashboard.data} />
<AverageFirmwareAge data={getDashboard.data} />
<FirmwareLatestPieChart data={getDashboard.data} />
<UnknownFirmwareBarChart data={getDashboard.data.unknownFirmwares} />
<FirmwareDashboardEndpointDisplay data={getDashboard.data.endPoints} />
<OuisBarChart data={getDashboard.data.ouis} />
<ConnectedPieChart data={getDashboard.data} />
<DeviceTypesPieChart data={getDashboard.data.deviceTypes} />
</Masonry>
)}
</>
</Box>
</>
);
};

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Card>
<CardHeader>
<Heading size="md" my="auto" mr={2}>
{t('analytics.firmware')} {getFirmware.data ? `(${getFirmware.data.length})` : ''}
</Heading>
@@ -155,7 +156,7 @@ const FirmwareListTable = () => {
/>
</HStack>
</CardHeader>
<CardBody p={4}>
<CardBody>
<Box overflowX="auto" w="100%">
<LoadingOverlay isLoading={getDeviceTypes.isFetching || getFirmware.isFetching}>
<DataTable<Firmware>
@@ -171,7 +172,7 @@ const FirmwareListTable = () => {
</Box>
</CardBody>
<FirmwareDetailsModal firmware={firmware} modalProps={modalProps} />
</>
</Card>
);
};

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Card>
<CardHeader>
<DeviceLogsSearchBar onSearchSelect={onSerialSelect} />
<Spacer />
<HStack spacing={2}>
@@ -133,7 +134,7 @@ const LogsCard = () => {
</CSVLink>
</HStack>
</CardHeader>
<CardBody p={4}>
<CardBody>
<Box overflowX="auto" w="100%">
<Table size="sm">
<Thead>
@@ -158,7 +159,7 @@ const LogsCard = () => {
</Box>
</Box>
</CardBody>
</>
</Card>
);
};

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Card>
<CardHeader>
<Spacer />
<HStack spacing={2}>
<ShownLogsDropdown
@@ -148,7 +149,7 @@ const FmsLogsCard = () => {
</CSVLink>
</HStack>
</CardHeader>
<CardBody p={4}>
<CardBody>
<Box overflowX="auto" w="100%">
<Table size="sm">
<Thead>
@@ -182,7 +183,7 @@ const FmsLogsCard = () => {
</Box>
</Box>
</CardBody>
</>
</Card>
);
};

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Card>
<CardHeader>
<Spacer />
<HStack spacing={2}>
<ShownLogsDropdown
@@ -148,7 +149,7 @@ const GeneralLogsCard = () => {
</CSVLink>
</HStack>
</CardHeader>
<CardBody p={4}>
<CardBody>
<Box overflowX="auto" w="100%">
<Table size="sm">
<Thead>
@@ -182,7 +183,7 @@ const GeneralLogsCard = () => {
</Box>
</Box>
</CardBody>
</>
</Card>
);
};

View File

@@ -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 (
<>
<CardHeader px={4} pt={4}>
<Card>
<CardHeader>
<Spacer />
<HStack spacing={2}>
<ShownLogsDropdown
@@ -148,7 +149,7 @@ const SecLogsCard = () => {
</CSVLink>
</HStack>
</CardHeader>
<CardBody p={4}>
<CardBody>
<Box overflowX="auto" w="100%">
<Table size="sm">
<Thead>
@@ -182,7 +183,7 @@ const SecLogsCard = () => {
</Box>
</Box>
</CardBody>
</>
</Card>
);
};

View File

@@ -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 (
<Box>
<Flex mb={2}>
<>
<CardHeader>
<Heading size="md" my="auto">
{t('keys.other')} ({query.data?.apiKeys.length})
</Heading>
@@ -33,8 +35,8 @@ const ApiKeyTable = ({ userId }: Props) => {
/>
<RefreshButton onClick={query.refetch} isFetching={query.isFetching} isCompact />
</HStack>
</Flex>
<Box>
</CardHeader>
<CardBody>
<DataTable
columns={columns as Column<object>[]}
saveSettingsId="apiKeys.profile.table"
@@ -46,8 +48,8 @@ const ApiKeyTable = ({ userId }: Props) => {
showAllRows
hideControls
/>
</Box>
</Box>
</CardBody>
</>
);
};

View File

@@ -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 (
<Card p={4}>
<CardBody>
<Box w="100%">
<ApiKeyTable userId={user?.id ?? ''} />
</Box>
</CardBody>
<Card>
<ApiKeyTable userId={user?.id ?? ''} />
</Card>
);
};

View File

@@ -70,8 +70,8 @@ const GeneralInformationProfile = () => {
}, [isEditing]);
return (
<Card p={4}>
<CardHeader mb={2}>
<Card>
<CardHeader>
<Heading size="md">{t('profile.your_profile')}</Heading>
<Spacer />
<HStack>

View File

@@ -41,8 +41,8 @@ const MultiFactorAuthProfile = () => {
};
return (
<Card p={4}>
<CardHeader mb={2}>
<Card>
<CardHeader>
<Heading size="md">{t('account.mfa')}</Heading>
<Tag colorScheme={currentMfaMethod ? 'green' : 'red'} fontSize="lg" fontWeight="bold" ml={2}>
{currentMfaMethod ? t('profile.enabled').toUpperCase() : t('profile.disabled').toUpperCase()}

View File

@@ -115,8 +115,8 @@ const ProfileNotes = () => {
);
return (
<Card p={4}>
<CardHeader mb={2}>
<Card>
<CardHeader>
<Heading size="md">{t('common.notes')}</Heading>
<Spacer />
<Popover trigger="click" placement="auto">

View File

@@ -12,7 +12,7 @@ const SummaryInformationProfile = () => {
const { user, avatar } = useAuth();
return (
<Card p={4}>
<Card>
<CardBody display="block">
<Box
h="120px"

View File

@@ -10,9 +10,9 @@ import {
useToast,
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { CreateButton } from '../../../components/Buttons/CreateButton';
import { SaveButton } from '../../../components/Buttons/SaveButton';
import { Modal } from '../../../components/Modals/Modal';
import { CreateButton } from '../../components/Buttons/CreateButton';
import { SaveButton } from '../../components/Buttons/SaveButton';
import { Modal } from '../../components/Modals/Modal';
import { useCreateSystemSecret } from 'hooks/Network/Secrets';
import { AxiosError } from 'models/Axios';

View File

@@ -1,7 +1,7 @@
import * as React from 'react';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { DataTable } from '../../../components/DataTables/DataTable';
import { DataTable } from '../../components/DataTables/DataTable';
import SystemSecretActions from './Actions';
import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets';
import { Column } from 'models/Table';
@@ -58,7 +58,7 @@ const SystemSecretsTable = () => {
columns={columns as Column<object>[]}
saveSettingsId="system.secrets.table"
data={getSecrets.data ?? []}
obj={t('keys.other')}
obj={t('system.secrets')}
sortBy={[{ id: 'key', desc: false }]}
showAllRows
hideControls

View File

@@ -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 (
<Box px={4} py={4}>
<Card variant="widget" {...props}>
<CardHeader>
<Heading size="md" my="auto">
{t('system.secrets')}
</Heading>
<Spacer />
<SystemSecretCreateButton />
</CardHeader>
<CardBody p={4}>
<SystemSecretsTable />
</CardBody>
</Card>
</Box>
<Card {...props}>
<CardHeader>
<Heading size="md" my="auto">
{t('system.secrets')}
</Heading>
<Spacer />
<SystemSecretCreateButton />
</CardHeader>
<CardBody p={4}>
<SystemSecretsTable />
</CardBody>
</Card>
);
};
export default SystemConfigurationPage;

View File

@@ -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) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
}, [endpoints, token]);
return (
<Card p={0}>
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
<TabList>
<CardHeader>
<Tab>{t('system.services')}</Tab>
<Tab hidden={!isRoot}>{t('system.configuration')}</Tab>
</CardHeader>
</TabList>
<TabPanels>
<TabPanel p={0}>
<Box
borderLeft="1px solid"
borderRight="1px solid"
borderBottom="1px solid"
borderColor="var(--chakra-colors-chakra-border-color)"
borderBottomLeftRadius="15px"
borderBottomRightRadius="15px"
>
{!isOnlySec && (
<CardHeader px={4} pt={4}>
<Spacer />
<RefreshButton onClick={refetch} isFetching={isFetching} />
</CardHeader>
)}
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
{endpointsList}
</SimpleGrid>
</Box>
</TabPanel>
<TabPanel p={0}>
<Box
borderLeft="1px solid"
borderRight="1px solid"
borderBottom="1px solid"
borderColor="var(--chakra-colors-chakra-border-color)"
borderBottomLeftRadius="15px"
borderBottomRightRadius="15px"
>
<SystemSecretsCard />
</Box>
</TabPanel>
</TabPanels>
</Tabs>
</Card>
);
};
export default SystemPage;

View File

@@ -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) => (
<Icon as={WifiHigh} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
component: DevicesPage,
icon: () => <WifiHigh size={28} weight="bold" />,
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) => (
<Icon as={FloppyDisk} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
component: FirmwarePage,
icon: () => <FloppyDisk size={28} weight="bold" />,
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 as={TerminalWindow} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
icon: () => <TerminalWindow size={28} weight="bold" />,
component: ScriptsPage,
},
{
id: 'configurations',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/configurations',
name: 'configurations.title',
icon: (active: boolean) => (
<Icon as={Barcode} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
icon: () => <Barcode size={28} weight="bold" />,
component: DefaultConfigurationsPage,
},
{
id: 'logs-group',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/logs',
name: 'controller.devices.logs',
icon: (active: boolean) => (
<Icon as={ListBullets} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
component: NotificationsPage,
icon: () => <ListBullets size={28} weight="bold" />,
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) => (
<Icon as={WifiHigh} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
navName: 'PATH',
icon: () => <WifiHigh size={28} weight="bold" />,
component: DevicePage,
},
{
id: 'account-page',
hidden: true,
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/account',
name: 'account.title',
icon: (active: boolean) => (
<Icon as={UsersThree} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
icon: () => <UsersThree size={28} weight="bold" />,
component: ProfilePage,
},
{
id: 'users-page',
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
path: '/users',
name: 'users.title',
icon: (active: boolean) => (
<Icon as={UsersThree} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
icon: () => <UsersThree size={28} weight="bold" />,
component: UsersPage,
},
{
id: 'system-group',
authorized: ['root', 'partner', 'admin'],
path: '/system',
name: 'system.title',
icon: (active: boolean) => (
<Icon as={Info} color="inherit" h={active ? '32px' : '24px'} w={active ? '32px' : '24px'} />
),
component: SystemPage,
icon: () => <Info size={28} weight="bold" />,
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,
},
],
},
];

View File

@@ -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: {

View File

@@ -2,6 +2,8 @@ const CardBody = {
baseStyle: {
display: 'flex',
width: '100%',
py: '12px',
px: '12px',
},
};

View File

@@ -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',
},
};

View File

@@ -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,
},
};

View File

@@ -1,12 +0,0 @@
const PanelContainer = {
baseStyle: {
p: '30px 10px 0px',
minHeight: 'calc(100vh - 123px)',
},
};
export default {
components: {
PanelContainer,
},
};

View File

@@ -1,14 +0,0 @@
const PanelContent = {
baseStyle: {
ms: 'auto',
me: 'auto',
ps: '15px',
pe: '15px',
},
};
export default {
components: {
PanelContent,
},
};

View File

@@ -22,6 +22,9 @@ export default {
boxShadow: 'none',
},
},
solid: {
border: '0.5px solid',
},
},
baseStyle: {
borderRadius: '15px',

View File

@@ -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,
});

View File

@@ -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 });

View File

@@ -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,
},
});