diff --git a/package-lock.json b/package-lock.json index c3b4914..c70619b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 75bd5e5..1f95789 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.9.0(1)", + "version": "2.9.0(2)", "description": "", "private": true, "main": "index.tsx", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 41853e2..ea95f16 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -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", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index bc57da6..bfede2d 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -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", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 9e71e24..57573a0 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -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", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 680b996..a9f54a7 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -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é", diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index a1d86e3..a057d1f 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -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", diff --git a/src/hooks/Network/DeviceLogs.ts b/src/hooks/Network/DeviceLogs.ts index 8f78a85..ef1fb3b 100644 --- a/src/hooks/Network/DeviceLogs.ts +++ b/src/hooks/Network/DeviceLogs.ts @@ -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, + }, + ); diff --git a/src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx b/src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx new file mode 100644 index 0000000..c79455f --- /dev/null +++ b/src/pages/Device/LogsCard/LogHistory/CrashLogs.tsx @@ -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([]); + 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 ( + + + + + + []} + hiddenColumns={hiddenColumns} + setHiddenColumns={setHiddenColumns} + preference="gateway.device.logs.hiddenColumns" + /> + + + + + + + {getLogs.data !== undefined && ( + + )} + + {modal} + + ); +}; + +export default CrashLogs; diff --git a/src/pages/Device/LogsCard/LogHistory/DeleteModal.tsx b/src/pages/Device/LogsCard/LogHistory/DeleteModal.tsx index 4fc6578..907439f 100644 --- a/src/pages/Device/LogsCard/LogHistory/DeleteModal.tsx +++ b/src/pages/Device/LogsCard/LogHistory/DeleteModal.tsx @@ -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(); diff --git a/src/pages/Device/LogsCard/LogHistory/DetailedLogViewModal.tsx b/src/pages/Device/LogsCard/LogHistory/DetailedLogViewModal.tsx new file mode 100644 index 0000000..6df24bb --- /dev/null +++ b/src/pages/Device/LogsCard/LogHistory/DetailedLogViewModal.tsx @@ -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 ( + + {hasCopied ? `${t('common.copied')}!` : t('common.copy')} + + } + > + + + + + + {t('controller.devices.severity')}: {log.severity} + + + {t('controller.devices.config_id')}: {log.UUID} + + + {log.log} + + + + ); +}; + +export default DetailedLogViewModal; diff --git a/src/pages/Device/LogsCard/LogHistory/index.tsx b/src/pages/Device/LogsCard/LogHistory/index.tsx index 3fdc026..4ae9cef 100644 --- a/src/pages/Device/LogsCard/LogHistory/index.tsx +++ b/src/pages/Device/LogsCard/LogHistory/index.tsx @@ -16,7 +16,7 @@ const LogHistory = ({ serialNumber }: Props) => { const { t } = useTranslation(); const [limit, setLimit] = React.useState(25); const [hiddenColumns, setHiddenColumns] = React.useState([]); - 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" /> - + diff --git a/src/pages/Device/LogsCard/LogHistory/useDeviceLogsTable.tsx b/src/pages/Device/LogsCard/LogHistory/useDeviceLogsTable.tsx index 661fc4f..d1ccf4d 100644 --- a/src/pages/Device/LogsCard/LogHistory/useDeviceLogsTable.tsx +++ b/src/pages/Device/LogsCard/LogHistory/useDeviceLogsTable.tsx @@ -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(); 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 ? ( + + onOpen(v)} + colorScheme="blue" + icon={} + size="xs" + mr={2} + /> + + {v.log} + + + ) : ( + v.log + ), + [onOpen], + ); + const dateCell = React.useCallback( (v: number) => ( @@ -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: , }; }; diff --git a/src/pages/Device/LogsCard/index.tsx b/src/pages/Device/LogsCard/index.tsx index 0a99524..abfe7f9 100644 --- a/src/pages/Device/LogsCard/index.tsx +++ b/src/pages/Device/LogsCard/index.tsx @@ -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) => { {t('controller.devices.logs')} + + {t('devices.crash_logs')} + @@ -51,10 +55,12 @@ const DeviceLogsCard = ({ serialNumber }: Props) => { - + + +