Merge pull request #156 from stephb9959/main

[WIFI-12067] Added crash logs to device details page
This commit is contained in:
Charles Bourque
2023-01-06 14:55:06 -05:00
committed by GitHub
14 changed files with 273 additions and 41 deletions

4
package-lock.json generated
View File

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

View File

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

View File

@@ -392,6 +392,7 @@
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
"weight": "Gewicht",
"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden",
"wifi_frames": "WiFi-Frames"
},
"contacts": {
@@ -601,6 +602,7 @@
"certificate_expires_in": "Zertifikat läuft ab in",
"certificate_expiry": "Zert. Läuft ab in",
"connected": "In Verbindung gebracht",
"crash_logs": "Absturzprotokolle",
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
"create_success": " Geräte erfolgreich erstellt",
"current_firmware": "Aktuelle Firmware",
@@ -614,6 +616,7 @@
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
"logs_one": "Log",
"new_devices": "Neue Geräte",
"no_model_image": "Kein Modellbild gefunden",
"not_connected": "Nicht verbunden",

View File

@@ -392,6 +392,7 @@
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
"weight": "Weight",
"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band",
"wifi_frames": "WiFi Frames"
},
"contacts": {
@@ -601,6 +602,7 @@
"certificate_expires_in": "Certificate Expiry",
"certificate_expiry": "Cert. Expires In",
"connected": "Connected",
"crash_logs": "Crash Logs",
"create_errors": "errors while trying to create devices",
"create_success": " devices successfully created",
"current_firmware": "Current Firmware",
@@ -614,6 +616,7 @@
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
"logs_one": "Log",
"new_devices": "new devices",
"no_model_image": "No Model Image Found",
"not_connected": "Not Connected",

View File

@@ -392,6 +392,7 @@
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
"weight": "Peso",
"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi",
"wifi_frames": "Marcos WiFi"
},
"contacts": {
@@ -601,6 +602,7 @@
"certificate_expires_in": "El certificado caduca en",
"certificate_expiry": "Cert. Expira en",
"connected": "Conectado",
"crash_logs": "Registros de fallas",
"create_errors": "errores al intentar crear dispositivos",
"create_success": " dispositivos creados con éxito",
"current_firmware": "Firmware actual",
@@ -614,6 +616,7 @@
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
"logs_one": "Iniciar sesión",
"new_devices": "Nuevos dispositivos",
"no_model_image": "No se encontró ninguna imagen de modelo",
"not_connected": "No conectado",

View File

@@ -392,6 +392,7 @@
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
"weight": "Poids",
"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi",
"wifi_frames": "Cadres Wi-Fi"
},
"contacts": {
@@ -601,6 +602,7 @@
"certificate_expires_in": "Le certificat expire dans",
"certificate_expiry": "Cert. Expire dans",
"connected": "Connecté",
"crash_logs": "Journaux des plantages",
"create_errors": "erreurs lors de la tentative de création d'appareils",
"create_success": " appareils créés avec succès",
"current_firmware": "Firmware actuel",
@@ -614,6 +616,7 @@
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
"logs_one": "Bûche",
"new_devices": "nouveaux appareils",
"no_model_image": "Aucune image de modèle trouvée",
"not_connected": "Pas connecté",

View File

@@ -392,6 +392,7 @@
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
"weight": "Peso",
"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi",
"wifi_frames": "Quadros WiFi"
},
"contacts": {
@@ -601,6 +602,7 @@
"certificate_expires_in": "Certificado expira em",
"certificate_expiry": "Certificado expira em",
"connected": "Conectado",
"crash_logs": "Registros de falhas",
"create_errors": "erros ao tentar criar dispositivos",
"create_success": " dispositivos criados com sucesso",
"current_firmware": "Firmware atual",
@@ -614,6 +616,7 @@
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
"logs_one": "Registro",
"new_devices": "novos dispositivos",
"no_model_image": "Nenhuma imagem de modelo encontrada",
"not_connected": "Não conectado",

View File

@@ -11,8 +11,10 @@ export type DeviceLog = {
severity: number;
};
const getDeviceLogs = (limit: number, serialNumber?: string) => async () =>
axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () =>
axiosGw
.get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`)
.then((response) => response.data) as Promise<{
values: DeviceLog[];
serialNumber: string;
}>;
@@ -21,20 +23,29 @@ export const useGetDeviceLogs = ({
serialNumber,
limit,
onError,
logType,
}: {
serialNumber?: string;
limit: number;
onError?: (e: AxiosError) => void;
logType?: 0 | 1;
}) =>
useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), {
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
keepPreviousData: true,
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 30000,
onError,
});
const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`);
const deleteLogs = async ({
serialNumber,
endDate,
logType,
}: {
serialNumber: string;
endDate: number;
logType: 0 | 1;
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
export const useDeleteLogs = () => {
const queryClient = useQueryClient();
@@ -45,46 +56,62 @@ export const useDeleteLogs = () => {
});
};
const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) =>
const getLogsBatch = (
serialNumber?: string,
start?: number,
end?: number,
limit?: number,
offset?: number,
logType?: 0 | 1,
) =>
axiosGw
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`)
.get(
`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`,
)
.then((response) => response.data) as Promise<{
values: DeviceLog[];
serialNumber: string;
}>;
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => {
let offset = 0;
const limit = 100;
let logs: DeviceLog[] = [];
let latestResponse: {
values: DeviceLog[];
serialNumber: string;
const getDeviceLogsWithTimestamps =
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => {
let offset = 0;
const limit = 100;
let logs: DeviceLog[] = [];
let latestResponse: {
values: DeviceLog[];
serialNumber: string;
};
do {
// eslint-disable-next-line no-await-in-loop
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType);
logs = logs.concat(latestResponse.values);
offset += limit;
} while (latestResponse.values.length === limit);
return {
values: logs,
};
};
do {
// eslint-disable-next-line no-await-in-loop
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset);
logs = logs.concat(latestResponse.values);
offset += limit;
} while (latestResponse.values.length === limit);
return {
values: logs,
};
};
export const useGetDeviceLogsWithTimestamps = ({
serialNumber,
start,
end,
onError,
logType,
}: {
serialNumber?: string;
start?: number;
end?: number;
onError?: (e: AxiosError) => void;
logType?: 0 | 1;
}) =>
useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
});
useQuery(
['devicelogs', serialNumber, { start, end, logType }],
getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0),
{
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
staleTime: 1000 * 60,
onError,
},
);

View File

@@ -0,0 +1,94 @@
import * as React from 'react';
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import HistoryDatePickers from '../DatePickers';
import DeleteLogModal from './DeleteModal';
import useDeviceLogsTable from './useDeviceLogsTable';
import { RefreshButton } from 'components/Buttons/RefreshButton';
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
import { DataTable } from 'components/DataTables/DataTable';
import { Column } from 'models/Table';
type Props = {
serialNumber: string;
};
const CrashLogs = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({
serialNumber,
limit,
logType: 1,
});
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
};
const onClear = () => {
setTime(undefined);
};
const raiseLimit = () => {
setLimit(limit + 25);
};
const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit;
const data = React.useMemo(() => {
if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded);
if (getLogs.data) return getLogs.data.values;
return [];
}, [getLogs.data, getCustomLogs.data]);
return (
<Box>
<Flex>
<Spacer />
<HStack>
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
<ColumnPicker
columns={columns as Column<unknown>[]}
hiddenColumns={hiddenColumns}
setHiddenColumns={setHiddenColumns}
preference="gateway.device.logs.hiddenColumns"
/>
<DeleteLogModal serialNumber={serialNumber} logType={0} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
</HStack>
</Flex>
<Box overflowY="auto" h="300px">
<DataTable
columns={
columns as {
id: string;
Header: string;
Footer: string;
accessor: string;
}[]
}
data={data}
isLoading={getLogs.isFetching || getCustomLogs.isFetching}
hiddenColumns={hiddenColumns}
obj={t('controller.devices.logs')}
// @ts-ignore
hideControls
showAllRows
/>
{getLogs.data !== undefined && (
<Center mt={1} hidden={getCustomLogs.data !== undefined}>
{!noMoreAvailable || getLogs.isFetching ? (
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}>
{t('controller.devices.show_more')}
</Button>
) : (
<Heading size="sm">{t('controller.devices.no_more_available')}!</Heading>
)}
</Center>
)}
</Box>
{modal}
</Box>
);
};
export default CrashLogs;

View File

@@ -16,8 +16,8 @@ const CustomInputButton = React.forwardRef(
),
);
type Props = { serialNumber: string };
const DeleteLogModal = ({ serialNumber }: Props) => {
type Props = { serialNumber: string; logType: 0 | 1 };
const DeleteLogModal = ({ serialNumber, logType }: Props) => {
const { t } = useTranslation();
const toast = useToast();
const modalProps = useDisclosure();
@@ -26,7 +26,7 @@ const DeleteLogModal = ({ serialNumber }: Props) => {
const onDeleteClick = () => {
deleteLogs.mutate(
{ endDate: Math.floor(date.getTime() / 1000), serialNumber },
{ endDate: Math.floor(date.getTime() / 1000), serialNumber, logType },
{
onSuccess: () => {
modalProps.onClose();

View File

@@ -0,0 +1,55 @@
import * as React from 'react';
import { Box, Button, Code, Heading, useClipboard } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { Modal } from 'components/Modals/Modal';
import { DeviceLog } from 'hooks/Network/DeviceLogs';
type Props = {
modalProps: {
isOpen: boolean;
onClose: () => void;
};
log?: DeviceLog;
};
const DetailedLogViewModal = ({ modalProps, log }: Props) => {
const { t } = useTranslation();
const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2));
React.useEffect(() => {
setValue(JSON.stringify(log?.log ?? {}, null, 2));
}, [log]);
if (!log) return null;
return (
<Modal
isOpen={modalProps.isOpen}
onClose={modalProps.onClose}
title={t('devices.logs_one')}
topRightButtons={
<Button onClick={onCopy} size="md" colorScheme="teal">
{hasCopied ? `${t('common.copied')}!` : t('common.copy')}
</Button>
}
>
<Box>
<Heading size="sm">
<FormattedDate date={log.recorded} />
</Heading>
<Heading size="sm">
{t('controller.devices.severity')}: {log.severity}
</Heading>
<Heading size="sm">
{t('controller.devices.config_id')}: {log.UUID}
</Heading>
<Code whiteSpace="pre-line" mt={2}>
{log.log}
</Code>
</Box>
</Modal>
);
};
export default DetailedLogViewModal;

View File

@@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const [limit, setLimit] = React.useState(25);
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit });
const { time, setTime, getCustomLogs, getLogs, columns } = useDeviceLogsTable({ serialNumber, limit, logType: 0 });
const setNewTime = (start: Date, end: Date) => {
setTime({ start, end });
@@ -48,7 +48,7 @@ const LogHistory = ({ serialNumber }: Props) => {
setHiddenColumns={setHiddenColumns}
preference="gateway.device.logs.hiddenColumns"
/>
<DeleteLogModal serialNumber={serialNumber} />
<DeleteLogModal serialNumber={serialNumber} logType={0} />
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
</HStack>
</Flex>

View File

@@ -1,6 +1,8 @@
import * as React from 'react';
import { Box } from '@chakra-ui/react';
import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react';
import { MagnifyingGlass } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import DetailedLogViewModal from './DetailedLogViewModal';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
import { DeviceLog, useGetDeviceLogs, useGetDeviceLogsWithTimestamps } from 'hooks/Network/DeviceLogs';
import { Column } from 'models/Table';
@@ -8,18 +10,49 @@ import { Column } from 'models/Table';
type Props = {
serialNumber: string;
limit: number;
logType: 0 | 1;
};
const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
const { t } = useTranslation();
const getLogs = useGetDeviceLogs({ serialNumber, limit });
const getLogs = useGetDeviceLogs({ serialNumber, limit, logType });
const modalProps = useDisclosure();
const [log, setLog] = React.useState<DeviceLog | undefined>();
const [time, setTime] = React.useState<{ start: Date; end: Date } | undefined>();
const getCustomLogs = useGetDeviceLogsWithTimestamps({
serialNumber,
start: time ? Math.floor(time.start.getTime() / 1000) : undefined,
end: time ? Math.floor(time.end.getTime() / 1000) : undefined,
logType,
});
const onOpen = React.useCallback((v: DeviceLog) => {
setLog(v);
modalProps.onOpen();
}, []);
const logCell = React.useCallback(
(v: DeviceLog) =>
logType === 1 ? (
<Box display="flex">
<IconButton
aria-label="Open Log Details"
onClick={() => onOpen(v)}
colorScheme="blue"
icon={<MagnifyingGlass size={16} />}
size="xs"
mr={2}
/>
<Text my="auto" maxW="calc(20vw)" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
{v.log}
</Text>
</Box>
) : (
v.log
),
[onOpen],
);
const dateCell = React.useCallback(
(v: number) => (
<Box>
@@ -65,6 +98,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
Footer: '',
accessor: 'log',
customWidth: '35px',
Cell: (v) => logCell(v.cell.row.original),
disableSortBy: true,
},
{
@@ -85,6 +119,7 @@ const useDeviceLogsTable = ({ serialNumber, limit }: Props) => {
getCustomLogs,
time,
setTime,
modal: <DetailedLogViewModal modalProps={modalProps} log={log} />,
};
};

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import CommandHistory from './CommandHistory';
import HealthCheckHistory from './HealthCheckHistory';
import LogHistory from './LogHistory';
import CrashLogs from './LogHistory/CrashLogs';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
@@ -32,6 +33,9 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
<Tab fontSize="lg" fontWeight="bold">
{t('controller.devices.logs')}
</Tab>
<Tab fontSize="lg" fontWeight="bold">
{t('devices.crash_logs')}
</Tab>
</TabList>
<TabPanels>
<TabPanel p={0}>
@@ -51,10 +55,12 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
<TabPanel>
<HealthCheckHistory serialNumber={serialNumber} />
</TabPanel>
<TabPanel>
<LogHistory serialNumber={serialNumber} />
</TabPanel>
<TabPanel>
<CrashLogs serialNumber={serialNumber} />
</TabPanel>
</TabPanels>
</Tabs>
</CardBody>