[WIFI-11223] System page fixes

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2022-10-26 12:37:08 +01:00
parent d06bfd91ff
commit da7f29a9e0
15 changed files with 376 additions and 28 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.8.0(1)",
"version": "2.8.0(2)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.8.0(1)",
"version": "2.8.0(2)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",

View File

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

View File

@@ -610,6 +610,7 @@
"new_devices": "Neue Geräte",
"not_connected": "Nicht verbunden",
"not_found_gateway": "Fehler: Gerät hat sich noch nicht mit dem Gateway verbunden",
"notifications": "Gerätebenachrichtigungen",
"one": "Gerät",
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
"sanity": "Gesundheit",
@@ -878,6 +879,9 @@
"endpoint": "Endpunkt",
"hostname": "Hostname",
"info": "Systeminformationen",
"level": "Protokollstufe",
"logging": "Protokollierung",
"no_log_levels": "Keine gemeldeten Protokollebenen",
"os": "Betriebssystem",
"processors": "Prozessoren",
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
@@ -886,6 +890,8 @@
"success_reload": "Reload-Befehl erfolgreich gesendet!",
"systems_to_reload": "Wählen Sie Systeme zum Neuladen aus",
"title": "System",
"update_level_success": "Loglevel aktualisiert!",
"update_levels": "Aktualisieren",
"uptime": "Betriebszeit",
"version": "Ausführung"
},

View File

@@ -610,6 +610,7 @@
"new_devices": "new devices",
"not_connected": "Not Connected",
"not_found_gateway": "Error: device has not yet connected to the controller",
"notifications": "Device Notifications",
"one": "Device",
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
"sanity": "Sanity",
@@ -878,6 +879,9 @@
"endpoint": "Endpoint",
"hostname": "Host Name",
"info": "System Info",
"level": "Log Level",
"logging": "Logging",
"no_log_levels": "No Reported Log Levels ",
"os": "Operating System",
"processors": "Processors",
"reload_chosen_subsystems": "Reload Chosen Subsystems",
@@ -886,6 +890,8 @@
"success_reload": "Successfully sent reload command!",
"systems_to_reload": "Choose systems to reload",
"title": "System",
"update_level_success": "Updated log levels!",
"update_levels": "Update",
"uptime": "Uptime",
"version": "Version"
},

View File

@@ -610,6 +610,7 @@
"new_devices": "Nuevos dispositivos",
"not_connected": "No conectado",
"not_found_gateway": "Error: el dispositivo aún no se ha conectado a la puerta de enlace",
"notifications": "notificaciones de dispositivos",
"one": "Dispositivo",
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
"sanity": "Cordura",
@@ -878,6 +879,9 @@
"endpoint": "punto final",
"hostname": "Nombre de host",
"info": "Información del sistema",
"level": "nivel de registro",
"logging": "Inicio sesión",
"no_log_levels": "Niveles de registro no informados",
"os": "sistema operativo",
"processors": "Procesadores",
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
@@ -886,6 +890,8 @@
"success_reload": "¡Comando de recarga enviado con éxito!",
"systems_to_reload": "Elige sistemas para recargar",
"title": "Sistema",
"update_level_success": "¡Niveles de registro actualizados!",
"update_levels": "Actualizar",
"uptime": "Tiempo de actividad",
"version": "Versión"
},

View File

@@ -610,6 +610,7 @@
"new_devices": "nouveaux appareils",
"not_connected": "Pas connecté",
"not_found_gateway": "Erreur : l'appareil n'est pas encore connecté à la passerelle",
"notifications": "notifications de l'appareil",
"one": "Dispositif",
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
"sanity": "Santé mentale",
@@ -878,6 +879,9 @@
"endpoint": "Point final",
"hostname": "nom d'hôte",
"info": "Information système",
"level": "niveau de journal",
"logging": "Enregistrement",
"no_log_levels": "Aucun niveau de journal signalé",
"os": "Système opérateur",
"processors": "Processeurs",
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
@@ -886,6 +890,8 @@
"success_reload": "Commande de rechargement envoyée avec succès !",
"systems_to_reload": "Choisissez les systèmes à recharger",
"title": "Système",
"update_level_success": "Niveaux de journal mis à jour !",
"update_levels": "Mettre à jour",
"uptime": "La disponibilité",
"version": "Version"
},

View File

@@ -610,6 +610,7 @@
"new_devices": "novos dispositivos",
"not_connected": "Não conectado",
"not_found_gateway": "Erro: o dispositivo ainda não se conectou ao gateway",
"notifications": "Notificações do dispositivo",
"one": "Dispositivo",
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
"sanity": "Sanidade",
@@ -878,6 +879,9 @@
"endpoint": "Ponto final",
"hostname": "Nome de anfitrião",
"info": "Informação do sistema",
"level": "nível de log",
"logging": "Exploração madeireira",
"no_log_levels": "Nenhum nível de registro relatado",
"os": "Sistema Operacional",
"processors": "Processadores",
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
@@ -886,6 +890,8 @@
"success_reload": "Comando de recarga enviado com sucesso!",
"systems_to_reload": "Escolha sistemas para recarregar",
"title": "Sistema",
"update_level_success": "Níveis de log atualizados!",
"update_levels": "Atualizar",
"uptime": "Tempo de atividade",
"version": "Versão"
},

View File

@@ -1,5 +1,5 @@
import * as React from 'react';
import { HStack, Modal as ChakraModal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
import { HStack, LayoutProps, Modal as ChakraModal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react';
import { ModalHeader } from '../GenericModal/ModalHeader';
import { CloseButton } from 'components/Buttons/CloseButton';
@@ -10,12 +10,14 @@ export type ModalProps = {
topRightButtons?: React.ReactNode;
options?: {
modalSize?: 'sm' | 'md' | 'lg';
maxWidth?: LayoutProps['maxWidth'];
};
children: React.ReactElement;
};
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
const maxWidth = React.useMemo(() => {
if (options?.maxWidth) return options.maxWidth;
if (options?.modalSize === 'sm') return undefined;
if (options?.modalSize === 'lg') {
return { sm: '90%', md: '900px', lg: '1000px', xl: '80%' };

View File

@@ -1,5 +1,5 @@
import { useToast } from '@chakra-ui/react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as axios from 'axios';
import { useTranslation } from 'react-i18next';
import { AxiosError } from 'models/Axios';
@@ -61,7 +61,7 @@ export const useGetSubsystems = ({
},
},
)
.then(({ data }: { data: { list: string[] } }) => data.list),
.then(({ data }: { data: { list: string[] } }) => data.list ?? []),
{
staleTime: 60000,
enabled,
@@ -121,3 +121,81 @@ export const useReloadSubsystems = ({
},
);
};
export const useGetSystemLogLevels = ({
endpoint,
enabled,
token,
}: {
endpoint: string;
enabled: boolean;
token: string;
}) =>
useQuery(
['get-log-levels', endpoint],
() =>
axiosInstance
.post(
`${endpoint}/api/v1/system`,
{ command: 'getloglevels' },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
)
.then(({ data }: { data: { tagList: { tag: string; value: string }[] } }) => data.tagList ?? []),
{
staleTime: 60000,
enabled,
},
);
export const useGetSystemLogLevelNames = ({
endpoint,
enabled,
token,
}: {
endpoint: string;
enabled: boolean;
token: string;
}) =>
useQuery(
['get-log-level-names', endpoint],
() =>
axiosInstance
.post(
`${endpoint}/api/v1/system`,
{ command: 'getloglevelnames' },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
)
.then(({ data }: { data: { list: string[] } }) => data.list ?? []),
{
staleTime: 60000,
enabled,
},
);
const changeLogLevel = (endpoint: string, token: string) => async (subsystems: { tag: string; value: string }[]) =>
axiosInstance.post(
`${endpoint}/api/v1/system`,
{ command: 'setloglevel', subsystems },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
export const useUpdateSystemLogLevels = ({ endpoint, token }: { endpoint: string; token: string }) => {
const queryClient = useQueryClient();
return useMutation(changeLogLevel(endpoint, token), {
onSuccess: () => {
queryClient.invalidateQueries(['get-log-levels', endpoint]);
},
});
};

View File

@@ -29,10 +29,10 @@ const LogsCard = () => {
<Td fontFamily="monospace" pt={2} fontSize="md">
{msg.data?.serialNumber ?? '-'}
</Td>
<Td>
<Td whiteSpace="nowrap">
<Box>{labels[msg.data.type] ?? msg.data.type}</Box>
</Td>
<Td>{JSON.stringify(msg.data)}</Td>
<Td whiteSpace="nowrap">{JSON.stringify(msg.data)}</Td>
</Tr>
);
}
@@ -42,10 +42,10 @@ const LogsCard = () => {
<Td fontFamily="monospace" pt={2} fontSize="md">
-
</Td>
<Td>
<Td whiteSpace="nowrap">
<Box>{t('common.unknown')}</Box>
</Td>
<Td>{JSON.stringify(msg.data)}</Td>
<Td whiteSpace="nowrap">{JSON.stringify(msg.data)}</Td>
</Tr>
);
},
@@ -99,7 +99,7 @@ const LogsCard = () => {
<Th w="100px">{t('common.time')}</Th>
<Th w="140px">{t('inventory.serial_number')}</Th>
<Th w="200px">{t('common.type')}</Th>
<Th minW="60%">{t('analytics.raw_data')}</Th>
<Th minW="100%">{t('analytics.raw_data')}</Th>
</Thead>
<Tbody>{rows}</Tbody>
</Table>

View File

@@ -0,0 +1,189 @@
import * as React from 'react';
import {
Alert,
AlertDescription,
AlertIcon,
Box,
Button,
Center,
Heading,
Select,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
UseDisclosureReturn,
useToast,
} from '@chakra-ui/react';
import axios from 'axios';
import { FloppyDisk } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { v4 as uuid } from 'uuid';
import { LoadingOverlay } from 'components/LoadingOverlay';
import { Modal } from 'components/Modals/Modal';
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
type Props = {
modalProps: UseDisclosureReturn;
endpoint: string;
token: string;
};
const SystemLoggingModal = ({ modalProps, endpoint, token }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const getLevels = useGetSystemLogLevels({ endpoint, token, enabled: false });
const getNames = useGetSystemLogLevelNames({ endpoint, token, enabled: false });
const updateLevels = useUpdateSystemLogLevels({ endpoint, token });
const [newLevels, setNewLevels] = React.useState<{ tag: string; value: string }[]>([]);
const onUpdate = () => {
updateLevels.mutate(newLevels, {
onSuccess: () => {
toast({
id: 'log-level-update-success',
title: t('common.success'),
description: t('system.update_level_success'),
status: 'success',
duration: 5000,
isClosable: true,
position: 'top-right',
});
modalProps.onClose();
},
});
};
const onLevelChange = React.useCallback(
(tag: string, originalValue: string) => (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === originalValue) {
setNewLevels((prev) => prev.filter((level) => level.tag !== tag));
} else {
setNewLevels((prev) => [
...prev.filter(({ tag: foundTag }) => foundTag !== tag),
{ tag, value: e.target.value },
]);
}
},
[newLevels, getLevels.data],
);
const levelOptions = getNames.data
? getNames.data
.sort((a, b) => a.localeCompare(b))
.map((level) => (
<option key={uuid()} value={level}>
{level}
</option>
))
: [];
const row = React.useCallback(
(tag: string, value: string) => {
const newValue = newLevels.find(({ tag: foundTag }) => foundTag === tag)?.value;
return (
<Tr key={uuid()}>
<Td>
<Heading size="sm">{tag}</Heading>
</Td>
<Td fontFamily="monospace" pt={2} fontSize="md">
{getNames.data ? (
<Select
variant={newValue ? 'filled' : undefined}
_hover={{
bg: newValue ? 'teal.300' : undefined,
}}
bg={newValue ? 'teal.300' : undefined}
value={newValue ?? value}
onChange={onLevelChange(tag, value)}
>
{levelOptions}
</Select>
) : (
value
)}
</Td>
</Tr>
);
},
[t, getNames.data, onLevelChange],
);
const rows = React.useMemo(() => {
if (!getLevels.data) return [];
return getLevels.data.sort((a, b) => a.tag.localeCompare(b.tag)).map((level) => row(level.tag, level.value));
}, [getLevels.data, row]);
React.useEffect(() => {
if (modalProps.isOpen) {
setNewLevels([]);
getLevels.refetch();
getNames.refetch();
}
}, [modalProps.isOpen]);
React.useEffect(() => {
setNewLevels([]);
}, [getLevels.data]);
return (
<Modal
isOpen={modalProps.isOpen}
onClose={modalProps.onClose}
title={t('system.logging')}
options={{
modalSize: 'sm',
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>
}
>
<>
{updateLevels.error && (
<Alert status="error">
<AlertIcon />
<AlertDescription>
{axios.isAxiosError(updateLevels.error)
? updateLevels.error.response?.data?.ErrorDescription
: t('common.error')}
</AlertDescription>
</Alert>
)}
<LoadingOverlay isLoading={getLevels.isFetching}>
<Box overflowX="auto">
<Table size="sm">
<Thead>
<Tr>
<Th>{t('system.subsystems')}</Th>
<Th w="220px">{t('system.level')}</Th>
</Tr>
</Thead>
<Tbody>{rows}</Tbody>
</Table>
{getLevels.data && rows.length === 0 ? (
<Center mt={2}>
<Alert status="info" w="unset">
<AlertIcon />
<AlertDescription>{t('system.no_log_levels')}</AlertDescription>
</Alert>
</Center>
) : null}
</Box>
</LoadingOverlay>
</>
</Modal>
);
};
export default SystemLoggingModal;

View File

@@ -0,0 +1,26 @@
import * as React from 'react';
import { Button, useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import SystemLoggingModal from './Modal';
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
type Props = {
endpoint: EndpointApiResponse;
token: string;
};
const SystemLoggingButton = ({ endpoint, token }: Props) => {
const { t } = useTranslation();
const modalProps = useDisclosure();
return (
<>
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
{t('system.logging')}
</Button>
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
</>
);
};
export default SystemLoggingButton;

View File

@@ -18,7 +18,10 @@ const SystemCertificatesTable: React.FC<Props> = ({ certificates }) => {
const memoizedExpiry = useCallback((expiresOn: number) => compactDate(expiresOn), []);
const columns = React.useMemo(
(): Column<{ expiresOn: number; filename: string }>[] => [
(): Column<{
expiresOn: number;
filename: string;
}>[] => [
{
id: 'expiresOn',
Header: t('certificates.expires_on'),
@@ -41,7 +44,7 @@ const SystemCertificatesTable: React.FC<Props> = ({ certificates }) => {
return (
<DataTable
columns={columns}
columns={columns as Column<object>[]}
data={certificates ?? []}
obj={t('certificates.title')}
hideControls

View File

@@ -19,6 +19,7 @@ import {
import { MultiValue, Select } from 'chakra-react-select';
import { ArrowsClockwise } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import SystemLoggingButton from './LoggingButton';
import SystemCertificatesTable from './SystemCertificatesTable';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
@@ -32,7 +33,7 @@ interface Props {
token: string;
}
const SystemTile: React.FC<Props> = ({ endpoint, token }) => {
const SystemTile = ({ endpoint, token }: Props) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const [subs, setSubs] = useState<{ value: string; label: string }[]>([]);
@@ -68,6 +69,7 @@ const SystemTile: React.FC<Props> = ({ endpoint, token }) => {
<Box display="flex" mb={2}>
<Heading pt={0}>{endpoint.type}</Heading>
<Spacer />
<SystemLoggingButton endpoint={endpoint} token={token} />
<Button
mt={1}
minWidth="112px"
@@ -83,35 +85,51 @@ const SystemTile: React.FC<Props> = ({ endpoint, token }) => {
<VStack w="100%">
<SimpleGrid minChildWidth="500px" w="100%">
<Flex>
<Box w="150px">{t('system.endpoint')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.endpoint')}:
</Heading>
{endpoint.uri}
</Flex>
<Flex>
<Box w="150px">{t('system.hostname')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.hostname')}:
</Heading>
{system?.hostname}
</Flex>
<Flex>
<Box w="150px">{t('system.os')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.os')}:
</Heading>
{system?.os}
</Flex>
<Flex>
<Box w="150px">{t('system.processors')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.processors')}:
</Heading>
{system?.processors}
</Flex>
<Flex>
<Box w="150px">{t('system.start')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.start')}:
</Heading>
{system?.start ? <FormattedDate date={system?.start} /> : '-'}
</Flex>
<Flex>
<Box w="150px">{t('system.uptime')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.uptime')}:
</Heading>
{system?.uptime ? compactSecondsToDetailed(system.uptime, t) : '-'}
</Flex>
<Flex>
<Box w="150px">{t('system.version')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.version')}:
</Heading>
{system?.version}
</Flex>
<Flex>
<Box w="150px">{t('certificates.title')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('certificates.title')}:
</Heading>
{system?.certificates && system.certificates?.length > 0 ? (
<Button variant="link" onClick={onOpen} p={0} m={0} maxH={7}>
{t('common.details')} {system.certificates.length}
@@ -122,7 +140,9 @@ const SystemTile: React.FC<Props> = ({ endpoint, token }) => {
</Flex>
</SimpleGrid>
<Flex w="100%">
<Box w="150px">{t('system.subsystems')}:</Box>
<Heading size="sm" w="150px" my="auto">
{t('system.subsystems')}:
</Heading>
<Box w="400px">
<Select
chakraStyles={{

View File

@@ -31,13 +31,13 @@ const SystemPage = () => {
.map((endpoint) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
}, [endpoints, token, isUserLoaded]);
if (!isUserLoaded) return null;
return (
<Flex flexDirection="column" pt="75px">
{isUserLoaded ? (
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
{endpointsList}
</SimpleGrid>
) : null}
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
{endpointsList}
</SimpleGrid>
</Flex>
);
};