mirror of
https://github.com/optim-enterprises-bv/OptimCloud-gw-ui.git
synced 2025-11-02 03:07:46 +00:00
[WIFI-11564] Add logs page
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
87
package-lock.json
generated
87
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(8)",
|
"version": "2.8.0(9)",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(8)",
|
"version": "2.8.0(9)",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.11",
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
"react-masonry-css": "^1.0.16",
|
"react-masonry-css": "^1.0.16",
|
||||||
"react-router-dom": "^6.4.2",
|
"react-router-dom": "^6.4.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"react-virtualized-auto-sizer": "^1.0.7",
|
||||||
|
"react-window": "^1.8.8",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
@@ -57,6 +59,8 @@
|
|||||||
"@types/react-csv": "^1.1.3",
|
"@types/react-csv": "^1.1.3",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react-table": "^7.7.12",
|
"@types/react-table": "^7.7.12",
|
||||||
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
"@vitejs/plugin-react": "^2.1.0",
|
||||||
"eslint": "8.25.0",
|
"eslint": "8.25.0",
|
||||||
@@ -3577,6 +3581,24 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-virtualized-auto-sizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-window": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -7937,6 +7959,34 @@
|
|||||||
"react-dom": ">=16.6.0"
|
"react-dom": ">=16.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-virtualized-auto-sizer": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc",
|
||||||
|
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-window": {
|
||||||
|
"version": "1.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||||
|
"integrity": "sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"memoize-one": ">=3.1.1 <6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/recrawl-sync": {
|
"node_modules/recrawl-sync": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -11791,6 +11841,24 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-virtualized-auto-sizer": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-GH8sAnBEM5GV9LTeiz56r4ZhMOUSrP43tAQNSRVxNexDjcNKLCEtnxusAItg1owFUFE6k0NslV26gqVClVvong==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/react-window": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/resolve": {
|
"@types/resolve": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -14355,6 +14423,21 @@
|
|||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-virtualized-auto-sizer": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
|
"react-window": {
|
||||||
|
"version": "1.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||||
|
"integrity": "sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.0.0",
|
||||||
|
"memoize-one": ">=3.1.1 <6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"recrawl-sync": {
|
"recrawl-sync": {
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(8)",
|
"version": "2.8.0(9)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
@@ -50,6 +50,8 @@
|
|||||||
"@tanstack/react-query": "^4.12.0",
|
"@tanstack/react-query": "^4.12.0",
|
||||||
"react-router-dom": "^6.4.2",
|
"react-router-dom": "^6.4.2",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"react-virtualized-auto-sizer": "^1.0.7",
|
||||||
|
"react-window": "^1.8.8",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^3.1.8",
|
"vite": "^3.1.8",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
@@ -64,6 +66,8 @@
|
|||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react-table": "^7.7.12",
|
"@types/react-table": "^7.7.12",
|
||||||
"@types/uuid": "^8.3.4",
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
|
"@types/react-window": "^1.8.5",
|
||||||
"eslint": "8.25.0",
|
"eslint": "8.25.0",
|
||||||
"vite-tsconfig-paths": "^3.5.1",
|
"vite-tsconfig-paths": "^3.5.1",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
|
|||||||
@@ -788,6 +788,22 @@
|
|||||||
"your_new_password": "Dein neues Passwort",
|
"your_new_password": "Dein neues Passwort",
|
||||||
"your_password": "Ihr Passwort"
|
"your_password": "Ihr Passwort"
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"configuration_upgrade": "Konfigurationsaktualisierung",
|
||||||
|
"device_firmware_upgrade": "Firmware-Aktualisierung",
|
||||||
|
"device_statistics": "Gerätestatistik",
|
||||||
|
"export": "Export",
|
||||||
|
"filter": "Filter",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"global_connections": "Globale Verbindungen",
|
||||||
|
"level": "Niveau",
|
||||||
|
"message": "Botschaft",
|
||||||
|
"one": "Log",
|
||||||
|
"receiving_types": "Typen empfangen",
|
||||||
|
"security": "Sicherheit",
|
||||||
|
"source": "Quelle",
|
||||||
|
"thread": "Faden"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"auto_align": "Automatisch ausrichten",
|
"auto_align": "Automatisch ausrichten",
|
||||||
"auto_map": "Automatische Karte",
|
"auto_map": "Automatische Karte",
|
||||||
@@ -804,6 +820,10 @@
|
|||||||
"title": "Karte",
|
"title": "Karte",
|
||||||
"visibility": "Sichtweite"
|
"visibility": "Sichtweite"
|
||||||
},
|
},
|
||||||
|
"notification": {
|
||||||
|
"one": "Benachrichtigung",
|
||||||
|
"other": "Benachrichtigungen"
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
|
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
|
||||||
"delete_operator": "Betreiber löschen",
|
"delete_operator": "Betreiber löschen",
|
||||||
@@ -849,6 +869,21 @@
|
|||||||
"vendor": "Verkäufer",
|
"vendor": "Verkäufer",
|
||||||
"version": "Ausführung"
|
"version": "Ausführung"
|
||||||
},
|
},
|
||||||
|
"script": {
|
||||||
|
"automatic": "Automatik",
|
||||||
|
"custom_domain": "Benutzerdefinierten Domain",
|
||||||
|
"deferred": "Aufgeschoben",
|
||||||
|
"device_title": "Führen Sie das Geräteskript aus",
|
||||||
|
"explanation": "Führen Sie ein benutzerdefiniertes Skript auf diesem Gerät aus und laden Sie die Ergebnisse herunter",
|
||||||
|
"file_too_large": "Bitte wählen Sie eine Datei aus, die kleiner als 500 KB ist",
|
||||||
|
"now": "Jetzt",
|
||||||
|
"one": "Skript",
|
||||||
|
"schedule_success": "Geplante Skriptausführung!",
|
||||||
|
"signature": "Unterschrift",
|
||||||
|
"timeout": "Auszeit",
|
||||||
|
"upload_destination": "Ziel hochladen",
|
||||||
|
"when": "Ausführung planen"
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"billing_code": "Abrechnungscode",
|
"billing_code": "Abrechnungscode",
|
||||||
"billing_frequency": "Abrechnungshäufigkeit",
|
"billing_frequency": "Abrechnungshäufigkeit",
|
||||||
|
|||||||
@@ -788,6 +788,22 @@
|
|||||||
"your_new_password": "Your new password",
|
"your_new_password": "Your new password",
|
||||||
"your_password": "Your password"
|
"your_password": "Your password"
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"configuration_upgrade": "Configuration Update",
|
||||||
|
"device_firmware_upgrade": "Firmware Upgrade",
|
||||||
|
"device_statistics": "Device Statistics",
|
||||||
|
"export": "Export",
|
||||||
|
"filter": "Filter",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"global_connections": "Global Connections",
|
||||||
|
"level": "Level",
|
||||||
|
"message": "Message",
|
||||||
|
"one": "Log",
|
||||||
|
"receiving_types": "Receiving Types",
|
||||||
|
"security": "Security",
|
||||||
|
"source": "Source",
|
||||||
|
"thread": "Thread"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"auto_align": "Auto Align",
|
"auto_align": "Auto Align",
|
||||||
"auto_map": "Auto Map",
|
"auto_map": "Auto Map",
|
||||||
@@ -804,6 +820,10 @@
|
|||||||
"title": "Map",
|
"title": "Map",
|
||||||
"visibility": "Visibility"
|
"visibility": "Visibility"
|
||||||
},
|
},
|
||||||
|
"notification": {
|
||||||
|
"one": "Notification",
|
||||||
|
"other": "Notifications"
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
|
"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
|
||||||
"delete_operator": "Delete Operator",
|
"delete_operator": "Delete Operator",
|
||||||
@@ -849,6 +869,21 @@
|
|||||||
"vendor": "Vendor",
|
"vendor": "Vendor",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
|
"script": {
|
||||||
|
"automatic": "Automatic",
|
||||||
|
"custom_domain": "Custom Domain",
|
||||||
|
"deferred": "Deferred",
|
||||||
|
"device_title": "Run Device Script",
|
||||||
|
"explanation": "Run a custom script on this device and download its results",
|
||||||
|
"file_too_large": "Please select a file that is less than 500KB",
|
||||||
|
"now": "Now",
|
||||||
|
"one": "Script",
|
||||||
|
"schedule_success": "Scheduled script execution!",
|
||||||
|
"signature": "Signature",
|
||||||
|
"timeout": "Timeout",
|
||||||
|
"upload_destination": "Upload Destination",
|
||||||
|
"when": "Schedule Execution"
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"billing_code": "Billing Code",
|
"billing_code": "Billing Code",
|
||||||
"billing_frequency": "Billing Frequency",
|
"billing_frequency": "Billing Frequency",
|
||||||
|
|||||||
@@ -788,6 +788,22 @@
|
|||||||
"your_new_password": "Tu nueva contraseña",
|
"your_new_password": "Tu nueva contraseña",
|
||||||
"your_password": "Tu contraseña"
|
"your_password": "Tu contraseña"
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"configuration_upgrade": "Actualización de configuración",
|
||||||
|
"device_firmware_upgrade": "Actualización de firmware",
|
||||||
|
"device_statistics": "Estadísticas del dispositivo",
|
||||||
|
"export": "Exportar",
|
||||||
|
"filter": "Filtrar",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"global_connections": "Conexiones globales",
|
||||||
|
"level": "Nivel",
|
||||||
|
"message": "Mensaje",
|
||||||
|
"one": "Iniciar sesión",
|
||||||
|
"receiving_types": "Tipos de recepción",
|
||||||
|
"security": "SEGURIDAD",
|
||||||
|
"source": "Fuente",
|
||||||
|
"thread": "Hilo"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"auto_align": "Alineación automática",
|
"auto_align": "Alineación automática",
|
||||||
"auto_map": "Mapa automático",
|
"auto_map": "Mapa automático",
|
||||||
@@ -804,6 +820,10 @@
|
|||||||
"title": "Mapa",
|
"title": "Mapa",
|
||||||
"visibility": "Visibilidad"
|
"visibility": "Visibilidad"
|
||||||
},
|
},
|
||||||
|
"notification": {
|
||||||
|
"one": "Notificación",
|
||||||
|
"other": "Notificaciones"
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
|
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
|
||||||
"delete_operator": "Eliminar operador",
|
"delete_operator": "Eliminar operador",
|
||||||
@@ -849,6 +869,21 @@
|
|||||||
"vendor": "Vendedor",
|
"vendor": "Vendedor",
|
||||||
"version": "Versión"
|
"version": "Versión"
|
||||||
},
|
},
|
||||||
|
"script": {
|
||||||
|
"automatic": "Automático",
|
||||||
|
"custom_domain": "Dominio personalizado",
|
||||||
|
"deferred": "Diferido",
|
||||||
|
"device_title": "Ejecutar secuencia de comandos del dispositivo",
|
||||||
|
"explanation": "Ejecute un script personalizado en este dispositivo y descargue sus resultados",
|
||||||
|
"file_too_large": "Seleccione un archivo que tenga menos de 500 KB",
|
||||||
|
"now": "ahora",
|
||||||
|
"one": "Guión",
|
||||||
|
"schedule_success": "¡Ejecución de script programada!",
|
||||||
|
"signature": "Firma",
|
||||||
|
"timeout": "Se acabó el tiempo",
|
||||||
|
"upload_destination": "Cargar destino",
|
||||||
|
"when": "Programar Ejecucion"
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"billing_code": "Código de facturación",
|
"billing_code": "Código de facturación",
|
||||||
"billing_frequency": "Frecuencia de facturación",
|
"billing_frequency": "Frecuencia de facturación",
|
||||||
|
|||||||
@@ -788,6 +788,22 @@
|
|||||||
"your_new_password": "Votre nouveau mot de passe",
|
"your_new_password": "Votre nouveau mot de passe",
|
||||||
"your_password": "Votre mot de passe"
|
"your_password": "Votre mot de passe"
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"configuration_upgrade": "Mise à jour de la configuration",
|
||||||
|
"device_firmware_upgrade": "Mise à jour du firmware",
|
||||||
|
"device_statistics": "Statistiques de l'appareil",
|
||||||
|
"export": "Exportation",
|
||||||
|
"filter": "Filtre",
|
||||||
|
"firmware": "Micrologiciel",
|
||||||
|
"global_connections": "Connexions mondiales",
|
||||||
|
"level": "Niveau",
|
||||||
|
"message": "Message",
|
||||||
|
"one": "Bûche",
|
||||||
|
"receiving_types": "Types de réception",
|
||||||
|
"security": "SÉCURITÉ",
|
||||||
|
"source": "La source",
|
||||||
|
"thread": "Fil de discussion"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"auto_align": "Alignement automatique",
|
"auto_align": "Alignement automatique",
|
||||||
"auto_map": "Carte automatique",
|
"auto_map": "Carte automatique",
|
||||||
@@ -804,6 +820,10 @@
|
|||||||
"title": "Carte",
|
"title": "Carte",
|
||||||
"visibility": "Visibilité"
|
"visibility": "Visibilité"
|
||||||
},
|
},
|
||||||
|
"notification": {
|
||||||
|
"one": "Notification",
|
||||||
|
"other": "Les notifications"
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
|
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
|
||||||
"delete_operator": "Supprimer l'opérateur",
|
"delete_operator": "Supprimer l'opérateur",
|
||||||
@@ -849,6 +869,21 @@
|
|||||||
"vendor": "vendeur",
|
"vendor": "vendeur",
|
||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
|
"script": {
|
||||||
|
"automatic": "Automatique",
|
||||||
|
"custom_domain": "Domaine personnalisé",
|
||||||
|
"deferred": "Différé",
|
||||||
|
"device_title": "Exécuter le script de périphérique",
|
||||||
|
"explanation": "Exécutez un script personnalisé sur cet appareil et téléchargez ses résultats",
|
||||||
|
"file_too_large": "Veuillez sélectionner un fichier de moins de 500 Ko",
|
||||||
|
"now": "À présent",
|
||||||
|
"one": "Scénario",
|
||||||
|
"schedule_success": "Exécution du script planifié !",
|
||||||
|
"signature": "signature",
|
||||||
|
"timeout": "Temps libre",
|
||||||
|
"upload_destination": "Destination de téléchargement",
|
||||||
|
"when": "Planifier l'exécution"
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"billing_code": "Code de facturation",
|
"billing_code": "Code de facturation",
|
||||||
"billing_frequency": "Fréquence de facturation",
|
"billing_frequency": "Fréquence de facturation",
|
||||||
|
|||||||
@@ -788,6 +788,22 @@
|
|||||||
"your_new_password": "Sua nova senha",
|
"your_new_password": "Sua nova senha",
|
||||||
"your_password": "Sua senha"
|
"your_password": "Sua senha"
|
||||||
},
|
},
|
||||||
|
"logs": {
|
||||||
|
"configuration_upgrade": "Atualização de configuração",
|
||||||
|
"device_firmware_upgrade": "Atualização de firmware",
|
||||||
|
"device_statistics": "Estatísticas do dispositivo",
|
||||||
|
"export": "Exportar",
|
||||||
|
"filter": "Filtro",
|
||||||
|
"firmware": "Firmware",
|
||||||
|
"global_connections": "Conexões Globais",
|
||||||
|
"level": "Nível",
|
||||||
|
"message": "mensagem",
|
||||||
|
"one": "Registro",
|
||||||
|
"receiving_types": "Tipos de recebimento",
|
||||||
|
"security": "SEGURANÇA",
|
||||||
|
"source": "Fonte",
|
||||||
|
"thread": "FIO"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"auto_align": "Alinhamento Automático",
|
"auto_align": "Alinhamento Automático",
|
||||||
"auto_map": "Mapa automático",
|
"auto_map": "Mapa automático",
|
||||||
@@ -804,6 +820,10 @@
|
|||||||
"title": "Mapa",
|
"title": "Mapa",
|
||||||
"visibility": "visibilidade"
|
"visibility": "visibilidade"
|
||||||
},
|
},
|
||||||
|
"notification": {
|
||||||
|
"one": "Notificação",
|
||||||
|
"other": "Notificações"
|
||||||
|
},
|
||||||
"operator": {
|
"operator": {
|
||||||
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
|
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
|
||||||
"delete_operator": "Excluir operador",
|
"delete_operator": "Excluir operador",
|
||||||
@@ -849,6 +869,21 @@
|
|||||||
"vendor": "fornecedor",
|
"vendor": "fornecedor",
|
||||||
"version": "Versão"
|
"version": "Versão"
|
||||||
},
|
},
|
||||||
|
"script": {
|
||||||
|
"automatic": "Automático",
|
||||||
|
"custom_domain": "Domínio personalizado",
|
||||||
|
"deferred": "Diferido",
|
||||||
|
"device_title": "Executar script de dispositivo",
|
||||||
|
"explanation": "Execute um script personalizado neste dispositivo e baixe seus resultados",
|
||||||
|
"file_too_large": "Selecione um arquivo com menos de 500 KB",
|
||||||
|
"now": "agora",
|
||||||
|
"one": "Roteiro",
|
||||||
|
"schedule_success": "Execução de script agendada!",
|
||||||
|
"signature": "Assinatura",
|
||||||
|
"timeout": "Tempo esgotado",
|
||||||
|
"upload_destination": "Carregar destino",
|
||||||
|
"when": "Agendar Execução"
|
||||||
|
},
|
||||||
"service": {
|
"service": {
|
||||||
"billing_code": "código de cobrança",
|
"billing_code": "código de cobrança",
|
||||||
"billing_frequency": "Freqüência de cobrança",
|
"billing_frequency": "Freqüência de cobrança",
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
|
|||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
import { AuthProvider } from 'contexts/AuthProvider';
|
import { AuthProvider } from 'contexts/AuthProvider';
|
||||||
import { ControllerSocketProvider } from 'contexts/ControllerSocketProvider';
|
import { ControllerSocketProvider } from 'contexts/ControllerSocketProvider';
|
||||||
|
import { FirmwareSocketProvider } from 'contexts/FirmwareSocketProvider';
|
||||||
import { ProvisioningSocketProvider } from 'contexts/ProvisioningSocketProvider';
|
import { ProvisioningSocketProvider } from 'contexts/ProvisioningSocketProvider';
|
||||||
|
import { SecuritySocketProvider } from 'contexts/SecuritySocketProvider';
|
||||||
import Router from 'router';
|
import Router from 'router';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
@@ -24,11 +26,15 @@ const App = () => {
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Suspense fallback={<Spinner />}>
|
<Suspense fallback={<Spinner />}>
|
||||||
<AuthProvider token={storageToken !== null ? storageToken : undefined}>
|
<AuthProvider token={storageToken !== null ? storageToken : undefined}>
|
||||||
|
<SecuritySocketProvider>
|
||||||
|
<FirmwareSocketProvider>
|
||||||
<ProvisioningSocketProvider>
|
<ProvisioningSocketProvider>
|
||||||
<ControllerSocketProvider>
|
<ControllerSocketProvider>
|
||||||
<Router />
|
<Router />
|
||||||
</ControllerSocketProvider>
|
</ControllerSocketProvider>
|
||||||
</ProvisioningSocketProvider>
|
</ProvisioningSocketProvider>
|
||||||
|
</FirmwareSocketProvider>
|
||||||
|
</SecuritySocketProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
|
|||||||
56
src/components/ShownLogsDropdown/index.tsx
Normal file
56
src/components/ShownLogsDropdown/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ChevronDownIcon } from '@chakra-ui/icons';
|
||||||
|
import { Button, Checkbox, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
availableLogTypes: { id: number; helper?: string }[];
|
||||||
|
hiddenLogIds: number[];
|
||||||
|
setHiddenLogIds: (ids: number[]) => void;
|
||||||
|
};
|
||||||
|
const ShownLogsDropdown = ({ availableLogTypes, hiddenLogIds, setHiddenLogIds }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const labels: { [key: number]: string } = {
|
||||||
|
1: t('logs.one'),
|
||||||
|
1000: t('controller.dashboard.device_dashboard_refresh'),
|
||||||
|
2000: t('logs.configuration_upgrade'),
|
||||||
|
3000: t('logs.device_firmware_upgrade'),
|
||||||
|
4000: t('common.connected'),
|
||||||
|
5000: t('common.disconnected'),
|
||||||
|
6000: t('controller.devices.new_statistics'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActive = (id: number) => !hiddenLogIds.includes(id);
|
||||||
|
const onToggle = (id: number) => () => {
|
||||||
|
if (isActive(id)) {
|
||||||
|
setHiddenLogIds([...hiddenLogIds, id]);
|
||||||
|
} else {
|
||||||
|
setHiddenLogIds(hiddenLogIds.filter((hid) => hid !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const label = (id: number, helper?: string) => {
|
||||||
|
if (labels[id] !== undefined) {
|
||||||
|
return labels[id];
|
||||||
|
}
|
||||||
|
return helper ?? id;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu closeOnSelect={false}>
|
||||||
|
<MenuButton as={Button} rightIcon={<ChevronDownIcon />} isDisabled={availableLogTypes.length === 0}>
|
||||||
|
{t('logs.receiving_types')} ({availableLogTypes.length - hiddenLogIds.length})
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{availableLogTypes.map((logType) => (
|
||||||
|
<MenuItem key={uuid()} onClick={onToggle(logType.id)} isFocusable={false}>
|
||||||
|
<Checkbox isChecked={isActive(logType.id)}>{label(logType.id, logType.helper)}</Checkbox>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShownLogsDropdown;
|
||||||
@@ -45,8 +45,8 @@ export const useControllerDeviceSearch = ({ minLength = 4 }: UseControllerDevice
|
|||||||
|
|
||||||
const onInputChange = useCallback(
|
const onInputChange = useCallback(
|
||||||
(v: string) => {
|
(v: string) => {
|
||||||
if (v !== tempValue) {
|
|
||||||
setTempValue(v);
|
setTempValue(v);
|
||||||
|
if (v !== tempValue) {
|
||||||
debounceChange(v);
|
debounceChange(v);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,71 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { QueryClient, useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useControllerStore } from './useStore';
|
import { useControllerStore } from './useStore';
|
||||||
import { SerialSearchMessage, WebSocketInitialMessage, WebSocketNotification } from './utils';
|
import { ControllerSocketRawMessage } from './utils';
|
||||||
import { axiosGw, axiosSec } from 'constants/axiosInstances';
|
import { axiosGw, axiosSec } from 'constants/axiosInstances';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
|
||||||
|
|
||||||
const extractWebSocketNotification = (
|
|
||||||
message?: WebSocketInitialMessage | SerialSearchMessage,
|
|
||||||
): WebSocketNotification | undefined => {
|
|
||||||
if (message && message.notification) {
|
|
||||||
if (message.notification.type === 'device_connection') {
|
|
||||||
return {
|
|
||||||
type: 'DEVICE_CONNECTION',
|
|
||||||
serialNumber: message.notification.content.serialNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (message.notification.type === 'device_disconnection') {
|
|
||||||
return {
|
|
||||||
type: 'DEVICE_DISCONNECTION',
|
|
||||||
serialNumber: message.notification.content.serialNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (message.notification.type === 'device_statistics') {
|
|
||||||
return {
|
|
||||||
type: 'DEVICE_STATISTICS',
|
|
||||||
serialNumber: message.notification.content.serialNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (message.notification.type === 'device_connections_statistics') {
|
|
||||||
return {
|
|
||||||
type: 'DEVICE_CONNECTIONS_STATISTICS',
|
|
||||||
statistics: message.notification.content,
|
|
||||||
serialNumber: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (message?.serialNumbers) {
|
|
||||||
return {
|
|
||||||
type: 'DEVICE_SEARCH_RESULTS',
|
|
||||||
serialNumbers: message.serialNumbers,
|
|
||||||
serialNumber: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Invalidate and update queries related to device connection status
|
|
||||||
const connectedNotification = (serialNumber: string, isConnected: boolean, queryClient: QueryClient) => {
|
|
||||||
queryClient.invalidateQueries(['device', 'status', serialNumber]);
|
|
||||||
|
|
||||||
const queries = queryClient.getQueriesData(['devices', 'all']);
|
|
||||||
|
|
||||||
for (const query of queries) {
|
|
||||||
if (query[1] && query) {
|
|
||||||
const { devicesWithStatus } = query[1] as { devicesWithStatus: DeviceWithStatus[] };
|
|
||||||
for (let i = 0; i < devicesWithStatus?.length ?? 0; i += 1) {
|
|
||||||
const device = devicesWithStatus[i];
|
|
||||||
if (device && device.serialNumber === serialNumber) {
|
|
||||||
device.connected = isConnected;
|
|
||||||
devicesWithStatus[i] = device;
|
|
||||||
queryClient.setQueryData(query[0], { ...{ devicesWithStatus: [...devicesWithStatus] } });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ControllerSocketContextReturn = Record<string, unknown>;
|
export type ControllerSocketContextReturn = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -76,43 +14,20 @@ const ControllerSocketContext = React.createContext<ControllerSocketContextRetur
|
|||||||
|
|
||||||
export const ControllerSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
export const ControllerSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||||
const { token, isUserLoaded } = useAuth();
|
const { token, isUserLoaded } = useAuth();
|
||||||
const { addMessage, isOpen, setIsOpen, setLastSearchResults, webSocket, onStartWebSocket } = useControllerStore(
|
const { addMessage, isOpen, webSocket, onStartWebSocket } = useControllerStore((state) => ({
|
||||||
(state) => ({
|
|
||||||
addMessage: state.addMessage,
|
addMessage: state.addMessage,
|
||||||
setIsOpen: state.setWebSocketOpen,
|
|
||||||
isOpen: state.isWebSocketOpen,
|
isOpen: state.isWebSocketOpen,
|
||||||
lastSearchResults: state.lastSearchResults,
|
|
||||||
setLastSearchResults: state.setLastSearchResults,
|
|
||||||
webSocket: state.webSocket,
|
webSocket: state.webSocket,
|
||||||
onStartWebSocket: state.startWebSocket,
|
onStartWebSocket: state.startWebSocket,
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const onMessage = useCallback((message: MessageEvent<string>) => {
|
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(message.data) as WebSocketInitialMessage | undefined;
|
const data = JSON.parse(message.data) as ControllerSocketRawMessage | undefined;
|
||||||
const extracted = extractWebSocketNotification(data);
|
if (data) {
|
||||||
if (extracted) {
|
addMessage(data, queryClient);
|
||||||
addMessage(extracted);
|
|
||||||
if (extracted.type === 'DEVICE_CONNECTION') {
|
|
||||||
queryClient.invalidateQueries(['device', extracted.serialNumber]);
|
|
||||||
connectedNotification(extracted.serialNumber, true, queryClient);
|
|
||||||
} else if (extracted.type === 'DEVICE_DISCONNECTION') {
|
|
||||||
queryClient.invalidateQueries(['device', extracted.serialNumber]);
|
|
||||||
connectedNotification(extracted.serialNumber, false, queryClient);
|
|
||||||
} else if (extracted.type === 'DEVICE_STATISTICS') {
|
|
||||||
queryClient.invalidateQueries(['deviceStatistics', extracted.serialNumber, 'latestHour']);
|
|
||||||
} else if (extracted.type === 'DEVICE_CONNECTIONS_STATISTICS') {
|
|
||||||
queryClient.setQueryData(['devices', 'all', 'connection-statistics'], {
|
|
||||||
connectedDevices: extracted.statistics.numberOfDevices,
|
|
||||||
connectingDevices: extracted.statistics.numberOfConnectingDevices,
|
|
||||||
averageConnectionTime: extracted.statistics.averageConnectedTime,
|
|
||||||
} as DevicesStats);
|
|
||||||
} else if (extracted.type === 'DEVICE_SEARCH_RESULTS') {
|
|
||||||
setLastSearchResults(extracted.serialNumbers);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -147,12 +62,13 @@ export const ControllerSocketProvider = ({ children }: { children: React.ReactEl
|
|||||||
|
|
||||||
if (webSocket) {
|
if (webSocket) {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
timeoutId = setTimeout(() => {
|
/* timeoutId = setTimeout(() => {
|
||||||
if (webSocket) webSocket.onclose = () => {};
|
if (webSocket) webSocket.onclose = () => {};
|
||||||
webSocket?.close();
|
webSocket?.close();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, 5000);
|
}, 5000); */
|
||||||
} else {
|
} else {
|
||||||
|
// If tab is active again, verify if browser killed the WS
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!isOpen && isUserLoaded && axiosGw?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
if (!isOpen && isUserLoaded && axiosGw?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||||
|
|||||||
@@ -1,19 +1,117 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import { SocketEventCallback, WebSocketNotification } from './utils';
|
import { ControllerSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
|
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
||||||
|
import { NotificationType } from 'models/Socket';
|
||||||
|
|
||||||
export type WebSocketMessage =
|
export type ControllerWebSocketMessage =
|
||||||
| {
|
| {
|
||||||
type: 'NOTIFICATION';
|
type: 'NOTIFICATION';
|
||||||
data: WebSocketNotification;
|
data: SocketWebSocketNotificationData;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
}
|
}
|
||||||
| { type: 'UNKNOWN'; data: Record<string, unknown>; timestamp: Date };
|
| {
|
||||||
|
type: 'UNKNOWN';
|
||||||
|
data: {
|
||||||
|
type?: string;
|
||||||
|
type_id?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawWebSocketMessage = (
|
||||||
|
message?: ControllerSocketRawMessage,
|
||||||
|
): SocketWebSocketNotificationData | undefined => {
|
||||||
|
if (message && message.notification) {
|
||||||
|
if (message.notification.type_id === 4000 || message.notification.type === 'device_connection') {
|
||||||
|
return {
|
||||||
|
type: 'DEVICE_CONNECTION',
|
||||||
|
serialNumber: message.notification.content.serialNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.notification.type_id === 5000 || message.notification.type === 'device_disconnection') {
|
||||||
|
return {
|
||||||
|
type: 'DEVICE_DISCONNECTION',
|
||||||
|
serialNumber: message.notification.content.serialNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.notification.type_id === 6000 || message.notification.type === 'device_statistics') {
|
||||||
|
return {
|
||||||
|
type: 'DEVICE_STATISTICS',
|
||||||
|
serialNumber: message.notification.content.serialNumber,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.notification.type_id === 1000 || message.notification.type === 'device_connections_statistics') {
|
||||||
|
return {
|
||||||
|
type: 'DEVICE_CONNECTIONS_STATISTICS',
|
||||||
|
statistics: message.notification.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.notification.type_id === 1) {
|
||||||
|
return {
|
||||||
|
type: 'LOG',
|
||||||
|
log: message.notification.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (message?.serialNumbers) {
|
||||||
|
return {
|
||||||
|
type: 'DEVICE_SEARCH_RESULTS',
|
||||||
|
serialNumbers: message.serialNumbers,
|
||||||
|
};
|
||||||
|
} else if (message?.notificationTypes) {
|
||||||
|
return {
|
||||||
|
type: 'INITIAL_MESSAGE',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invalidate and update queries related to device connection status
|
||||||
|
const handleConnectionNotification = (serialNumber: string, isConnected: boolean, queryClient: QueryClient) => {
|
||||||
|
queryClient.invalidateQueries(['device', serialNumber]);
|
||||||
|
queryClient.invalidateQueries(['device', 'status', serialNumber]);
|
||||||
|
|
||||||
|
const queries = queryClient.getQueriesData(['devices', 'all']);
|
||||||
|
|
||||||
|
for (const query of queries) {
|
||||||
|
if (query[1] && query) {
|
||||||
|
const { devicesWithStatus } = query[1] as { devicesWithStatus: DeviceWithStatus[] };
|
||||||
|
for (let i = 0; i < devicesWithStatus?.length ?? 0; i += 1) {
|
||||||
|
const device = devicesWithStatus[i];
|
||||||
|
if (device && device.serialNumber === serialNumber) {
|
||||||
|
device.connected = isConnected;
|
||||||
|
devicesWithStatus[i] = device;
|
||||||
|
queryClient.setQueryData(query[0], { ...{ devicesWithStatus: [...devicesWithStatus] } });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invalidate latest device stats
|
||||||
|
const handleDeviceStatsNotification = (serialNumber: string, queryClient: QueryClient) => {
|
||||||
|
queryClient.invalidateQueries(['deviceStatistics', serialNumber, 'latestHour']);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set new global connection stats
|
||||||
|
const handleGlobalConnectionStats = (stats: DevicesStats, queryClient: QueryClient) => {
|
||||||
|
queryClient.setQueryData(['devices', 'all', 'connection-statistics'], stats);
|
||||||
|
};
|
||||||
|
|
||||||
export type ControllerStoreState = {
|
export type ControllerStoreState = {
|
||||||
lastMessage?: WebSocketMessage;
|
availableLogTypes: NotificationType[];
|
||||||
allMessages: WebSocketMessage[];
|
hiddenLogIds: number[];
|
||||||
addMessage: (message: WebSocketNotification) => void;
|
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||||
|
lastMessage?: ControllerWebSocketMessage;
|
||||||
|
allMessages: ControllerWebSocketMessage[];
|
||||||
|
addMessage: (rawMsg: ControllerSocketRawMessage, queryClient: QueryClient) => void;
|
||||||
eventListeners: SocketEventCallback[];
|
eventListeners: SocketEventCallback[];
|
||||||
addEventListeners: (callback: SocketEventCallback[]) => void;
|
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||||
webSocket?: WebSocket;
|
webSocket?: WebSocket;
|
||||||
@@ -23,15 +121,50 @@ export type ControllerStoreState = {
|
|||||||
setWebSocketOpen: (isOpen: boolean) => void;
|
setWebSocketOpen: (isOpen: boolean) => void;
|
||||||
lastSearchResults: string[];
|
lastSearchResults: string[];
|
||||||
setLastSearchResults: (result: string[]) => void;
|
setLastSearchResults: (result: string[]) => void;
|
||||||
|
errors: { str: string; timestamp: Date }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||||
allMessages: [] as WebSocketMessage[],
|
availableLogTypes: [],
|
||||||
addMessage: (msg: WebSocketNotification) => {
|
hiddenLogIds: [],
|
||||||
const obj: WebSocketMessage = {
|
setHiddenLogIds: (logsToHide: number[]) => {
|
||||||
|
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||||
|
set(() => ({
|
||||||
|
hiddenLogIds: logsToHide,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
allMessages: [] as ControllerWebSocketMessage[],
|
||||||
|
addMessage: (rawMsg: ControllerSocketRawMessage, queryClient: QueryClient) => {
|
||||||
|
try {
|
||||||
|
const msg = parseRawWebSocketMessage(rawMsg);
|
||||||
|
if (msg) {
|
||||||
|
// Handle notification-specific logic
|
||||||
|
if (msg.type === 'DEVICE_CONNECTION' || msg.type === 'DEVICE_DISCONNECTION') {
|
||||||
|
handleConnectionNotification(msg.serialNumber, true, queryClient);
|
||||||
|
} else if (msg.type === 'DEVICE_STATISTICS') {
|
||||||
|
handleDeviceStatsNotification(msg.serialNumber, queryClient);
|
||||||
|
} else if (msg.type === 'DEVICE_CONNECTIONS_STATISTICS') {
|
||||||
|
handleGlobalConnectionStats(
|
||||||
|
{
|
||||||
|
connectedDevices: msg.statistics.numberOfDevices,
|
||||||
|
connectingDevices: msg.statistics.numberOfConnectingDevices,
|
||||||
|
averageConnectionTime: msg.statistics.averageConnectedTime,
|
||||||
|
},
|
||||||
|
queryClient,
|
||||||
|
);
|
||||||
|
} else if (msg.type === 'DEVICE_SEARCH_RESULTS') {
|
||||||
|
set({ lastSearchResults: msg.serialNumbers });
|
||||||
|
} else if (msg.type === 'INITIAL_MESSAGE') {
|
||||||
|
if (msg.message.notificationTypes) {
|
||||||
|
set({ availableLogTypes: msg.message.notificationTypes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// General handling
|
||||||
|
const obj: ControllerWebSocketMessage = {
|
||||||
type: 'NOTIFICATION',
|
type: 'NOTIFICATION',
|
||||||
data: msg,
|
data: msg,
|
||||||
timestamp: new Date(),
|
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||||
|
id: uuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventsToFire = get().eventListeners.filter(
|
const eventsToFire = get().eventListeners.filter(
|
||||||
@@ -47,7 +180,9 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
allMessages:
|
allMessages:
|
||||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
lastMessage: obj,
|
lastMessage: obj,
|
||||||
eventListeners: get().eventListeners.filter(({ id }) => !eventsToFire.find(({ id: findId }) => findId === id)),
|
eventListeners: get().eventListeners.filter(
|
||||||
|
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +191,14 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
lastMessage: obj,
|
lastMessage: obj,
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
// TODO - Add error message to socket logs
|
||||||
|
return set((state) => ({
|
||||||
|
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
eventListeners: [] as SocketEventCallback[],
|
eventListeners: [] as SocketEventCallback[],
|
||||||
addEventListeners: (events: SocketEventCallback[]) =>
|
addEventListeners: (events: SocketEventCallback[]) =>
|
||||||
@@ -71,7 +214,9 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
if (tries <= 10) {
|
if (tries <= 10) {
|
||||||
set({
|
set({
|
||||||
webSocket: new WebSocket(
|
webSocket: new WebSocket(
|
||||||
`${axiosGw?.defaults?.baseURL ? axiosGw.defaults.baseURL.replace('https', 'wss') : ''}/ws`,
|
`${
|
||||||
|
axiosGw?.defaults?.baseURL ? axiosGw.defaults.baseURL.replace('https', 'wss').replace('http', 'ws') : ''
|
||||||
|
}/ws`,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
const ws = get().webSocket;
|
const ws = get().webSocket;
|
||||||
@@ -89,4 +234,5 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
lastSearchResults: [] as string[],
|
lastSearchResults: [] as string[],
|
||||||
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||||
|
errors: [],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,36 +1,83 @@
|
|||||||
|
import { InitialSocketMessage } from 'models/Socket';
|
||||||
|
|
||||||
|
export type ControllerSocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||||
|
export const ControllerSocketNotificationTypeMap = {
|
||||||
|
1: 'logs',
|
||||||
|
1000: 'device_connections_statistics',
|
||||||
|
2000: 'device_configuration_upgrade',
|
||||||
|
3000: 'device_firmware_upgrade',
|
||||||
|
4000: 'device_connection',
|
||||||
|
5000: 'device_disconnection',
|
||||||
|
6000: 'device_statistics',
|
||||||
|
};
|
||||||
|
|
||||||
type ConnectionMessage = {
|
type ConnectionMessage = {
|
||||||
notification: {
|
notification: {
|
||||||
notificationId: number;
|
notificationId: number;
|
||||||
type: 'device_disconnection' | 'device_connection' | 'device_statistics';
|
type?: 'device_disconnection' | 'device_connection' | 'device_statistics';
|
||||||
|
type_id?: 4000 | 5000 | 6000;
|
||||||
content: {
|
content: {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogMessage = {
|
||||||
|
notification: {
|
||||||
|
notificationId: number;
|
||||||
|
type?: undefined;
|
||||||
|
type_id: 1;
|
||||||
|
content: {
|
||||||
|
level: LogLevel;
|
||||||
|
msg: string;
|
||||||
|
source: string;
|
||||||
|
thread_id: number;
|
||||||
|
thread_name: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ConnectionStatisticsMessage = {
|
type ConnectionStatisticsMessage = {
|
||||||
notification: {
|
notification: {
|
||||||
notificationId: number;
|
notificationId: number;
|
||||||
type: 'device_connections_statistics';
|
type?: 'device_connections_statistics';
|
||||||
|
type_id?: 1000;
|
||||||
content: {
|
content: {
|
||||||
|
serialNumber?: undefined;
|
||||||
numberOfDevices: number;
|
numberOfDevices: number;
|
||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SerialSearchMessage = {
|
export type SerialSearchMessage = {
|
||||||
serialNumbers: string[];
|
serialNumbers: string[];
|
||||||
notification: undefined;
|
notification?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebSocketInitialMessage = ConnectionMessage | ConnectionStatisticsMessage;
|
export type ControllerSocketRawMessage =
|
||||||
|
| Partial<ConnectionMessage>
|
||||||
|
| Partial<ConnectionStatisticsMessage>
|
||||||
|
| Partial<LogMessage>
|
||||||
|
| InitialSocketMessage
|
||||||
|
| SerialSearchMessage;
|
||||||
|
|
||||||
export type WebSocketNotification =
|
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||||
|
|
||||||
|
export type SocketWebSocketNotificationData =
|
||||||
| {
|
| {
|
||||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION' | 'DEVICE_STATISTICS';
|
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION' | 'DEVICE_STATISTICS';
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
log?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'DEVICE_CONNECTIONS_STATISTICS';
|
type: 'DEVICE_CONNECTIONS_STATISTICS';
|
||||||
@@ -39,12 +86,31 @@ export type WebSocketNotification =
|
|||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
};
|
};
|
||||||
serialNumber: undefined;
|
serialNumber?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'DEVICE_SEARCH_RESULTS';
|
type: 'DEVICE_SEARCH_RESULTS';
|
||||||
serialNumbers: string[];
|
serialNumbers: string[];
|
||||||
serialNumber: undefined;
|
serialNumber?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'LOG';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log: LogMessage['notification']['content'];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'INITIAL_MESSAGE';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
message: InitialSocketMessage;
|
||||||
};
|
};
|
||||||
export type SocketEventCallback = {
|
export type SocketEventCallback = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
92
src/contexts/FirmwareSocketProvider/index.tsx
Normal file
92
src/contexts/FirmwareSocketProvider/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useFirmwareStore } from './useStore';
|
||||||
|
import { FirmwareSocketRawMessage } from './utils';
|
||||||
|
import { axiosFms, axiosSec } from 'constants/axiosInstances';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
export type FirmwareSocketContextReturn = Record<string, unknown>;
|
||||||
|
|
||||||
|
const FirmwareSocketContext = React.createContext<FirmwareSocketContextReturn>({
|
||||||
|
webSocket: undefined,
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FirmwareSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||||
|
const { token, isUserLoaded } = useAuth();
|
||||||
|
const { addMessage, isOpen, webSocket, onStartWebSocket } = useFirmwareStore((state) => ({
|
||||||
|
addMessage: state.addMessage,
|
||||||
|
isOpen: state.isWebSocketOpen,
|
||||||
|
webSocket: state.webSocket,
|
||||||
|
onStartWebSocket: state.startWebSocket,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message.data) as FirmwareSocketRawMessage | undefined;
|
||||||
|
if (data) {
|
||||||
|
addMessage(data, queryClient);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// useEffect for created the WebSocket and 'storing' it in useRef
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUserLoaded && axiosFms?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||||
|
onStartWebSocket(token ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsCurrent = webSocket;
|
||||||
|
return () => wsCurrent?.close();
|
||||||
|
}, [isUserLoaded]);
|
||||||
|
|
||||||
|
// useEffect for generating global notifications
|
||||||
|
useEffect(() => {
|
||||||
|
if (webSocket) {
|
||||||
|
webSocket.addEventListener('message', onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (webSocket) webSocket.removeEventListener('message', onMessage);
|
||||||
|
};
|
||||||
|
}, [webSocket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
if (webSocket) {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
/* timeoutId = setTimeout(() => {
|
||||||
|
if (webSocket) webSocket.onclose = () => {};
|
||||||
|
webSocket?.close();
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 5000); */
|
||||||
|
} else {
|
||||||
|
// If tab is active again, verify if browser killed the WS
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!isOpen && isUserLoaded && axiosFms?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||||
|
onStartWebSocket(token ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [webSocket, isOpen]);
|
||||||
|
|
||||||
|
const values: FirmwareSocketContextReturn = useMemo(() => ({}), []);
|
||||||
|
|
||||||
|
return <FirmwareSocketContext.Provider value={values}>{children}</FirmwareSocketContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGlobalFirmwareSocket: () => FirmwareSocketContextReturn = () => React.useContext(FirmwareSocketContext);
|
||||||
158
src/contexts/FirmwareSocketProvider/useStore.ts
Normal file
158
src/contexts/FirmwareSocketProvider/useStore.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import create from 'zustand';
|
||||||
|
import { FirmwareSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||||
|
import { axiosFms } from 'constants/axiosInstances';
|
||||||
|
import { NotificationType } from 'models/Socket';
|
||||||
|
|
||||||
|
export type FirmwareWebSocketMessage =
|
||||||
|
| {
|
||||||
|
type: 'NOTIFICATION';
|
||||||
|
data: SocketWebSocketNotificationData;
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'UNKNOWN';
|
||||||
|
data: {
|
||||||
|
type?: string;
|
||||||
|
type_id?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawWebSocketMessage = (message?: FirmwareSocketRawMessage): SocketWebSocketNotificationData | undefined => {
|
||||||
|
if (message && message.notification) {
|
||||||
|
if (message.notification.type_id === 1) {
|
||||||
|
return {
|
||||||
|
type: 'LOG',
|
||||||
|
log: message.notification.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (message?.notificationTypes) {
|
||||||
|
return {
|
||||||
|
type: 'INITIAL_MESSAGE',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FirmwareStoreState = {
|
||||||
|
availableLogTypes: NotificationType[];
|
||||||
|
hiddenLogIds: number[];
|
||||||
|
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||||
|
lastMessage?: FirmwareWebSocketMessage;
|
||||||
|
allMessages: FirmwareWebSocketMessage[];
|
||||||
|
addMessage: (rawMsg: FirmwareSocketRawMessage, queryClient: QueryClient) => void;
|
||||||
|
eventListeners: SocketEventCallback[];
|
||||||
|
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||||
|
webSocket?: WebSocket;
|
||||||
|
send: (str: string) => void;
|
||||||
|
startWebSocket: (token: string, tries?: number) => void;
|
||||||
|
isWebSocketOpen: boolean;
|
||||||
|
setWebSocketOpen: (isOpen: boolean) => void;
|
||||||
|
lastSearchResults: string[];
|
||||||
|
setLastSearchResults: (result: string[]) => void;
|
||||||
|
errors: { str: string; timestamp: Date }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFirmwareStore = create<FirmwareStoreState>((set, get) => ({
|
||||||
|
availableLogTypes: [],
|
||||||
|
hiddenLogIds: [],
|
||||||
|
setHiddenLogIds: (logsToHide: number[]) => {
|
||||||
|
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||||
|
set(() => ({
|
||||||
|
hiddenLogIds: logsToHide,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
allMessages: [] as FirmwareWebSocketMessage[],
|
||||||
|
addMessage: (rawMsg: FirmwareSocketRawMessage) => {
|
||||||
|
try {
|
||||||
|
const msg = parseRawWebSocketMessage(rawMsg);
|
||||||
|
if (msg) {
|
||||||
|
// Handle notification-specific logic
|
||||||
|
if (msg.type === 'INITIAL_MESSAGE') {
|
||||||
|
if (msg.message.notificationTypes) {
|
||||||
|
set({ availableLogTypes: msg.message.notificationTypes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// General handling
|
||||||
|
const obj: FirmwareWebSocketMessage = {
|
||||||
|
type: 'NOTIFICATION',
|
||||||
|
data: msg,
|
||||||
|
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||||
|
id: uuid(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventsToFire = get().eventListeners.filter(
|
||||||
|
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventsToFire.length > 0) {
|
||||||
|
for (const event of eventsToFire) {
|
||||||
|
event.callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return set((state) => ({
|
||||||
|
allMessages:
|
||||||
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
|
lastMessage: obj,
|
||||||
|
eventListeners: get().eventListeners.filter(
|
||||||
|
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return set((state) => ({
|
||||||
|
allMessages:
|
||||||
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
|
lastMessage: obj,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
// TODO - Add error message to socket logs
|
||||||
|
return set((state) => ({
|
||||||
|
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventListeners: [] as SocketEventCallback[],
|
||||||
|
addEventListeners: (events: SocketEventCallback[]) =>
|
||||||
|
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||||
|
isWebSocketOpen: false,
|
||||||
|
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||||
|
send: (str: string) => {
|
||||||
|
const ws = get().webSocket;
|
||||||
|
if (ws) ws.send(str);
|
||||||
|
},
|
||||||
|
startWebSocket: (token: string, tries = 0) => {
|
||||||
|
const newTries = tries + 1;
|
||||||
|
if (tries <= 10) {
|
||||||
|
set({
|
||||||
|
webSocket: new WebSocket(
|
||||||
|
`${
|
||||||
|
axiosFms?.defaults?.baseURL ? axiosFms.defaults.baseURL.replace('https', 'wss').replace('http', 'ws') : ''
|
||||||
|
}/ws`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const ws = get().webSocket;
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = () => {
|
||||||
|
set({ isWebSocketOpen: true });
|
||||||
|
ws.send(`token:${token}`);
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
set({ isWebSocketOpen: false });
|
||||||
|
setTimeout(() => get().startWebSocket(token, newTries), 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastSearchResults: [] as string[],
|
||||||
|
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||||
|
errors: [],
|
||||||
|
}));
|
||||||
51
src/contexts/FirmwareSocketProvider/utils.ts
Normal file
51
src/contexts/FirmwareSocketProvider/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { InitialSocketMessage } from 'models/Socket';
|
||||||
|
|
||||||
|
export type FirmwareSocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||||
|
export const FirmwareSocketNotificationTypeMap = {
|
||||||
|
1: 'logs',
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogMessage = {
|
||||||
|
notification: {
|
||||||
|
notificationId: number;
|
||||||
|
type?: undefined;
|
||||||
|
type_id: 1;
|
||||||
|
content: {
|
||||||
|
level: LogLevel;
|
||||||
|
msg: string;
|
||||||
|
source: string;
|
||||||
|
thread_id: number;
|
||||||
|
thread_name: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FirmwareSocketRawMessage = Partial<LogMessage> | InitialSocketMessage;
|
||||||
|
|
||||||
|
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||||
|
|
||||||
|
export type SocketWebSocketNotificationData =
|
||||||
|
| {
|
||||||
|
type: 'LOG';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log: LogMessage['notification']['content'];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'INITIAL_MESSAGE';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
message: InitialSocketMessage;
|
||||||
|
};
|
||||||
|
export type SocketEventCallback = {
|
||||||
|
id: string;
|
||||||
|
type: 'LOG';
|
||||||
|
serialNumber: string;
|
||||||
|
callback: () => void;
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import debounce from '../../../../helpers/debounce';
|
||||||
import useWebSocketCommand from './useWebSocketCommand';
|
import useWebSocketCommand from './useWebSocketCommand';
|
||||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
import debounce from 'helpers/debounce';
|
|
||||||
|
|
||||||
export type UseDeviceSearchProps = {
|
export type UseDeviceSearchProps = {
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
@@ -14,7 +14,7 @@ export const useProviderDeviceSearch = ({ minLength = 4, operatorId }: UseDevice
|
|||||||
{ command: string; serial_prefix: string; operatorId?: string } | undefined
|
{ command: string; serial_prefix: string; operatorId?: string } | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [results, setResults] = useState<string[]>([]);
|
const [results, setResults] = useState<string[]>([]);
|
||||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||||
if (newResult.response.serialNumbers) setResults(newResult.response.serialNumbers as string[]);
|
if (newResult.response.serialNumbers) setResults(newResult.response.serialNumbers as string[]);
|
||||||
};
|
};
|
||||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import useWebSocketCommand from './useWebSocketCommand';
|
import useWebSocketCommand from './useWebSocketCommand';
|
||||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
import debounce from 'helpers/debounce';
|
import debounce from 'helpers/debounce';
|
||||||
|
|
||||||
export type UseLocationSearchProps = {
|
export type UseLocationSearchProps = {
|
||||||
@@ -11,7 +11,7 @@ export const useLocationSearch = ({ minLength = 8 }: UseLocationSearchProps) =>
|
|||||||
const [tempValue, setTempValue] = useState('');
|
const [tempValue, setTempValue] = useState('');
|
||||||
const [waitingSearch, setWaitingSearch] = useState<{ command: string; address: string } | undefined>(undefined);
|
const [waitingSearch, setWaitingSearch] = useState<{ command: string; address: string } | undefined>(undefined);
|
||||||
const [results, setResults] = useState<string[]>([]);
|
const [results, setResults] = useState<string[]>([]);
|
||||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||||
if (newResult.response.results) setResults(newResult.response.results as string[]);
|
if (newResult.response.results) setResults(newResult.response.results as string[]);
|
||||||
};
|
};
|
||||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import useWebSocketCommand from './useWebSocketCommand';
|
import useWebSocketCommand from './useWebSocketCommand';
|
||||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
import debounce from 'helpers/debounce';
|
import debounce from 'helpers/debounce';
|
||||||
import { Subscriber } from 'models/Subscriber';
|
import { Subscriber } from 'models/Subscriber';
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ export const useSubscriberSearch = ({ minLength = 4, operatorId, mode }: UseSubs
|
|||||||
{ command: string; emailSearch?: string; nameSearch?: string; operatorId?: string } | undefined
|
{ command: string; emailSearch?: string; nameSearch?: string; operatorId?: string } | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [results, setResults] = useState<Subscriber[]>([]);
|
const [results, setResults] = useState<Subscriber[]>([]);
|
||||||
const onNewResult = (newResult: ProviderCommandResponse) => {
|
const onNewResult = (newResult: ProvisioningCommandResponse) => {
|
||||||
if (newResult.response.users) setResults(newResult.response.users as Subscriber[]);
|
if (newResult.response.users) setResults(newResult.response.users as Subscriber[]);
|
||||||
};
|
};
|
||||||
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
const { isOpen, send } = useWebSocketCommand({ callback: onNewResult });
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useProviderStore } from 'contexts/ProvisioningSocketProvider/useStore';
|
import { useProvisioningStore } from '../../useStore';
|
||||||
import { ProviderCommandResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningCommandResponse } from '../../utils';
|
||||||
import { randomIntId } from 'helpers/stringHelper';
|
import { randomIntId } from 'helpers/stringHelper';
|
||||||
|
|
||||||
const useProviderWebSocketCommand = ({ callback }: { callback: (command: ProviderCommandResponse) => void }) => {
|
const useProviderWebSocketCommand = ({ callback }: { callback: (command: ProvisioningCommandResponse) => void }) => {
|
||||||
const { isOpen, webSocket, lastMessage } = useProviderStore((state) => ({
|
const { isOpen, webSocket, lastMessage } = useProvisioningStore((state) => ({
|
||||||
isOpen: state.isWebSocketOpen,
|
isOpen: state.isWebSocketOpen,
|
||||||
webSocket: state.webSocket,
|
webSocket: state.webSocket,
|
||||||
lastMessage: state.lastMessage,
|
lastMessage: state.lastMessage,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notification: ProviderWebSocketVenueUpdateResponse;
|
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigurationPushesNotificationContent = ({ notification }: Props) => {
|
const ConfigurationPushesNotificationContent = ({ notification }: Props) => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notification: ProviderWebSocketVenueUpdateResponse;
|
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeviceRebootNotificationContent = ({ notification }: Props) => {
|
const DeviceRebootNotificationContent = ({ notification }: Props) => {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import React from 'react';
|
|||||||
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
import { Box, Heading, ListItem, UnorderedList } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notification: ProviderWebSocketVenueUpdateResponse;
|
notification: ProvisioningVenueNotificationMessage['notification'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeviceUpgradeNotificationContent = ({ notification }: Props) => {
|
const DeviceUpgradeNotificationContent = ({ notification }: Props) => {
|
||||||
|
|||||||
@@ -2,24 +2,24 @@ import React from 'react';
|
|||||||
import ConfigurationPushesNotificationContent from './ConfigurationPushes';
|
import ConfigurationPushesNotificationContent from './ConfigurationPushes';
|
||||||
import DeviceRebootNotificationContent from './DeviceReboot';
|
import DeviceRebootNotificationContent from './DeviceReboot';
|
||||||
import DeviceUpgradeNotificationContent from './DeviceUpgrade';
|
import DeviceUpgradeNotificationContent from './DeviceUpgrade';
|
||||||
import { ProviderWebSocketVenueUpdateResponse } from 'contexts/ProvisioningSocketProvider/utils';
|
import { ProvisioningVenueNotificationMessage } from 'contexts/ProvisioningSocketProvider/utils';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
notification?: ProviderWebSocketVenueUpdateResponse;
|
notification?: ProvisioningVenueNotificationMessage['notification'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationContent = ({ notification }: Props) => {
|
const NotificationContent = ({ notification }: Props) => {
|
||||||
if (!notification) return null;
|
if (!notification) return null;
|
||||||
|
|
||||||
if (notification.type === 'entity_configuration_update' || notification.type === 'venue_configuration_update') {
|
if (notification.type_id === 2000 || notification.type === 'venue_config_update') {
|
||||||
return <ConfigurationPushesNotificationContent notification={notification} />;
|
return <ConfigurationPushesNotificationContent notification={notification} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.type === 'venue_rebooter') {
|
if (notification.type_id === 3000 || notification.type === 'venue_rebooter') {
|
||||||
return <DeviceRebootNotificationContent notification={notification} />;
|
return <DeviceRebootNotificationContent notification={notification} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notification.type === 'venue_upgrader') {
|
if (notification.type_id === 1000 || notification.type === 'venue_fw_upgrade') {
|
||||||
return <DeviceUpgradeNotificationContent notification={notification} />;
|
return <DeviceUpgradeNotificationContent notification={notification} />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { StringMap, TOptions } from 'i18next';
|
import { TOptions } from 'i18next';
|
||||||
import { X } from 'phosphor-react';
|
import { X } from 'phosphor-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ProviderWebSocketVenueUpdateResponse } from '../../utils';
|
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
||||||
import NotificationContent from '.';
|
import NotificationContent from '.';
|
||||||
|
|
||||||
const getStatusFromNotification = (notification: ProviderWebSocketVenueUpdateResponse) => {
|
const getStatusFromNotification = (notification: ProvisioningVenueNotificationMessage['notification']) => {
|
||||||
let status: 'success' | 'warning' | 'error' = 'success';
|
let status: 'success' | 'warning' | 'error' = 'success';
|
||||||
if (notification.content.warning?.length > 0) status = 'warning';
|
if (notification.content.warning?.length > 0) status = 'warning';
|
||||||
if (notification.content.error?.length > 0) status = 'error';
|
if (notification.content.error?.length > 0) status = 'error';
|
||||||
@@ -35,15 +35,10 @@ const getStatusFromNotification = (notification: ProviderWebSocketVenueUpdateRes
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getNotificationDescription = (
|
const getNotificationDescription = (
|
||||||
t: (key: string, options?: string | TOptions<StringMap> | undefined) => string,
|
t: (key: string, options?: string | TOptions<Record<string, number>> | undefined) => string,
|
||||||
notification: ProviderWebSocketVenueUpdateResponse,
|
notification: ProvisioningVenueNotificationMessage['notification'],
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (notification.type_id === 1000 || notification.type_id === 2000 || notification.type_id === 3000) {
|
||||||
notification.content.type === 'venue_configuration_update' ||
|
|
||||||
notification.content.type === 'entity_configuration_update' ||
|
|
||||||
notification.content.type === 'venue_rebooter' ||
|
|
||||||
notification.content.type === 'venue_upgrader'
|
|
||||||
) {
|
|
||||||
return t('configurations.notification_details', {
|
return t('configurations.notification_details', {
|
||||||
success: notification.content.success?.length ?? 0,
|
success: notification.content.success?.length ?? 0,
|
||||||
warning: notification.content.warning?.length ?? 0,
|
warning: notification.content.warning?.length ?? 0,
|
||||||
@@ -56,16 +51,19 @@ const getNotificationDescription = (
|
|||||||
const useWebSocketNotification = () => {
|
const useWebSocketNotification = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [notif, setNotif] = useState<ProviderWebSocketVenueUpdateResponse | undefined>(undefined);
|
const [notif, setNotif] = useState<ProvisioningVenueNotificationMessage['notification'] | undefined>(undefined);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const openDetails = useCallback((newObj: ProviderWebSocketVenueUpdateResponse, closeToast?: () => void) => {
|
const openDetails = useCallback(
|
||||||
|
(newObj: ProvisioningVenueNotificationMessage['notification'], closeToast?: () => void) => {
|
||||||
setNotif(newObj);
|
setNotif(newObj);
|
||||||
if (closeToast) closeToast();
|
if (closeToast) closeToast();
|
||||||
onOpen();
|
onOpen();
|
||||||
}, []);
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const pushNotification = useCallback((notification: ProviderWebSocketVenueUpdateResponse) => {
|
const pushNotification = useCallback((notification: ProvisioningVenueNotificationMessage['notification']) => {
|
||||||
toast({
|
toast({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import useWebSocketNotification from './hooks/NotificationContent/useWebSocketNotification';
|
import useWebSocketNotification from './hooks/NotificationContent/useWebSocketNotification';
|
||||||
import { useProviderStore } from './useStore';
|
import { useProvisioningStore } from './useStore';
|
||||||
import { extractProviderWebSocketResponse } from './utils';
|
import { ProvisioningSocketRawMessage } from './utils';
|
||||||
import { axiosProv, axiosSec } from 'constants/axiosInstances';
|
import { axiosProv, axiosSec } from 'constants/axiosInstances';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
export type ProviderSocketContextReturn = Record<string, unknown>;
|
export type ProvisioningSocketContextReturn = Record<string, unknown>;
|
||||||
|
|
||||||
const ProviderSocketContext = React.createContext<ProviderSocketContextReturn>({
|
const ProvisioningSocketContext = React.createContext<ProvisioningSocketContextReturn>({
|
||||||
webSocket: undefined,
|
webSocket: undefined,
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
});
|
});
|
||||||
@@ -15,22 +15,18 @@ const ProviderSocketContext = React.createContext<ProviderSocketContextReturn>({
|
|||||||
export const ProvisioningSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
export const ProvisioningSocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||||
const { token, isUserLoaded } = useAuth();
|
const { token, isUserLoaded } = useAuth();
|
||||||
const { pushNotification, modal } = useWebSocketNotification();
|
const { pushNotification, modal } = useWebSocketNotification();
|
||||||
const { addMessage, isOpen, setIsOpen, webSocket, onStartWebSocket } = useProviderStore((state) => ({
|
const { addMessage, isOpen, webSocket, onStartWebSocket } = useProvisioningStore((state) => ({
|
||||||
addMessage: state.addMessage,
|
addMessage: state.addMessage,
|
||||||
setIsOpen: state.setWebSocketOpen,
|
|
||||||
isOpen: state.isWebSocketOpen,
|
isOpen: state.isWebSocketOpen,
|
||||||
webSocket: state.webSocket,
|
webSocket: state.webSocket,
|
||||||
onStartWebSocket: state.startWebSocket,
|
onStartWebSocket: state.startWebSocket,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const onMessage = useCallback((msg: MessageEvent<string>) => {
|
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||||
try {
|
try {
|
||||||
const extracted = extractProviderWebSocketResponse(msg);
|
const data = JSON.parse(message.data) as ProvisioningSocketRawMessage | undefined;
|
||||||
if (extracted) {
|
if (data) {
|
||||||
addMessage(extracted);
|
addMessage(data, pushNotification);
|
||||||
if (extracted.type === 'NOTIFICATION') {
|
|
||||||
pushNotification(extracted.data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -65,12 +61,13 @@ export const ProvisioningSocketProvider = ({ children }: { children: React.React
|
|||||||
|
|
||||||
if (webSocket) {
|
if (webSocket) {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
timeoutId = setTimeout(() => {
|
/* timeoutId = setTimeout(() => {
|
||||||
if (webSocket) webSocket.onclose = () => {};
|
if (webSocket) webSocket.onclose = () => {};
|
||||||
webSocket?.close();
|
webSocket?.close();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}, 5000);
|
}, 5000); */
|
||||||
} else {
|
} else {
|
||||||
|
// If tab is active again, verify if browser killed the WS
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!isOpen && isUserLoaded && axiosProv?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
if (!isOpen && isUserLoaded && axiosProv?.defaults?.baseURL !== axiosSec?.defaults?.baseURL) {
|
||||||
@@ -86,17 +83,17 @@ export const ProvisioningSocketProvider = ({ children }: { children: React.React
|
|||||||
};
|
};
|
||||||
}, [webSocket, isOpen]);
|
}, [webSocket, isOpen]);
|
||||||
|
|
||||||
const values: ProviderSocketContextReturn = useMemo(() => ({}), []);
|
const values: ProvisioningSocketContextReturn = useMemo(() => ({}), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProviderSocketContext.Provider value={values}>
|
<ProvisioningSocketContext.Provider value={values}>
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{modal}
|
{modal}
|
||||||
</>
|
</>
|
||||||
</ProviderSocketContext.Provider>
|
</ProvisioningSocketContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGlobalProvisioningSocket: () => ProviderSocketContextReturn = () =>
|
export const useGlobalProvisioningSocket: () => ProvisioningSocketContextReturn = () =>
|
||||||
React.useContext(ProviderSocketContext);
|
React.useContext(ProvisioningSocketContext);
|
||||||
|
|||||||
@@ -1,41 +1,170 @@
|
|||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import { ProviderWebSocketMessage, ProviderWebSocketParsedMessage } from './utils';
|
import {
|
||||||
|
ACCEPTED_VENUE_NOTIFICATION_TYPES,
|
||||||
|
ProvisioningCommandResponse,
|
||||||
|
ProvisioningSocketRawMessage,
|
||||||
|
ProvisioningVenueNotificationMessage,
|
||||||
|
SocketEventCallback,
|
||||||
|
SocketWebSocketNotificationData,
|
||||||
|
} from './utils';
|
||||||
import { axiosProv } from 'constants/axiosInstances';
|
import { axiosProv } from 'constants/axiosInstances';
|
||||||
|
import { NotificationType } from 'models/Socket';
|
||||||
|
|
||||||
export type ProviderStoreState = {
|
export type ProvisioningWebSocketMessage =
|
||||||
lastMessage?: ProviderWebSocketMessage;
|
| {
|
||||||
allMessages: ProviderWebSocketMessage[];
|
type: 'NOTIFICATION';
|
||||||
addMessage: (message: ProviderWebSocketParsedMessage) => void;
|
data: SocketWebSocketNotificationData;
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'COMMAND';
|
||||||
|
data: ProvisioningCommandResponse;
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'UNKNOWN';
|
||||||
|
data: {
|
||||||
|
type?: string;
|
||||||
|
type_id?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawWebSocketMessage = (
|
||||||
|
message?: ProvisioningSocketRawMessage,
|
||||||
|
): SocketWebSocketNotificationData | undefined => {
|
||||||
|
if (message && message.notification) {
|
||||||
|
if (message.notification.type_id === 1) {
|
||||||
|
return {
|
||||||
|
type: 'LOG',
|
||||||
|
log: message.notification.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
message.notification.type_id === 1000 ||
|
||||||
|
message.notification.type_id === 2000 ||
|
||||||
|
message.notification.type_id === 3000
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: 'NOTIFICATION',
|
||||||
|
data: message.notification,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (message?.notificationTypes) {
|
||||||
|
return {
|
||||||
|
type: 'INITIAL_MESSAGE',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
} else if (message?.response && message.command_response_id) {
|
||||||
|
return {
|
||||||
|
type: 'COMMAND',
|
||||||
|
data: message as ProvisioningCommandResponse,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProvisioningStoreState = {
|
||||||
|
availableLogTypes: NotificationType[];
|
||||||
|
hiddenLogIds: number[];
|
||||||
|
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||||
|
lastMessage?: ProvisioningWebSocketMessage;
|
||||||
|
allMessages: ProvisioningWebSocketMessage[];
|
||||||
|
addMessage: (
|
||||||
|
rawMsg: ProvisioningSocketRawMessage,
|
||||||
|
pushNotification: (notification: ProvisioningVenueNotificationMessage['notification']) => void,
|
||||||
|
) => void;
|
||||||
|
eventListeners: SocketEventCallback[];
|
||||||
|
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||||
webSocket?: WebSocket;
|
webSocket?: WebSocket;
|
||||||
send: (str: string) => void;
|
send: (str: string) => void;
|
||||||
startWebSocket: (token: string, tries?: number) => void;
|
startWebSocket: (token: string, tries?: number) => void;
|
||||||
isWebSocketOpen: boolean;
|
isWebSocketOpen: boolean;
|
||||||
setWebSocketOpen: (isOpen: boolean) => void;
|
setWebSocketOpen: (isOpen: boolean) => void;
|
||||||
|
lastSearchResults: string[];
|
||||||
|
setLastSearchResults: (result: string[]) => void;
|
||||||
|
errors: { str: string; timestamp: Date }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProviderStore = create<ProviderStoreState>((set, get) => ({
|
export const useProvisioningStore = create<ProvisioningStoreState>((set, get) => ({
|
||||||
allMessages: [] as ProviderWebSocketMessage[],
|
availableLogTypes: [],
|
||||||
addMessage: (msg: ProviderWebSocketParsedMessage) => {
|
hiddenLogIds: [],
|
||||||
// @ts-ignore
|
setHiddenLogIds: (logsToHide: number[]) => {
|
||||||
const obj: ProviderWebSocketMessage =
|
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||||
|
set(() => ({
|
||||||
|
hiddenLogIds: logsToHide,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
allMessages: [] as ProvisioningWebSocketMessage[],
|
||||||
|
addMessage: (rawMsg, pushNotification) => {
|
||||||
|
try {
|
||||||
|
const msg = parseRawWebSocketMessage(rawMsg);
|
||||||
|
if (msg) {
|
||||||
|
// Handle notification-specific logic
|
||||||
|
if (msg.type === 'INITIAL_MESSAGE') {
|
||||||
|
if (msg.message.notificationTypes) {
|
||||||
|
set({ availableLogTypes: msg.message.notificationTypes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle venue notifications
|
||||||
|
if (msg.type === 'NOTIFICATION' && ACCEPTED_VENUE_NOTIFICATION_TYPES.includes(msg.data.type_id)) {
|
||||||
|
pushNotification(msg.data);
|
||||||
|
}
|
||||||
|
// General handling
|
||||||
|
const obj: ProvisioningWebSocketMessage =
|
||||||
msg.type === 'COMMAND'
|
msg.type === 'COMMAND'
|
||||||
? {
|
? {
|
||||||
type: msg.type,
|
type: 'COMMAND',
|
||||||
data: msg.data,
|
data: msg.data,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
|
id: uuid(),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
type: msg.type,
|
type: 'NOTIFICATION',
|
||||||
data: msg.data,
|
data: msg,
|
||||||
timestamp: new Date(),
|
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||||
|
id: uuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const eventsToFire = get().eventListeners.filter(({ type }) => type === msg.type);
|
||||||
|
|
||||||
|
if (eventsToFire.length > 0) {
|
||||||
|
for (const event of eventsToFire) {
|
||||||
|
event.callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return set((state) => ({
|
||||||
|
allMessages:
|
||||||
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
|
lastMessage: obj,
|
||||||
|
eventListeners: get().eventListeners.filter(
|
||||||
|
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return set((state) => ({
|
return set((state) => ({
|
||||||
allMessages:
|
allMessages:
|
||||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
lastMessage: obj,
|
lastMessage: obj,
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
// TODO - Add error message to socket logs
|
||||||
|
return set((state) => ({
|
||||||
|
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
eventListeners: [] as SocketEventCallback[],
|
||||||
|
addEventListeners: (events: SocketEventCallback[]) =>
|
||||||
|
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||||
isWebSocketOpen: false,
|
isWebSocketOpen: false,
|
||||||
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||||
send: (str: string) => {
|
send: (str: string) => {
|
||||||
@@ -65,4 +194,7 @@ export const useProviderStore = create<ProviderStoreState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
lastSearchResults: [] as string[],
|
||||||
|
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||||
|
errors: [],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
|
import { InitialSocketMessage } from 'models/Socket';
|
||||||
import { Subscriber } from 'models/Subscriber';
|
import { Subscriber } from 'models/Subscriber';
|
||||||
|
|
||||||
// Notifications we react to from the WS
|
export type ProvisioningSocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||||
export const acceptedNotificationTypes = [
|
export const ProvisioningSocketNotificationTypeMap = {
|
||||||
'venue_configuration_update',
|
1: 'logs',
|
||||||
'entity_configuration_update',
|
1000: 'venue_fw_upgrade',
|
||||||
'venue_rebooter',
|
2000: 'venue_config_update',
|
||||||
'venue_upgrader',
|
3000: 'venue_rebooter',
|
||||||
];
|
};
|
||||||
|
|
||||||
// Data received from WS on Venue update notification
|
export const ACCEPTED_VENUE_NOTIFICATION_TYPES = [1000, 2000, 3000];
|
||||||
export type ProviderWebSocketVenueUpdateResponse = {
|
|
||||||
notification_id: number;
|
export type ProvisioningCommandResponse = {
|
||||||
type: 'venue_configuration_update' | 'entity_configuration_update' | 'venue_rebooter' | 'venue_upgrader';
|
command_response_id: number;
|
||||||
|
response: { serialNumbers?: string[]; users?: Subscriber[]; results?: string[] };
|
||||||
|
notification?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogMessage = {
|
||||||
|
notification: {
|
||||||
|
notificationId: number;
|
||||||
|
type?: undefined;
|
||||||
|
type_id: 1;
|
||||||
|
content: {
|
||||||
|
level: LogLevel;
|
||||||
|
msg: string;
|
||||||
|
source: string;
|
||||||
|
thread_id: number;
|
||||||
|
thread_name: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
command_response_id?: undefined;
|
||||||
|
response?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProvisioningVenueNotificationMessage = {
|
||||||
|
notification: {
|
||||||
|
notification_id: number;
|
||||||
|
type?: 'venue_fw_upgrade' | 'venue_config_update' | 'venue_rebooter';
|
||||||
|
type_id: 1000 | 2000 | 3000;
|
||||||
content: {
|
content: {
|
||||||
type: 'venue_configuration_update' | 'entity_configuration_update' | 'venue_rebooter' | 'venue_upgrader';
|
|
||||||
title: string;
|
title: string;
|
||||||
details: string;
|
details: string;
|
||||||
success: string[];
|
success: string[];
|
||||||
@@ -24,53 +53,47 @@ export type ProviderWebSocketVenueUpdateResponse = {
|
|||||||
error: string[];
|
error: string[];
|
||||||
timeStamp: number;
|
timeStamp: number;
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
command_response_id?: undefined;
|
||||||
|
response?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProviderCommandResponse = {
|
export type ProvisioningSocketRawMessage =
|
||||||
command_response_id: number;
|
| Partial<LogMessage>
|
||||||
response: { serialNumbers?: string[]; users?: Subscriber[]; results?: string[] };
|
| Partial<ProvisioningVenueNotificationMessage>
|
||||||
};
|
| Partial<ProvisioningCommandResponse>
|
||||||
|
| InitialSocketMessage;
|
||||||
|
|
||||||
// Parsed WebSocket message
|
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||||
export type ProviderWebSocketParsedMessage =
|
|
||||||
|
export type SocketWebSocketNotificationData =
|
||||||
| {
|
| {
|
||||||
type: 'NOTIFICATION';
|
type: 'NOTIFICATION';
|
||||||
data: ProviderWebSocketVenueUpdateResponse;
|
data: ProvisioningVenueNotificationMessage['notification'];
|
||||||
|
log?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'LOG';
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log: LogMessage['notification']['content'];
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'COMMAND';
|
type: 'COMMAND';
|
||||||
data: ProviderCommandResponse;
|
data: ProvisioningCommandResponse;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'INITIAL_MESSAGE';
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
message: InitialSocketMessage;
|
||||||
};
|
};
|
||||||
|
export type SocketEventCallback = {
|
||||||
// Parsing raw WS messages into a more usable format
|
id: string;
|
||||||
export const extractProviderWebSocketResponse = (message: MessageEvent): ProviderWebSocketParsedMessage | undefined => {
|
type: 'LOG';
|
||||||
try {
|
serialNumber: string;
|
||||||
const data = JSON.parse(message.data);
|
callback: () => void;
|
||||||
if (data.notification && acceptedNotificationTypes.includes(data.notification.type)) {
|
|
||||||
const notification = data.notification as ProviderWebSocketVenueUpdateResponse;
|
|
||||||
return { data: notification, type: 'NOTIFICATION' };
|
|
||||||
}
|
|
||||||
if (data.command_response_id) {
|
|
||||||
return { data, type: 'COMMAND' } as {
|
|
||||||
type: 'COMMAND';
|
|
||||||
data: ProviderCommandResponse;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// What we store in the store
|
|
||||||
export type ProviderWebSocketMessage =
|
|
||||||
| {
|
|
||||||
type: 'NOTIFICATION';
|
|
||||||
data: ProviderWebSocketParsedMessage;
|
|
||||||
timestamp: Date;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'COMMAND';
|
|
||||||
data: ProviderCommandResponse;
|
|
||||||
timestamp: Date;
|
|
||||||
};
|
|
||||||
|
|||||||
91
src/contexts/SecuritySocketProvider/index.tsx
Normal file
91
src/contexts/SecuritySocketProvider/index.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useSecurityStore } from './useStore';
|
||||||
|
import { SecuritySocketRawMessage } from './utils';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
export type SecuritySocketContextReturn = Record<string, unknown>;
|
||||||
|
|
||||||
|
const SecuritySocketContext = React.createContext<SecuritySocketContextReturn>({
|
||||||
|
webSocket: undefined,
|
||||||
|
isOpen: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SecuritySocketProvider = ({ children }: { children: React.ReactElement }) => {
|
||||||
|
const { token, isUserLoaded } = useAuth();
|
||||||
|
const { addMessage, isOpen, webSocket, onStartWebSocket } = useSecurityStore((state) => ({
|
||||||
|
addMessage: state.addMessage,
|
||||||
|
isOpen: state.isWebSocketOpen,
|
||||||
|
webSocket: state.webSocket,
|
||||||
|
onStartWebSocket: state.startWebSocket,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message.data) as SecuritySocketRawMessage | undefined;
|
||||||
|
if (data) {
|
||||||
|
addMessage(data, queryClient);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// useEffect for created the WebSocket and 'storing' it in useRef
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUserLoaded) {
|
||||||
|
onStartWebSocket(token ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsCurrent = webSocket;
|
||||||
|
return () => wsCurrent?.close();
|
||||||
|
}, [isUserLoaded]);
|
||||||
|
|
||||||
|
// useEffect for generating global notifications
|
||||||
|
useEffect(() => {
|
||||||
|
if (webSocket) {
|
||||||
|
webSocket.addEventListener('message', onMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (webSocket) webSocket.removeEventListener('message', onMessage);
|
||||||
|
};
|
||||||
|
}, [webSocket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
if (webSocket) {
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
/* timeoutId = setTimeout(() => {
|
||||||
|
if (webSocket) webSocket.onclose = () => {};
|
||||||
|
webSocket?.close();
|
||||||
|
setIsOpen(false);
|
||||||
|
}, 5000); */
|
||||||
|
} else {
|
||||||
|
// If tab is active again, verify if browser killed the WS
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!isOpen && isUserLoaded) {
|
||||||
|
onStartWebSocket(token ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [webSocket, isOpen]);
|
||||||
|
|
||||||
|
const values: SecuritySocketContextReturn = useMemo(() => ({}), []);
|
||||||
|
|
||||||
|
return <SecuritySocketContext.Provider value={values}>{children}</SecuritySocketContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGlobalSecuritySocket: () => SecuritySocketContextReturn = () => React.useContext(SecuritySocketContext);
|
||||||
158
src/contexts/SecuritySocketProvider/useStore.ts
Normal file
158
src/contexts/SecuritySocketProvider/useStore.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import create from 'zustand';
|
||||||
|
import { SecuritySocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||||
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
|
import { NotificationType } from 'models/Socket';
|
||||||
|
|
||||||
|
export type SecurityWebSocketMessage =
|
||||||
|
| {
|
||||||
|
type: 'NOTIFICATION';
|
||||||
|
data: SocketWebSocketNotificationData;
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'UNKNOWN';
|
||||||
|
data: {
|
||||||
|
type?: string;
|
||||||
|
type_id?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
timestamp: Date;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseRawWebSocketMessage = (message?: SecuritySocketRawMessage): SocketWebSocketNotificationData | undefined => {
|
||||||
|
if (message && message.notification) {
|
||||||
|
if (message.notification.type_id === 1) {
|
||||||
|
return {
|
||||||
|
type: 'LOG',
|
||||||
|
log: message.notification.content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (message?.notificationTypes) {
|
||||||
|
return {
|
||||||
|
type: 'INITIAL_MESSAGE',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecurityStoreState = {
|
||||||
|
availableLogTypes: NotificationType[];
|
||||||
|
hiddenLogIds: number[];
|
||||||
|
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||||
|
lastMessage?: SecurityWebSocketMessage;
|
||||||
|
allMessages: SecurityWebSocketMessage[];
|
||||||
|
addMessage: (rawMsg: SecuritySocketRawMessage, queryClient: QueryClient) => void;
|
||||||
|
eventListeners: SocketEventCallback[];
|
||||||
|
addEventListeners: (callback: SocketEventCallback[]) => void;
|
||||||
|
webSocket?: WebSocket;
|
||||||
|
send: (str: string) => void;
|
||||||
|
startWebSocket: (token: string, tries?: number) => void;
|
||||||
|
isWebSocketOpen: boolean;
|
||||||
|
setWebSocketOpen: (isOpen: boolean) => void;
|
||||||
|
lastSearchResults: string[];
|
||||||
|
setLastSearchResults: (result: string[]) => void;
|
||||||
|
errors: { str: string; timestamp: Date }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSecurityStore = create<SecurityStoreState>((set, get) => ({
|
||||||
|
availableLogTypes: [],
|
||||||
|
hiddenLogIds: [],
|
||||||
|
setHiddenLogIds: (logsToHide: number[]) => {
|
||||||
|
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||||
|
set(() => ({
|
||||||
|
hiddenLogIds: logsToHide,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
allMessages: [] as SecurityWebSocketMessage[],
|
||||||
|
addMessage: (rawMsg: SecuritySocketRawMessage) => {
|
||||||
|
try {
|
||||||
|
const msg = parseRawWebSocketMessage(rawMsg);
|
||||||
|
if (msg) {
|
||||||
|
// Handle notification-specific logic
|
||||||
|
if (msg.type === 'INITIAL_MESSAGE') {
|
||||||
|
if (msg.message.notificationTypes) {
|
||||||
|
set({ availableLogTypes: msg.message.notificationTypes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// General handling
|
||||||
|
const obj: SecurityWebSocketMessage = {
|
||||||
|
type: 'NOTIFICATION',
|
||||||
|
data: msg,
|
||||||
|
timestamp: msg.log?.timestamp ? new Date(msg.log.timestamp * 1000) : new Date(),
|
||||||
|
id: uuid(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventsToFire = get().eventListeners.filter(
|
||||||
|
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventsToFire.length > 0) {
|
||||||
|
for (const event of eventsToFire) {
|
||||||
|
event.callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
return set((state) => ({
|
||||||
|
allMessages:
|
||||||
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
|
lastMessage: obj,
|
||||||
|
eventListeners: get().eventListeners.filter(
|
||||||
|
({ id }) => !eventsToFire.find(({ id: findId }) => findId === id),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return set((state) => ({
|
||||||
|
allMessages:
|
||||||
|
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||||
|
lastMessage: obj,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch {
|
||||||
|
// TODO - Add error message to socket logs
|
||||||
|
return set((state) => ({
|
||||||
|
errors: [...state.errors, { str: JSON.stringify(rawMsg), timestamp: new Date() }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventListeners: [] as SocketEventCallback[],
|
||||||
|
addEventListeners: (events: SocketEventCallback[]) =>
|
||||||
|
set((state) => ({ eventListeners: [...state.eventListeners, ...events] })),
|
||||||
|
isWebSocketOpen: false,
|
||||||
|
setWebSocketOpen: (isOpen: boolean) => set({ isWebSocketOpen: isOpen }),
|
||||||
|
send: (str: string) => {
|
||||||
|
const ws = get().webSocket;
|
||||||
|
if (ws) ws.send(str);
|
||||||
|
},
|
||||||
|
startWebSocket: (token: string, tries = 0) => {
|
||||||
|
const newTries = tries + 1;
|
||||||
|
if (tries <= 10) {
|
||||||
|
set({
|
||||||
|
webSocket: new WebSocket(
|
||||||
|
`${
|
||||||
|
axiosSec?.defaults?.baseURL ? axiosSec.defaults.baseURL.replace('https', 'wss').replace('http', 'ws') : ''
|
||||||
|
}/ws`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
const ws = get().webSocket;
|
||||||
|
if (ws) {
|
||||||
|
ws.onopen = () => {
|
||||||
|
set({ isWebSocketOpen: true });
|
||||||
|
ws.send(`token:${token}`);
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
set({ isWebSocketOpen: false });
|
||||||
|
setTimeout(() => get().startWebSocket(token, newTries), 3000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lastSearchResults: [] as string[],
|
||||||
|
setLastSearchResults: (results: string[]) => set({ lastSearchResults: results }),
|
||||||
|
errors: [],
|
||||||
|
}));
|
||||||
51
src/contexts/SecuritySocketProvider/utils.ts
Normal file
51
src/contexts/SecuritySocketProvider/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { InitialSocketMessage } from 'models/Socket';
|
||||||
|
|
||||||
|
export type SecuritySocketNotificationTypeId = 1 | 1000 | 2000 | 3000 | 4000 | 5000 | 6000;
|
||||||
|
export const SecuritySocketNotificationTypeMap = {
|
||||||
|
1: 'logs',
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogMessage = {
|
||||||
|
notification: {
|
||||||
|
notificationId: number;
|
||||||
|
type?: undefined;
|
||||||
|
type_id: 1;
|
||||||
|
content: {
|
||||||
|
level: LogLevel;
|
||||||
|
msg: string;
|
||||||
|
source: string;
|
||||||
|
thread_id: number;
|
||||||
|
thread_name: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecuritySocketRawMessage = Partial<LogMessage> | InitialSocketMessage;
|
||||||
|
|
||||||
|
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||||
|
|
||||||
|
export type SocketWebSocketNotificationData =
|
||||||
|
| {
|
||||||
|
type: 'LOG';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log: LogMessage['notification']['content'];
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'INITIAL_MESSAGE';
|
||||||
|
serialNumber?: undefined;
|
||||||
|
serialNumbers?: undefined;
|
||||||
|
notificationTypes?: undefined;
|
||||||
|
log?: undefined;
|
||||||
|
message: InitialSocketMessage;
|
||||||
|
};
|
||||||
|
export type SocketEventCallback = {
|
||||||
|
id: string;
|
||||||
|
type: 'LOG';
|
||||||
|
serialNumber: string;
|
||||||
|
callback: () => void;
|
||||||
|
};
|
||||||
14
src/models/Socket.ts
Normal file
14
src/models/Socket.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export type NotificationType = {
|
||||||
|
helper?: string;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InitialSocketMessage = {
|
||||||
|
notification: undefined;
|
||||||
|
serialNumbers: undefined;
|
||||||
|
command_response_id: undefined;
|
||||||
|
response: undefined;
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
notificationTypes?: NotificationType[];
|
||||||
|
};
|
||||||
82
src/pages/Notifications/DeviceLogs/DeviceLogsSearchBar.tsx
Normal file
82
src/pages/Notifications/DeviceLogs/DeviceLogsSearchBar.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Heading } from '@chakra-ui/react';
|
||||||
|
import { InputActionMeta, Select } from 'chakra-react-select';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useControllerDeviceSearch } from 'contexts/ControllerSocketProvider/hooks/Commands/useDeviceSearch';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSearchSelect: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeviceLogsSearchBar = ({ onSearchSelect }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { inputValue, results, onInputChange, isOpen } = useControllerDeviceSearch({
|
||||||
|
minLength: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const NoOptionsMessage = React.useCallback(
|
||||||
|
() => (
|
||||||
|
<Heading size="sm" textAlign="center">
|
||||||
|
{t('common.no_devices_found')}
|
||||||
|
</Heading>
|
||||||
|
),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
const onClick = React.useCallback((v: { value: string }) => {
|
||||||
|
onSearchSelect(v.value);
|
||||||
|
onInputChange(v.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onChange = React.useCallback((v: string, action: InputActionMeta) => {
|
||||||
|
if (action.action !== 'input-blur' && action.action !== 'menu-close') {
|
||||||
|
if (v.length === 0 || v.match('^[a-fA-F0-9-*]+$')) onInputChange(v);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const onFocus = React.useCallback(() => {
|
||||||
|
onSearchSelect('');
|
||||||
|
onInputChange('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
chakraStyles={{
|
||||||
|
control: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
borderRadius: '15px',
|
||||||
|
color: 'unset',
|
||||||
|
}),
|
||||||
|
input: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
width: '140px',
|
||||||
|
opacity: 1,
|
||||||
|
}),
|
||||||
|
dropdownIndicator: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
backgroundColor: 'unset',
|
||||||
|
border: 'unset',
|
||||||
|
}),
|
||||||
|
menu: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
color: 'black',
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
components={{ NoOptionsMessage }}
|
||||||
|
hideSelectedOptions={false}
|
||||||
|
isClearable
|
||||||
|
blurInputOnSelect
|
||||||
|
onFocus={onFocus}
|
||||||
|
// @ts-ignore
|
||||||
|
options={results.map((v: string) => ({ label: v, value: v }))}
|
||||||
|
filterOption={() => true}
|
||||||
|
inputValue={inputValue}
|
||||||
|
value={inputValue}
|
||||||
|
placeholder={t('logs.filter')}
|
||||||
|
onInputChange={onChange}
|
||||||
|
// @ts-ignore
|
||||||
|
onChange={onClick}
|
||||||
|
isDisabled={!isOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DeviceLogsSearchBar);
|
||||||
165
src/pages/Notifications/DeviceLogs/index.tsx
Normal file
165
src/pages/Notifications/DeviceLogs/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||||
|
import { Download } from 'phosphor-react';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import DeviceLogsSearchBar from './DeviceLogsSearchBar';
|
||||||
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import ShownLogsDropdown from 'components/ShownLogsDropdown';
|
||||||
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
|
import { dateForFilename } from 'helpers/dateFormatting';
|
||||||
|
|
||||||
|
const LogsCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useControllerStore((state) => ({
|
||||||
|
logs: state.allMessages,
|
||||||
|
availableLogTypes: state.availableLogTypes,
|
||||||
|
hiddenLogIds: state.hiddenLogIds,
|
||||||
|
setHiddenLogIds: state.setHiddenLogIds,
|
||||||
|
}));
|
||||||
|
const [show, setShow] = React.useState<'' | 'connections' | 'statistics' | 'global_statistics'>('');
|
||||||
|
const [serialNumber, setSerialNumber] = React.useState<string>('');
|
||||||
|
|
||||||
|
const onSerialSelect = React.useCallback((serial: string) => setSerialNumber(serial), []);
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
DEVICE_CONNECTION: t('common.connected'),
|
||||||
|
DEVICE_DISCONNECTION: t('common.disconnected'),
|
||||||
|
DEVICE_STATISTICS: t('controller.devices.new_statistics'),
|
||||||
|
DEVICE_CONNECTIONS_STATISTICS: t('controller.dashboard.device_dashboard_refresh'),
|
||||||
|
DEVICE_SEARCH_RESULTS: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
let arr = logs.filter(
|
||||||
|
({ data: d }) =>
|
||||||
|
d.type === 'DEVICE_CONNECTION' || d.type === 'DEVICE_DISCONNECTION' || d.type === 'DEVICE_STATISTICS',
|
||||||
|
);
|
||||||
|
if (show === 'connections') {
|
||||||
|
arr = arr.filter(
|
||||||
|
(msg) =>
|
||||||
|
msg.type === 'NOTIFICATION' &&
|
||||||
|
(msg.data.type === 'DEVICE_CONNECTION' || msg.data.type === 'DEVICE_DISCONNECTION'),
|
||||||
|
);
|
||||||
|
} else if (show === 'statistics') {
|
||||||
|
arr = arr.filter((msg) => msg.type === 'NOTIFICATION' && msg.data.type === 'DEVICE_STATISTICS');
|
||||||
|
} else if (show === 'global_statistics') {
|
||||||
|
arr = arr.filter((msg) => msg.type === 'NOTIFICATION' && msg.data.type === 'DEVICE_CONNECTIONS_STATISTICS');
|
||||||
|
}
|
||||||
|
if (serialNumber.trim().length > 0) {
|
||||||
|
arr = arr.filter(
|
||||||
|
(msg) =>
|
||||||
|
msg.data?.serialNumber !== undefined &&
|
||||||
|
typeof msg.data.serialNumber === 'string' &&
|
||||||
|
msg.data?.serialNumber.includes(serialNumber.trim()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.reverse();
|
||||||
|
}, [logs, show, serialNumber]);
|
||||||
|
|
||||||
|
type RowProps = { index: number; style: React.CSSProperties };
|
||||||
|
const Row = React.useCallback(
|
||||||
|
({ index, style }: RowProps) => {
|
||||||
|
const msg = data[index];
|
||||||
|
if (msg) {
|
||||||
|
if (msg.type === 'NOTIFICATION' && msg.data.serialNumber) {
|
||||||
|
return (
|
||||||
|
<Box style={style}>
|
||||||
|
<Flex w="100%">
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 130px" textAlign="left">
|
||||||
|
<Text fontFamily="mono">{msg.data?.serialNumber ?? '-'}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 140px">
|
||||||
|
<Text>{labels[msg.data.type] ?? msg.data.type}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box textAlign="left" w="calc(100% - 80px - 120px - 140px - 60px)">
|
||||||
|
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{JSON.stringify(msg.data)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[t, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadableLogs = React.useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((msg) => ({
|
||||||
|
timestamp: msg.timestamp.toLocaleString(),
|
||||||
|
serialNumber: msg.data?.serialNumber ?? '-',
|
||||||
|
type: labels[msg.data?.type] ?? msg.data.type,
|
||||||
|
data: JSON.stringify(msg.data),
|
||||||
|
})),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={4} pt={4}>
|
||||||
|
<DeviceLogsSearchBar onSearchSelect={onSerialSelect} />
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<ShownLogsDropdown
|
||||||
|
availableLogTypes={availableLogTypes}
|
||||||
|
setHiddenLogIds={setHiddenLogIds}
|
||||||
|
hiddenLogIds={hiddenLogIds}
|
||||||
|
/>
|
||||||
|
<Select size="md" value={show} onChange={(e) => setShow(e.target.value as '' | 'connections')} w="200px">
|
||||||
|
<option value="">{t('common.select_all')}</option>
|
||||||
|
<option value="connections">{t('controller.devices.connection_changes')}</option>
|
||||||
|
<option value="statistics">{t('logs.device_statistics')}</option>
|
||||||
|
<option value="global_statistics">{t('logs.global_connections')}</option>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<CSVLink
|
||||||
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
|
data={downloadableLogs as object[]}
|
||||||
|
>
|
||||||
|
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||||
|
{t('logs.export')}
|
||||||
|
</Button>
|
||||||
|
</CSVLink>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box overflowX="auto" w="100%">
|
||||||
|
<Table size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th w="110px">{t('common.time')}</Th>
|
||||||
|
<Th w="150px">{t('inventory.serial_number')}</Th>
|
||||||
|
<Th w="120px" pl={0}>
|
||||||
|
{t('common.type')}
|
||||||
|
</Th>
|
||||||
|
<Th>{t('analytics.raw_data')}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
</Table>
|
||||||
|
<Box ml={4} h="calc(70vh)">
|
||||||
|
<ReactVirtualizedAutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List height={height} width={width} itemCount={data.length} itemSize={35}>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</ReactVirtualizedAutoSizer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogsCard;
|
||||||
175
src/pages/Notifications/FmsLogs/index.tsx
Normal file
175
src/pages/Notifications/FmsLogs/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||||
|
import { Download } from 'phosphor-react';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
|
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 { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import ShownLogsDropdown from 'components/ShownLogsDropdown';
|
||||||
|
import { useFirmwareStore } from 'contexts/FirmwareSocketProvider/useStore';
|
||||||
|
import { LogLevel } from 'contexts/FirmwareSocketProvider/utils';
|
||||||
|
import { dateForFilename } from 'helpers/dateFormatting';
|
||||||
|
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||||
|
|
||||||
|
const FmsLogsCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useFirmwareStore((state) => ({
|
||||||
|
logs: state.allMessages,
|
||||||
|
availableLogTypes: state.availableLogTypes,
|
||||||
|
hiddenLogIds: state.hiddenLogIds,
|
||||||
|
setHiddenLogIds: state.setHiddenLogIds,
|
||||||
|
}));
|
||||||
|
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
const arr = logs.filter(
|
||||||
|
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||||
|
);
|
||||||
|
return arr.reverse();
|
||||||
|
}, [logs, level]);
|
||||||
|
|
||||||
|
const colorSchemeMap: Record<LogLevel, string> = {
|
||||||
|
information: 'blue',
|
||||||
|
critical: 'red',
|
||||||
|
debug: 'teal',
|
||||||
|
error: 'red',
|
||||||
|
fatal: 'purple',
|
||||||
|
notice: 'blue',
|
||||||
|
trace: 'blue',
|
||||||
|
warning: 'yellow',
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowProps = { index: number; style: React.CSSProperties };
|
||||||
|
const Row = React.useCallback(
|
||||||
|
({ index, style }: RowProps) => {
|
||||||
|
const msg = data[index];
|
||||||
|
if (msg) {
|
||||||
|
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||||
|
return (
|
||||||
|
<Box style={style}>
|
||||||
|
<Flex w="100%">
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 200px">
|
||||||
|
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.source}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 140px">
|
||||||
|
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Badge
|
||||||
|
ml={1}
|
||||||
|
size="lg"
|
||||||
|
fontSize="0.9em"
|
||||||
|
variant="solid"
|
||||||
|
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||||
|
>
|
||||||
|
{msg.data.log.level}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||||
|
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[t, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadableLogs = React.useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((msg) =>
|
||||||
|
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||||
|
? {
|
||||||
|
timestamp: msg.timestamp.toLocaleString(),
|
||||||
|
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||||
|
source: msg.data?.log?.source ?? '-',
|
||||||
|
level: msg.data?.log?.level,
|
||||||
|
message: JSON.stringify(msg.data?.log?.msg),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={4} pt={4}>
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<ShownLogsDropdown
|
||||||
|
availableLogTypes={availableLogTypes}
|
||||||
|
setHiddenLogIds={setHiddenLogIds}
|
||||||
|
hiddenLogIds={hiddenLogIds}
|
||||||
|
/>
|
||||||
|
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||||
|
<option value="">{t('common.select_all')}</option>
|
||||||
|
{Object.keys(colorSchemeMap).map((key) => (
|
||||||
|
<option key={uuid()} value={key}>
|
||||||
|
{uppercaseFirstLetter(key)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<CSVLink
|
||||||
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
|
data={downloadableLogs as object[]}
|
||||||
|
>
|
||||||
|
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||||
|
{t('logs.export')}
|
||||||
|
</Button>
|
||||||
|
</CSVLink>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box overflowX="auto" w="100%">
|
||||||
|
<Table size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th w="110px">{t('common.time')}</Th>
|
||||||
|
<Th w="200px">{t('logs.source')}</Th>
|
||||||
|
<Th w="160px">
|
||||||
|
{t('logs.thread')} ID-{t('common.name')}
|
||||||
|
</Th>
|
||||||
|
<Th w="90px" pl={0}>
|
||||||
|
{t('logs.level')}
|
||||||
|
</Th>
|
||||||
|
<Th>{t('logs.message')}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
</Table>
|
||||||
|
<Box ml={4} h="calc(70vh)">
|
||||||
|
<ReactVirtualizedAutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
itemCount={data.length}
|
||||||
|
itemSize={35}
|
||||||
|
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</ReactVirtualizedAutoSizer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FmsLogsCard;
|
||||||
175
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
175
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||||
|
import { Download } from 'phosphor-react';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
|
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 { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import ShownLogsDropdown from 'components/ShownLogsDropdown';
|
||||||
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
|
import { LogLevel } from 'contexts/ControllerSocketProvider/utils';
|
||||||
|
import { dateForFilename } from 'helpers/dateFormatting';
|
||||||
|
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||||
|
|
||||||
|
const GeneralLogsCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useControllerStore((state) => ({
|
||||||
|
logs: state.allMessages,
|
||||||
|
availableLogTypes: state.availableLogTypes,
|
||||||
|
hiddenLogIds: state.hiddenLogIds,
|
||||||
|
setHiddenLogIds: state.setHiddenLogIds,
|
||||||
|
}));
|
||||||
|
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
const arr = logs.filter(
|
||||||
|
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||||
|
);
|
||||||
|
return arr.reverse();
|
||||||
|
}, [logs, level]);
|
||||||
|
|
||||||
|
const colorSchemeMap: Record<LogLevel, string> = {
|
||||||
|
information: 'blue',
|
||||||
|
critical: 'red',
|
||||||
|
debug: 'teal',
|
||||||
|
error: 'red',
|
||||||
|
fatal: 'purple',
|
||||||
|
notice: 'blue',
|
||||||
|
trace: 'blue',
|
||||||
|
warning: 'yellow',
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowProps = { index: number; style: React.CSSProperties };
|
||||||
|
const Row = React.useCallback(
|
||||||
|
({ index, style }: RowProps) => {
|
||||||
|
const msg = data[index];
|
||||||
|
if (msg) {
|
||||||
|
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||||
|
return (
|
||||||
|
<Box style={style}>
|
||||||
|
<Flex w="100%">
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 200px">
|
||||||
|
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.source}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 140px">
|
||||||
|
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Badge
|
||||||
|
ml={1}
|
||||||
|
size="lg"
|
||||||
|
fontSize="0.9em"
|
||||||
|
variant="solid"
|
||||||
|
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||||
|
>
|
||||||
|
{msg.data.log.level}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||||
|
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[t, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadableLogs = React.useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((msg) =>
|
||||||
|
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||||
|
? {
|
||||||
|
timestamp: msg.timestamp.toLocaleString(),
|
||||||
|
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||||
|
source: msg.data?.log?.source ?? '-',
|
||||||
|
level: msg.data?.log?.level,
|
||||||
|
message: JSON.stringify(msg.data?.log?.msg),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={4} pt={4}>
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<ShownLogsDropdown
|
||||||
|
availableLogTypes={availableLogTypes}
|
||||||
|
setHiddenLogIds={setHiddenLogIds}
|
||||||
|
hiddenLogIds={hiddenLogIds}
|
||||||
|
/>
|
||||||
|
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||||
|
<option value="">{t('common.select_all')}</option>
|
||||||
|
{Object.keys(colorSchemeMap).map((key) => (
|
||||||
|
<option key={uuid()} value={key}>
|
||||||
|
{uppercaseFirstLetter(key)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<CSVLink
|
||||||
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
|
data={downloadableLogs as object[]}
|
||||||
|
>
|
||||||
|
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||||
|
{t('logs.export')}
|
||||||
|
</Button>
|
||||||
|
</CSVLink>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box overflowX="auto" w="100%">
|
||||||
|
<Table size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th w="110px">{t('common.time')}</Th>
|
||||||
|
<Th w="200px">{t('logs.source')}</Th>
|
||||||
|
<Th w="160px">
|
||||||
|
{t('logs.thread')} ID-{t('common.name')}
|
||||||
|
</Th>
|
||||||
|
<Th w="90px" pl={0}>
|
||||||
|
{t('logs.level')}
|
||||||
|
</Th>
|
||||||
|
<Th>{t('logs.message')}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
</Table>
|
||||||
|
<Box ml={4} h="calc(70vh)">
|
||||||
|
<ReactVirtualizedAutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
itemCount={data.length}
|
||||||
|
itemSize={35}
|
||||||
|
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</ReactVirtualizedAutoSizer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneralLogsCard;
|
||||||
175
src/pages/Notifications/SecLogs/index.tsx
Normal file
175
src/pages/Notifications/SecLogs/index.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Badge, Box, Button, Flex, HStack, Select, Spacer, Table, Text, Th, Thead, Tr } from '@chakra-ui/react';
|
||||||
|
import { Download } from 'phosphor-react';
|
||||||
|
import { CSVLink } from 'react-csv';
|
||||||
|
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 { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import ShownLogsDropdown from 'components/ShownLogsDropdown';
|
||||||
|
import { useSecurityStore } from 'contexts/SecuritySocketProvider/useStore';
|
||||||
|
import { LogLevel } from 'contexts/SecuritySocketProvider/utils';
|
||||||
|
import { dateForFilename } from 'helpers/dateFormatting';
|
||||||
|
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||||
|
|
||||||
|
const SecLogsCard = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useSecurityStore((state) => ({
|
||||||
|
logs: state.allMessages,
|
||||||
|
availableLogTypes: state.availableLogTypes,
|
||||||
|
hiddenLogIds: state.hiddenLogIds,
|
||||||
|
setHiddenLogIds: state.setHiddenLogIds,
|
||||||
|
}));
|
||||||
|
const [level, setLevel] = React.useState<'' | LogLevel>('');
|
||||||
|
|
||||||
|
const data = React.useMemo(() => {
|
||||||
|
const arr = logs.filter(
|
||||||
|
(d) => d.type === 'NOTIFICATION' && d.data.type === 'LOG' && (level === '' || d.data.log.level === level),
|
||||||
|
);
|
||||||
|
return arr.reverse();
|
||||||
|
}, [logs, level]);
|
||||||
|
|
||||||
|
const colorSchemeMap: Record<LogLevel, string> = {
|
||||||
|
information: 'blue',
|
||||||
|
critical: 'red',
|
||||||
|
debug: 'teal',
|
||||||
|
error: 'red',
|
||||||
|
fatal: 'purple',
|
||||||
|
notice: 'blue',
|
||||||
|
trace: 'blue',
|
||||||
|
warning: 'yellow',
|
||||||
|
};
|
||||||
|
|
||||||
|
type RowProps = { index: number; style: React.CSSProperties };
|
||||||
|
const Row = React.useCallback(
|
||||||
|
({ index, style }: RowProps) => {
|
||||||
|
const msg = data[index];
|
||||||
|
if (msg) {
|
||||||
|
if (msg.type === 'NOTIFICATION' && msg.data.type === 'LOG') {
|
||||||
|
return (
|
||||||
|
<Box style={style}>
|
||||||
|
<Flex w="100%">
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 200px">
|
||||||
|
<Text w="200px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.source}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 140px">
|
||||||
|
<Text w="140px" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{msg.data.log.thread_id}-{msg.data.log.thread_name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Box flex="0 1 110px">
|
||||||
|
<Badge
|
||||||
|
ml={1}
|
||||||
|
size="lg"
|
||||||
|
fontSize="0.9em"
|
||||||
|
variant="solid"
|
||||||
|
colorScheme={colorSchemeMap[msg.data.log.level]}
|
||||||
|
>
|
||||||
|
{msg.data.log.level}
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box textAlign="left" w="calc(100% - 180px - 210px - 120px - 60px)">
|
||||||
|
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||||
|
{JSON.stringify(msg.data.log.msg).replace(/"/g, '')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[t, data],
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadableLogs = React.useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((msg) =>
|
||||||
|
msg.type === 'NOTIFICATION' && msg.data.type === 'LOG'
|
||||||
|
? {
|
||||||
|
timestamp: msg.timestamp.toLocaleString(),
|
||||||
|
thread: `${msg.data.log.thread_id}-${msg.data.log.thread_name}`,
|
||||||
|
source: msg.data?.log?.source ?? '-',
|
||||||
|
level: msg.data?.log?.level,
|
||||||
|
message: JSON.stringify(msg.data?.log?.msg),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
),
|
||||||
|
[data],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardHeader px={4} pt={4}>
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<ShownLogsDropdown
|
||||||
|
availableLogTypes={availableLogTypes}
|
||||||
|
setHiddenLogIds={setHiddenLogIds}
|
||||||
|
hiddenLogIds={hiddenLogIds}
|
||||||
|
/>
|
||||||
|
<Select size="md" value={level} onChange={(e) => setLevel(e.target.value as '' | LogLevel)} w="130px">
|
||||||
|
<option value="">{t('common.select_all')}</option>
|
||||||
|
{Object.keys(colorSchemeMap).map((key) => (
|
||||||
|
<option key={uuid()} value={key}>
|
||||||
|
{uppercaseFirstLetter(key)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<CSVLink
|
||||||
|
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
|
data={downloadableLogs as object[]}
|
||||||
|
>
|
||||||
|
<Button onClick={() => {}} colorScheme="blue" leftIcon={<Download />}>
|
||||||
|
{t('logs.export')}
|
||||||
|
</Button>
|
||||||
|
</CSVLink>
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody p={4}>
|
||||||
|
<Box overflowX="auto" w="100%">
|
||||||
|
<Table size="sm">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th w="110px">{t('common.time')}</Th>
|
||||||
|
<Th w="200px">{t('logs.source')}</Th>
|
||||||
|
<Th w="160px">
|
||||||
|
{t('logs.thread')} ID-{t('common.name')}
|
||||||
|
</Th>
|
||||||
|
<Th w="90px" pl={0}>
|
||||||
|
{t('logs.level')}
|
||||||
|
</Th>
|
||||||
|
<Th>{t('logs.message')}</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
</Table>
|
||||||
|
<Box ml={4} h="calc(70vh)">
|
||||||
|
<ReactVirtualizedAutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
itemCount={data.length}
|
||||||
|
itemSize={35}
|
||||||
|
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||||
|
>
|
||||||
|
{Row}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</ReactVirtualizedAutoSizer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecLogsCard;
|
||||||
103
src/pages/Notifications/index.tsx
Normal file
103
src/pages/Notifications/index.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LogsCard from './DeviceLogs';
|
||||||
|
import FmsLogsCard from './FmsLogs';
|
||||||
|
import GeneralLogsCard from './GeneralLogs';
|
||||||
|
import SecLogsCard from './SecLogs';
|
||||||
|
import { Card } from 'components/Containers/Card';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
const INDEX_PARAM = 'notifications-tab-index';
|
||||||
|
|
||||||
|
const getDefaultTabIndex = () => {
|
||||||
|
const index = localStorage.getItem(INDEX_PARAM) || '0';
|
||||||
|
try {
|
||||||
|
return parseInt(index, 10);
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotificationsPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isUserLoaded } = useAuth();
|
||||||
|
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||||
|
|
||||||
|
const handleTabChange = (index: number) => {
|
||||||
|
setTabIndex(index);
|
||||||
|
localStorage.setItem(INDEX_PARAM, index.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDirection="column" pt="75px">
|
||||||
|
{isUserLoaded && (
|
||||||
|
<Card p={0}>
|
||||||
|
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||||
|
<TabList>
|
||||||
|
<CardHeader>
|
||||||
|
<Tab>{t('devices.notifications')}</Tab>
|
||||||
|
<Tab>{t('simulation.controller')}</Tab>
|
||||||
|
<Tab>{t('logs.security')}</Tab>
|
||||||
|
<Tab>{t('logs.firmware')}</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"
|
||||||
|
>
|
||||||
|
<LogsCard />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<GeneralLogsCard />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<SecLogsCard />
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<FmsLogsCard />
|
||||||
|
</Box>
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</Tabs>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@chakra-ui/react';
|
import { Icon } from '@chakra-ui/react';
|
||||||
import { Barcode, FloppyDisk, Info, UsersThree, WifiHigh } from 'phosphor-react';
|
import { Barcode, FloppyDisk, Info, ListBullets, UsersThree, WifiHigh } from 'phosphor-react';
|
||||||
import { Route } from 'models/Routes';
|
import { Route } from 'models/Routes';
|
||||||
|
|
||||||
const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations'));
|
const DefaultConfigurationsPage = React.lazy(() => import('pages/DefaultConfigurations'));
|
||||||
const DevicePage = React.lazy(() => import('pages/Device'));
|
const DevicePage = React.lazy(() => import('pages/Device'));
|
||||||
const DevicesPage = React.lazy(() => import('pages/Devices'));
|
const DevicesPage = React.lazy(() => import('pages/Devices'));
|
||||||
const FirmwarePage = React.lazy(() => import('pages/Firmware'));
|
const FirmwarePage = React.lazy(() => import('pages/Firmware'));
|
||||||
|
const NotificationsPage = React.lazy(() => import('pages/Notifications'));
|
||||||
const ProfilePage = React.lazy(() => import('pages/Profile'));
|
const ProfilePage = React.lazy(() => import('pages/Profile'));
|
||||||
const SystemPage = React.lazy(() => import('pages/SystemPage'));
|
const SystemPage = React.lazy(() => import('pages/SystemPage'));
|
||||||
const UsersPage = React.lazy(() => import('pages/UsersPage'));
|
const UsersPage = React.lazy(() => import('pages/UsersPage'));
|
||||||
@@ -39,6 +40,15 @@ const routes: Route[] = [
|
|||||||
),
|
),
|
||||||
component: DefaultConfigurationsPage,
|
component: DefaultConfigurationsPage,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
hidden: true,
|
hidden: true,
|
||||||
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
||||||
|
|||||||
Reference in New Issue
Block a user