[WIFI-11706] Fix for missing analytics in venue page

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2022-11-21 10:54:32 +00:00
parent 29c20bb79e
commit cb04468eaf
10 changed files with 385 additions and 62 deletions

4
package-lock.json generated
View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "wlan-cloud-owprov-ui",
"version": "2.8.0(13)",
"version": "2.8.0(17)",
"description": "",
"main": "index.tsx",
"scripts": {

View File

@@ -79,6 +79,7 @@
"live_view_help": "Hilfe zur Live-Ansicht",
"memory": "Erinnerung",
"memory_used": "Verwendeter Speicher",
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
"mode": "Modus",
"noise": "Lärm",
"packets": "Pakete",
@@ -613,6 +614,7 @@
"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)",
"new_devices": "Neue Geräte",
"no_model_image": "Kein Modellbild gefunden",
"not_connected": "Nicht verbunden",
"not_found_gateway": "Fehler: Gerät hat sich noch nicht mit dem Gateway verbunden",
"notifications": "Gerätebenachrichtigungen",

View File

@@ -79,6 +79,7 @@
"live_view_help": "Live View Help",
"memory": "Memory",
"memory_used": "Memory Used",
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
"mode": "Mode",
"noise": "Noise",
"packets": "Packets",
@@ -613,6 +614,7 @@
"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)",
"new_devices": "new devices",
"no_model_image": "No Model Image Found",
"not_connected": "Not Connected",
"not_found_gateway": "Error: device has not yet connected to the controller",
"notifications": "Device Notifications",

View File

@@ -79,6 +79,7 @@
"live_view_help": "Ayuda de visualización en vivo",
"memory": "Memoria",
"memory_used": "Memoria usada",
"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior",
"mode": "Modo",
"noise": "Ruido",
"packets": "Paquetes",
@@ -613,6 +614,7 @@
"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)",
"new_devices": "Nuevos dispositivos",
"no_model_image": "No se encontró ninguna imagen de modelo",
"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",

View File

@@ -79,6 +79,7 @@
"live_view_help": "Aide sur l'affichage en direct",
"memory": "mémoire",
"memory_used": "Mémoire utilisée",
"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
"mode": "Mode",
"noise": "Bruit",
"packets": "Paquets",
@@ -613,6 +614,7 @@
"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)",
"new_devices": "nouveaux appareils",
"no_model_image": "Aucune image de modèle trouvée",
"not_connected": "Pas connecté",
"not_found_gateway": "Erreur : l'appareil n'est pas encore connecté à la passerelle",
"notifications": "notifications de l'appareil",

View File

@@ -79,6 +79,7 @@
"live_view_help": "Ajuda da visualização ao vivo",
"memory": "Memória",
"memory_used": "Memória Usada",
"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior",
"mode": "Modo",
"noise": "Barulho",
"packets": "Pacotes",
@@ -613,6 +614,7 @@
"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)",
"new_devices": "novos dispositivos",
"no_model_image": "Nenhuma imagem de modelo encontrada",
"not_connected": "Não conectado",
"not_found_gateway": "Erro: o dispositivo ainda não se conectou ao gateway",
"notifications": "Notificações do dispositivo",

View File

@@ -1,45 +1,259 @@
import { useToast } from '@chakra-ui/react';
import { useMutation, useQuery } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { useTranslation } from 'react-i18next';
import { AxiosError } from 'models/Axios';
import { Note } from 'models/Note';
import { PageInfo, SortInfo } from 'models/Table';
import { axiosAnalytics } from 'utils/axiosInstances';
export type AnalyticsBoardDevice = {
associations_2g: number;
associations_5g: number;
associations_6g: number;
boardId: string;
connected: boolean;
connectionIp: string;
deviceType: string;
health: number;
lastConnection: number;
lastContact: number;
lastDisconnection: number;
lastFirmware: string;
lastFirmwareUpdate: number;
lastHealth: number;
lastPing: number;
lastState: number;
locale: string;
memory: number;
pings: number;
serialNumber: string;
states: number;
type: string;
uptime: number;
};
export type AnalyticsBoardDevicesApiResponse = {
devices: AnalyticsBoardDevice[];
};
export type AnalyticsBoardApiResponse = {
created: number;
description: string;
id: string;
modified: number;
name: string;
notes: Note[];
tags: string[];
venueList: {
description: string;
id: string;
interval: number;
monitorSubVenues: boolean;
name: string;
retention: number;
};
};
export type AnalyticsClientLifecycleApiResponse = {
ack_signal: number;
ack_signal_avg: number;
active_ms: number;
bssid: string;
busy_ms: number;
channel: number;
channel_width: number;
connected: number;
inactive: number;
ipv4: string;
ipv6: string;
mode: string;
noise: number;
receive_ms: number;
rssi: number;
rx_bitrate: number;
rx_bytes: number;
rx_chwidth: number;
rx_duration: number;
rx_mcs: number;
rx_nss: number;
rx_packets: number;
rx_vht: boolean;
ssid: string;
station_id: string;
timestamp: number;
tx_bitrate: number;
tx_bytes: number;
tx_chwidth: number;
tx_duration: number;
tx_mcs: number;
tx_nss: number;
tx_packets: number;
tx_power: number;
tx_retries: number;
tx_vht: boolean;
venue_id: string;
};
export type AnalyticsApData = {
collisions: number;
multicast: number;
rx_bytes: number;
rx_bytes_bw: number;
rx_bytes_delta: number;
rx_dropped: number;
rx_dropped_delta: number;
rx_dropped_pct: number;
rx_errors: number;
rx_errors_delta: number;
rx_errors_pct: number;
rx_packets: number;
rx_packets_bw: number;
rx_packets_delta: number;
tx_bytes: number;
tx_bytes_bw: number;
tx_bytes_delta: number;
tx_dropped: number;
tx_dropped_delta: number;
tx_dropped_pct: number;
tx_errors: number;
tx_errors_delta: number;
tx_errors_pct: number;
tx_packets: number;
tx_packets_bw: number;
tx_packets_delta: number;
};
export type AnalyticsRadioData = {
active_ms: number;
active_pct: number;
band: number;
busy_ms: number;
busy_pct: number;
channel: number;
channel_width: number;
noise: number;
receive_ms: number;
receive_pct: number;
temperature: number;
transmit_ms: number;
transmit_pct: number;
tx_power: number;
};
export type AnalyticsAssociationData = {
connected: number;
inactive: number;
rssi: number;
rx_bytes: number;
rx_bytes_bw: number;
rx_bytes_delta: number;
rx_packets: number;
rx_packets_bw: number;
rx_packets_delta: number;
rx_rate: {
bitrate: number;
chwidth: number;
ht: boolean;
mcs: number;
nss: number;
sgi: boolean;
};
station: string;
tx_bytes: number;
tx_bytes_bw: number;
tx_bytes_delta: number;
tx_duration: number;
tx_duration_delta: number;
tx_duration_pct: number;
tx_failed: number;
tx_failed_delta: number;
tx_failed_pct: number;
tx_packets: number;
tx_packets_bw: number;
tx_packets_delta: number;
tx_rate: {
bitrate: number;
chwidth: number;
ht: boolean;
mcs: number;
nss: number;
sgi: boolean;
};
tx_retries: number;
tx_retries_delta: number;
tx_retries_pct: number;
};
export type AnalyticsSsidData = {
associations: AnalyticsAssociationData[];
band: 2;
bssid: string;
channel: number;
mode: string;
rx_bytes_bw: {
avg: number;
max: number;
min: number;
};
rx_packets_bw: {
avg: number;
max: number;
min: number;
};
ssid: string;
tx_bytes_bw: {
avg: number;
max: number;
min: number;
};
tx_duration_pct: {
avg: number;
max: number;
min: number;
};
tx_failed_pct: {
avg: number;
max: number;
min: number;
};
tx_packets_bw: {
avg: number;
max: number;
min: number;
};
tx_retries_pct: {
avg: number;
max: number;
min: number;
};
};
export type AnalyticsTimePointApiResponse = {
ap_data: AnalyticsApData;
boardId: string;
device_info: AnalyticsBoardDevice;
id: string;
radio_data: AnalyticsRadioData[];
serialNumber: string;
ssid_data: AnalyticsSsidData[];
timestamp: number;
};
export type AnalyticsTimePointsApiResponse = {
points: AnalyticsTimePointApiResponse[][];
};
export const useGetAnalyticsBoard = ({ id }: { id?: string }) => {
const { t } = useTranslation();
const toast = useToast();
return useQuery(['get-board', id], () => axiosAnalytics.get(`board/${id}`).then(({ data }) => data), {
enabled: id !== undefined && id !== null && id.length > 0,
onError: (e: AxiosError) => {
if (!toast.isActive('board-fetching-error'))
toast({
id: 'board-fetching-error',
title: t('common.error'),
description: t('crud.error_fetching_obj', {
obj: t('analytics.board'),
e: e?.response?.data?.ErrorDescription,
}),
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
});
};
export const useGetAnalyticsBoardDevices = ({ id }: { id: string }) => {
const { t } = useTranslation();
const toast = useToast();
return useQuery(
['get-board-devices', id],
() => axiosAnalytics.get(`board/${id}/devices`).then(({ data }) => data.devices),
['get-board', id],
() => axiosAnalytics.get(`board/${id}`).then(({ data }: { data: AnalyticsBoardApiResponse }) => data),
{
enabled: id !== undefined && id !== null && id.length > 0,
onError: (e: AxiosError) => {
if (!toast.isActive('board-fetching-error'))
if (e.response?.status !== 404 && !toast.isActive('board-fetching-error'))
toast({
id: 'board-fetching-error',
title: t('common.error'),
@@ -57,24 +271,35 @@ export const useGetAnalyticsBoardDevices = ({ id }: { id: string }) => {
);
};
const getPartialClients = async (venueId: string, offset: number) =>
axiosAnalytics
.get(`wifiClientHistory?macsOnly=true&venue=${venueId}&limit=500&offset=${offset}`)
.then(({ data }) => data.entries as string[]);
export const useGetAnalyticsBoardDevices = ({ id }: { id?: string }) => {
const { t } = useTranslation();
const toast = useToast();
export const getAllClients = async (venueId: string) => {
const allClients: string[] = [];
let continueFirmware = true;
let offset = 0;
while (continueFirmware) {
// eslint-disable-next-line no-await-in-loop
const newClients = await getPartialClients(venueId, offset);
if (newClients === null || newClients.length === 0 || newClients.length < 500 || offset >= 50000)
continueFirmware = false;
allClients.push(...newClients);
offset += 500;
}
return allClients;
return useQuery(
['get-board-devices', id],
() =>
axiosAnalytics
.get(`board/${id}/devices`)
.then(({ data }: { data: AnalyticsBoardDevicesApiResponse }) => data.devices),
{
enabled: id !== undefined && id !== null && id.length > 0,
onError: (e: AxiosError) => {
if (e.response?.status !== 404 && !toast.isActive('board-fetching-error'))
toast({
id: 'board-fetching-error',
title: t('common.error'),
description: t('crud.error_fetching_obj', {
obj: t('analytics.board'),
e: e?.response?.data?.ErrorDescription,
}),
status: 'error',
duration: 5000,
isClosable: true,
position: 'top-right',
});
},
},
);
};
export const useGetAnalyticsClients = ({ venueId }: { venueId: string }) => {
@@ -83,7 +308,10 @@ export const useGetAnalyticsClients = ({ venueId }: { venueId: string }) => {
return useQuery(
['get-venue-analytics-clients', venueId],
() => axiosAnalytics.get(`wifiClientHistory?macsOnly=true&venue=${venueId}`).then(({ data }) => data.entries),
() =>
axiosAnalytics
.get(`wifiClientHistory?macsOnly=true&venue=${venueId}`)
.then(({ data }: { data: { entries: string[] } }) => data.entries),
{
onError: (e: AxiosError) => {
if (!toast.isActive('get-venue-analytics-clients-error'))
@@ -107,12 +335,35 @@ export const useGetAnalyticsClients = ({ venueId }: { venueId: string }) => {
export const useGetClientLifecycleTableSpecs = () =>
useQuery(
['get-lifecycles-table-spec'],
() => axiosAnalytics.get(`wifiClientHistory/0?orderSpec=true`).then(({ data }) => data.list),
() =>
axiosAnalytics
.get(`wifiClientHistory/0?orderSpec=true`)
.then(({ data }: { data: { list: string[] } }) => data.list),
{
staleTime: Infinity,
},
);
const getPartialClients = async (venueId: string, offset: number) =>
axiosAnalytics
.get(`wifiClientHistory?macsOnly=true&venue=${venueId}&limit=500&offset=${offset}`)
.then(({ data }) => data.entries as string[]);
export const getAllClients = async (venueId: string) => {
const allClients: string[] = [];
let continueFirmware = true;
let offset = 0;
while (continueFirmware) {
// eslint-disable-next-line no-await-in-loop
const newClients = await getPartialClients(venueId, offset);
if (newClients === null || newClients.length === 0 || newClients.length < 500 || offset >= 50000)
continueFirmware = false;
allClients.push(...newClients);
offset += 500;
}
return allClients;
};
export const useGetClientLifecycleCount = ({
venueId,
mac,
@@ -134,7 +385,7 @@ export const useGetClientLifecycleCount = ({
() =>
axiosAnalytics
.get(`wifiClientHistory/${mac}?venue=${venueId}&countOnly=true&fromDate=${fromDate}&endDate=${endDate}`)
.then(({ data }) => data.count),
.then(({ data }: { data: { count: number } }) => data.count),
{
enabled: mac !== undefined,
onError: (e: AxiosError) => {
@@ -190,7 +441,7 @@ export const useGetClientLifecycle = ({
(pageInfo?.limit ?? 10) * (pageInfo?.index ?? 1)
}${sortString}&fromDate=${fromDate}&endDate=${endDate}`,
)
.then(({ data }) => data.entries),
.then(({ data }: { data: { entries: AnalyticsClientLifecycleApiResponse[] } }) => data.entries),
{
keepPreviousData: true,
enabled: count !== undefined && pageInfo !== undefined,
@@ -205,7 +456,7 @@ export const useGetAnalyticsBoardTimepoints = ({
endTime,
enabled = true,
}: {
id: string;
id?: string;
startTime: Date;
endTime?: Date;
enabled?: boolean;
@@ -214,19 +465,21 @@ export const useGetAnalyticsBoardTimepoints = ({
const toast = useToast();
return useQuery(
['get-board-timepoints', id],
['get-venue-timepoints', id, startTime.toString(), endTime?.toString()],
() =>
axiosAnalytics
.get(
`board/${id}/timepoints?fromDate=${Math.floor(startTime.getTime() / 1000)}${
`board/${id}/timepoints?maxRecords=1000&fromDate=${Math.floor(startTime.getTime() / 1000)}${
endTime ? `&endDate=${Math.floor(endTime.getTime() / 1000)}` : ''
}`,
)
.then(({ data }) => data.points),
.then(({ data }: { data: AnalyticsTimePointsApiResponse }) => data.points),
{
enabled: id !== null && enabled,
enabled: id !== undefined && id !== '' && enabled,
keepPreviousData: true,
staleTime: Infinity,
onError: (e: AxiosError) => {
if (!toast.isActive('board-fetching-error'))
if (e.response?.status !== 404 && !toast.isActive('board-fetching-error'))
toast({
id: 'board-fetching-error',
title: t('common.error'),

View File

@@ -1,5 +1,17 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Center, Flex, Heading, Spacer, Spinner, useDisclosure } from '@chakra-ui/react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Center,
Flex,
Heading,
Spacer,
Spinner,
useDisclosure,
} from '@chakra-ui/react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import VenueAnalyticsHeader from './Header';
@@ -16,7 +28,7 @@ const VenueDashboard = ({ boardId }) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure(false);
const [tableOptions, setTableOptions] = useState(null);
const { data: devices, isFetching, refetch } = useGetAnalyticsBoardDevices({ id: boardId });
const { data: devices, isFetching, refetch, error } = useGetAnalyticsBoardDevices({ id: boardId });
const handleRefreshClick = () => {
refetch();
@@ -112,6 +124,21 @@ const VenueDashboard = ({ boardId }) => {
if (!isOpen) setTableOptions(null);
}, [isOpen]);
if (error)
return (
<Center mt={6}>
<Alert status="error" w="unset" borderRadius="15px">
<AlertIcon />
<Box>
<AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>
{error.response?.status === 404 ? t('analytics.missing_board') : error.response?.data?.ErrorDescription}
</AlertDescription>
</Box>
</Alert>
</Center>
);
return !devices ? (
<Center mt={6}>
<Spinner size="xl" />

View File

@@ -1,7 +1,19 @@
import React, { useState } from 'react';
import { Box, Center, Flex, Spacer, Spinner, useColorModeValue } from '@chakra-ui/react';
import {
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Box,
Center,
Flex,
Spacer,
Spinner,
useColorModeValue,
} from '@chakra-ui/react';
import PropTypes from 'prop-types';
import { FullScreen, useFullScreenHandle } from 'react-full-screen';
import { useTranslation } from 'react-i18next';
import CirclePack from './CirclePack';
import ExpandButton from './ExpandButton';
import CirclePackTimePickers from './TimePickers';
@@ -17,11 +29,32 @@ const propTypes = {
};
const VenueLiveView = ({ boardId, venue }) => {
const { t } = useTranslation();
const handle = useFullScreenHandle();
const color = useColorModeValue('gray.50', 'gray.800');
const [startTime, setStartTime] = useState(getHoursAgo(1));
const [endTime, setEndTime] = useState(new Date());
const { data: timepoints, isFetching, refetch } = useGetAnalyticsBoardTimepoints({ id: boardId, startTime, endTime });
const {
data: timepoints,
isFetching,
refetch,
error,
} = useGetAnalyticsBoardTimepoints({ id: boardId, startTime, endTime });
if (error)
return (
<Center mt={6}>
<Alert status="error" w="unset" borderRadius="15px">
<AlertIcon />
<Box>
<AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>
{error.response?.status === 404 ? t('analytics.missing_board') : error.response?.data?.ErrorDescription}
</AlertDescription>
</Box>
</Alert>
</Center>
);
return !timepoints ? (
<Center mt={6}>