mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-10-29 17:32:20 +00:00
[WIFI-12574] Theme improvements
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.10.0(35)",
|
||||
"version": "2.10.0(36)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
import React, { DOMAttributes } from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
Box,
|
||||
EffectProps,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
useColorModeValue,
|
||||
useStyleConfig,
|
||||
useToken,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
export interface CardHeaderProps extends LayoutProps, SpaceProps {
|
||||
variant?: string;
|
||||
export interface CardHeaderProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps,
|
||||
DOMAttributes<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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal file
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal 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;
|
||||
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal file
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
73
src/pages/Device/RadiusClients/ActionCell.tsx
Normal file
73
src/pages/Device/RadiusClients/ActionCell.tsx
Normal 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;
|
||||
117
src/pages/Device/RadiusClients/Modal.tsx
Normal file
117
src/pages/Device/RadiusClients/Modal.tsx
Normal 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;
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
34
src/pages/Devices/Dashboard/MemorySimpleChart.tsx
Normal file
34
src/pages/Devices/Dashboard/MemorySimpleChart.tsx
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
>
|
||||
<>
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
56
src/pages/EndpointsPage/index.tsx
Normal file
56
src/pages/EndpointsPage/index.tsx
Normal 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;
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -12,7 +12,7 @@ const SummaryInformationProfile = () => {
|
||||
const { user, avatar } = useAuth();
|
||||
|
||||
return (
|
||||
<Card p={4}>
|
||||
<Card>
|
||||
<CardBody display="block">
|
||||
<Box
|
||||
h="120px"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -2,6 +2,8 @@ const CardBody = {
|
||||
baseStyle: {
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
py: '12px',
|
||||
px: '12px',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
const PanelContainer = {
|
||||
baseStyle: {
|
||||
p: '30px 10px 0px',
|
||||
minHeight: 'calc(100vh - 123px)',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PanelContainer,
|
||||
},
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
const PanelContent = {
|
||||
baseStyle: {
|
||||
ms: 'auto',
|
||||
me: 'auto',
|
||||
ps: '15px',
|
||||
pe: '15px',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PanelContent,
|
||||
},
|
||||
};
|
||||
@@ -22,6 +22,9 @@ export default {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
},
|
||||
solid: {
|
||||
border: '0.5px solid',
|
||||
},
|
||||
},
|
||||
baseStyle: {
|
||||
borderRadius: '15px',
|
||||
|
||||
27
src/theme/components/modal.ts
Normal file
27
src/theme/components/modal.ts
Normal 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,
|
||||
});
|
||||
46
src/theme/components/tabs.ts
Normal file
46
src/theme/components/tabs.ts
Normal 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 });
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user