[WIFI-12257] Display GPS location on device page

Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
Charles
2023-02-01 19:51:01 +01:00
parent 75d995d54e
commit 999680e94b
14 changed files with 453 additions and 41 deletions

79
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ucentral-client",
"version": "2.9.0(5)",
"version": "2.9.0(7)",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ucentral-client",
"version": "2.9.0(5)",
"version": "2.9.0(7)",
"license": "ISC",
"dependencies": {
"@chakra-ui/icons": "^2.0.11",
@@ -16,6 +16,8 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/inter": "^4.5.14",
"@googlemaps/react-wrapper": "^1.1.35",
"@googlemaps/typescript-guards": "^2.0.3",
"@react-spring/web": "^9.5.5",
"@tanstack/react-query": "^4.12.0",
"@textea/json-viewer": "^2.10.0",
@@ -24,6 +26,7 @@
"chakra-react-select": "^4.3.0",
"chart.js": "^3.9.1",
"dagre": "^0.8.5",
"fast-equals": "^4.0.3",
"formik": "^2.2.9",
"framer-motion": "^7.6.1",
"i18next": "^22.0.0",
@@ -54,6 +57,7 @@
"zustand": "^4.1.2"
},
"devDependencies": {
"@types/google.maps": "^3.51.0",
"@types/node": "^18.11.2",
"@types/react": "^18.0.21",
"@types/react-csv": "^1.1.3",
@@ -2848,6 +2852,30 @@
"version": "4.5.14",
"license": "MIT"
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
"dependencies": {
"fast-deep-equal": "^3.1.3"
}
},
"node_modules/@googlemaps/react-wrapper": {
"version": "1.1.35",
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
"dependencies": {
"@googlemaps/js-api-loader": "^1.13.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@googlemaps/typescript-guards": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.7",
"dev": true,
@@ -3501,6 +3529,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/google.maps": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.11",
"dev": true,
@@ -5529,7 +5563,6 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@@ -5537,6 +5570,11 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/fast-equals": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
},
"node_modules/fast-glob": {
"version": "3.2.12",
"dev": true,
@@ -11427,6 +11465,27 @@
"@fontsource/inter": {
"version": "4.5.14"
},
"@googlemaps/js-api-loader": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.15.1.tgz",
"integrity": "sha512-AsnEgNsB7S/VdrHGEQUaUM2e5tmjFGKBAfzR/AqO8O7TPq/jQGvoRw5liPBw4EMF38RDsHmKDV89q/X+qiUREQ==",
"requires": {
"fast-deep-equal": "^3.1.3"
}
},
"@googlemaps/react-wrapper": {
"version": "1.1.35",
"resolved": "https://registry.npmjs.org/@googlemaps/react-wrapper/-/react-wrapper-1.1.35.tgz",
"integrity": "sha512-vK+BDQMHN0Oqr66cW3ZPWVK43BUmJJBu6P8T74tc6/fKpUJUlFEaZsupgIIRRRDW9ejB8uGagUmwOnA2gdcvbw==",
"requires": {
"@googlemaps/js-api-loader": "^1.13.2"
}
},
"@googlemaps/typescript-guards": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@googlemaps/typescript-guards/-/typescript-guards-2.0.3.tgz",
"integrity": "sha512-3iHuO8H0jPehftsMK0kgyJzPYU/g/oiTRw+wu/yltqSZ7wJPt3vfsJHkPiuRpQjbnnWygX+T3mkRGyK/eyZ/lw=="
},
"@humanwhocodes/config-array": {
"version": "0.10.7",
"dev": true,
@@ -11788,6 +11847,12 @@
"version": "0.0.39",
"dev": true
},
"@types/google.maps": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.51.0.tgz",
"integrity": "sha512-44/oQYjc5D6kxBcI3Qk9rk3IIOMwnlEMWDV7pwPJ2YI89s5Q1OzDrFvR7QJ3LFrpVXEhig+gyagFg54+foinFg==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.11",
"dev": true
@@ -13027,13 +13092,17 @@
}
},
"fast-deep-equal": {
"version": "3.1.3",
"dev": true
"version": "3.1.3"
},
"fast-diff": {
"version": "1.2.0",
"dev": true
},
"fast-equals": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
},
"fast-glob": {
"version": "3.2.12",
"dev": true,

View File

@@ -1,6 +1,6 @@
{
"name": "ucentral-client",
"version": "2.9.0(5)",
"version": "2.9.0(7)",
"description": "",
"private": true,
"main": "index.tsx",
@@ -22,12 +22,15 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@fontsource/inter": "^4.5.14",
"@googlemaps/react-wrapper": "^1.1.35",
"@googlemaps/typescript-guards": "^2.0.3",
"@react-spring/web": "^9.5.5",
"axios": "^1.1.3",
"buffer": "^6.0.3",
"chakra-react-select": "^4.3.0",
"dagre": "^0.8.5",
"formik": "^2.2.9",
"fast-equals": "^4.0.3",
"framer-motion": "^7.6.1",
"i18next": "^22.0.0",
"i18next-browser-languagedetector": "^6.1.8",
@@ -60,6 +63,7 @@
"zustand": "^4.1.2"
},
"devDependencies": {
"@types/google.maps": "^3.51.0",
"@types/node": "^18.11.2",
"@types/react": "^18.0.21",
"@types/react-csv": "^1.1.3",

View File

@@ -79,8 +79,11 @@
"live_view_help": "Hilfe zur Live-Ansicht",
"memory": "Erinnerung",
"memory_used": "Verwendeter Speicher",
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten",
"mode": "Modus",
"monitoring": "Überwachung",
"no_board": "Keine Überwachung",
"no_board_description": "Sie überwachen diesen Veranstaltungsort derzeit nicht, klicken Sie hier, um zu beginnen",
"noise": "Lärm",
"packets": "Pakete",
"radio": "RADIO",
@@ -91,6 +94,8 @@
"retries": "Wiederholungen",
"search_serials": "Zeitschriften suchen",
"stop_monitoring": "Beenden Sie die Überwachung",
"stop_monitoring_success": "Überwachungsort gestoppt!",
"stop_monitoring_warning": "Bist du sicher? Dadurch werden alle aufgezeichneten Überwachungsdaten für diesen Veranstaltungsort gelöscht",
"temperature": "Temperatur",
"title": "ANALYTICS",
"total_data": "Gesamtdaten",
@@ -675,7 +680,8 @@
"test_digicert_creds": "Anmeldeinformationen testen",
"title": "Entitäten",
"tree": "Entitätsbaum",
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden. Bitte erstellen Sie neue Entitäten und erstellen Sie Veranstaltungsorte unter diesen."
"update_success": "Entität aktualisiert!",
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
},
"footer": {
"powered_by": "Unterstützt von",
@@ -769,13 +775,17 @@
"city": "Stadt",
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
"country": "Land",
"elevation": "Elevation",
"geocode": "Geo-Code",
"lat": "Breite",
"longitude": "Längengrad",
"one": "Ort",
"other": "Standorte",
"postal": "Postleitzahl",
"state": "Bundesstaat / Provinz",
"title": "Standorte",
"to_claim": "Standorte zu beanspruchen"
"to_claim": "Standorte zu beanspruchen",
"view_gps": ""
},
"login": {
"access_policy": "Zugangsrichtlinien",
@@ -1037,6 +1047,9 @@
"os": "Betriebssystem",
"processors": "Prozessoren",
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
"secrets": "Systemgeheimnisse",
"secrets_create": "Geheimnis erstellen",
"secrets_one": "Systemgeheimnis",
"start": "Start",
"subsystems": "Subsysteme",
"success_reload": "Reload-Befehl erfolgreich gesendet!",
@@ -1107,6 +1120,7 @@
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
"title": "Veranstaltungsorte",
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
"update_success": "Veranstaltungsort aktualisiert!",
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",

View File

@@ -79,8 +79,11 @@
"live_view_help": "Live View Help",
"memory": "Memory",
"memory_used": "Memory Used",
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring",
"mode": "Mode",
"monitoring": "Monitoring",
"no_board": "No Monitoring",
"no_board_description": "You are not monitoring this Venue at the moment, click here to start",
"noise": "Noise",
"packets": "Packets",
"radio": "Radio",
@@ -91,6 +94,8 @@
"retries": "Retries",
"search_serials": "Search Serials",
"stop_monitoring": "Stop Monitoring",
"stop_monitoring_success": "Stopped Monitoring Venue!",
"stop_monitoring_warning": "Are you sure? This will erase all recorded monitoring data for this venue",
"temperature": "Temperature",
"title": "Analytics",
"total_data": "Total Data",
@@ -675,7 +680,8 @@
"test_digicert_creds": "Test Credentials",
"title": "Entities",
"tree": "Entity Tree",
"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these."
"update_success": "Entity updated!",
"venues_under_root": "Venues cannot be created directly under the root entity"
},
"footer": {
"powered_by": "Powered By",
@@ -769,13 +775,17 @@
"city": "City",
"claim_explanation": "To claim locations you can use the table below",
"country": "Country",
"elevation": "Elevation",
"geocode": "Geo Code",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Location",
"other": "Locations",
"postal": "ZIP/Postal Code",
"state": "State/Province",
"title": "Locations",
"to_claim": "Locations to claim"
"to_claim": "Locations to claim",
"view_gps": "View GPS Location"
},
"login": {
"access_policy": "Access Policy",
@@ -1037,6 +1047,9 @@
"os": "Operating System",
"processors": "Processors",
"reload_chosen_subsystems": "Reload Chosen Subsystems",
"secrets": "System Secrets",
"secrets_create": "Create Secret",
"secrets_one": "System Secret",
"start": "Start",
"subsystems": "Subsystems",
"success_reload": "Successfully sent reload command!",
@@ -1107,6 +1120,7 @@
"successfully_update_devices": "Updating {{num}} devices!",
"title": "Venues",
"update_all_devices": "Update All Device Configurations",
"update_success": "Venue updated!",
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
"upgrade_all_devices_success": "Successfully started upgrading devices!",

View File

@@ -79,8 +79,11 @@
"live_view_help": "Ayuda de visualización en vivo",
"memory": "Memoria",
"memory_used": "Memoria usada",
"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior",
"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo",
"mode": "Modo",
"monitoring": "Vigilancia",
"no_board": "Sin monitoreo",
"no_board_description": "No está monitoreando este lugar en este momento, haga clic aquí para comenzar",
"noise": "Ruido",
"packets": "Paquetes",
"radio": "RADIO",
@@ -91,6 +94,8 @@
"retries": "Reintentos",
"search_serials": "Buscar seriales",
"stop_monitoring": "Dejar de monitorear",
"stop_monitoring_success": "¡Se detuvo el lugar de monitoreo!",
"stop_monitoring_warning": "¿Está seguro? Esto borrará todos los datos de monitoreo grabados para este lugar.",
"temperature": "temperatura",
"title": "ANALÍTICA",
"total_data": "Datos totales",
@@ -675,7 +680,8 @@
"test_digicert_creds": "Credenciales de prueba",
"title": "entidades",
"tree": "Árbol de entidades",
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas."
"update_success": "¡Entidad actualizada!",
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
},
"footer": {
"powered_by": "energizado por",
@@ -769,13 +775,17 @@
"city": "ciudad",
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
"country": "País",
"elevation": "Elevación",
"geocode": "Código geográfico",
"lat": "Latitud",
"longitude": "Longitud",
"one": "Ubicación",
"other": "Ubicaciones",
"postal": "código postal",
"state": "Provincia del estado",
"title": "Ubicaciones",
"to_claim": "Ubicaciones para reclamar"
"to_claim": "Ubicaciones para reclamar",
"view_gps": ""
},
"login": {
"access_policy": "Política de acceso",
@@ -1037,6 +1047,9 @@
"os": "sistema operativo",
"processors": "Procesadores",
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
"secrets": "Secretos del sistema",
"secrets_create": "Crear secreto",
"secrets_one": "Secreto del sistema",
"start": "comienzo",
"subsystems": "Subsistemas",
"success_reload": "¡Comando de recarga enviado con éxito!",
@@ -1107,6 +1120,7 @@
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
"title": "Sedes",
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
"update_success": "Lugar actualizado!",
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",

View File

@@ -79,8 +79,11 @@
"live_view_help": "Aide sur l'affichage en direct",
"memory": "mémoire",
"memory_used": "Mémoire utilisée",
"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance",
"mode": "Mode",
"monitoring": "surveillance",
"no_board": "Aucune surveillance",
"no_board_description": "Vous ne surveillez pas ce lieu pour le moment, cliquez ici pour commencer",
"noise": "Bruit",
"packets": "Paquets",
"radio": "Radio",
@@ -91,6 +94,8 @@
"retries": "Tentatives",
"search_serials": "Rechercher des publications en série",
"stop_monitoring": "Arrêter la surveillance",
"stop_monitoring_success": "Lieu de surveillance arrêté !",
"stop_monitoring_warning": "Êtes-vous sûr? Cela effacera toutes les données de surveillance enregistrées pour ce lieu",
"temperature": "Température",
"title": "ANALYTIQUE",
"total_data": "Données totales",
@@ -675,7 +680,8 @@
"test_digicert_creds": "Tester les informations d'identification",
"title": "Entités",
"tree": "Arborescence des entités",
"venues_under_root": "Les sites ne peuvent pas être créés directement sous l'entité racine. Veuillez créer de nouvelles entités et créer des lieux sous celles-ci."
"update_success": "Entité mise à jour !",
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
},
"footer": {
"powered_by": "Alimenté par",
@@ -769,13 +775,17 @@
"city": "Ville",
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
"country": "Pays",
"elevation": "Élévation",
"geocode": "Geo code",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Emplacement",
"other": "Emplacements",
"postal": "Zip / code postal",
"state": "Etat / Province",
"title": "Emplacements",
"to_claim": "Emplacements à réclamer"
"to_claim": "Emplacements à réclamer",
"view_gps": ""
},
"login": {
"access_policy": "Politique d'accès",
@@ -1037,6 +1047,9 @@
"os": "Système opérateur",
"processors": "Processeurs",
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
"secrets": "Secrets du système",
"secrets_create": "Créer un secret",
"secrets_one": "Code secret du système",
"start": "Début",
"subsystems": "Sous-systèmes",
"success_reload": "Commande de rechargement envoyée avec succès !",
@@ -1107,6 +1120,7 @@
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
"title": "Les lieux",
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
"update_success": "Lieu mis à jour !",
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",

View File

@@ -79,8 +79,11 @@
"live_view_help": "Ajuda da visualização ao vivo",
"memory": "Memória",
"memory_used": "Memória Usada",
"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior",
"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento",
"mode": "Modo",
"monitoring": "Monitoramento",
"no_board": "Sem monitoramento",
"no_board_description": "Você não está monitorando este local no momento, clique aqui para começar",
"noise": "Barulho",
"packets": "Pacotes",
"radio": "Rádio",
@@ -91,6 +94,8 @@
"retries": "Novas tentativas",
"search_serials": "Pesquisar séries",
"stop_monitoring": "Parar o monitoramento",
"stop_monitoring_success": "Local de monitoramento interrompido!",
"stop_monitoring_warning": "Tem certeza? Isso apagará todos os dados de monitoramento gravados para este local",
"temperature": "Temperatura",
"title": "Analytics",
"total_data": "Dados totais",
@@ -675,7 +680,8 @@
"test_digicert_creds": "Credenciais de teste",
"title": "Entidades",
"tree": "Árvore de entidades",
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas."
"update_success": "Entidade atualizada!",
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
},
"footer": {
"powered_by": "Distribuído por",
@@ -769,13 +775,17 @@
"city": "Cidade",
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
"country": "País",
"elevation": "elevação",
"geocode": "Código geográfico",
"lat": "Latitude",
"longitude": "Longitude",
"one": "Localização",
"other": "Localizações",
"postal": "CEP / Código Postal",
"state": "Estado / Província",
"title": "Localizações",
"to_claim": "Locais para reivindicar"
"to_claim": "Locais para reivindicar",
"view_gps": ""
},
"login": {
"access_policy": "Política de Acesso",
@@ -1037,6 +1047,9 @@
"os": "Sistema Operacional",
"processors": "Processadores",
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
"secrets": "Segredos do sistema",
"secrets_create": "Criar Segredo",
"secrets_one": "Segredo do sistema",
"start": "Começar",
"subsystems": "Subsistemas",
"success_reload": "Comando de recarga enviado com sucesso!",
@@ -1107,6 +1120,7 @@
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
"title": "Locais",
"update_all_devices": "Atualizar todas as configurações do dispositivo",
"update_success": "Local atualizado!",
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
const _GoogleMapMarker = (options: google.maps.MarkerOptions) => {
const [marker, setMarker] = React.useState<google.maps.Marker>();
React.useEffect(() => {
if (!marker) {
setMarker(new google.maps.Marker());
}
return () => {
if (marker) {
marker.setMap(null);
}
};
}, [marker]);
React.useEffect(() => {
if (marker) {
marker.setOptions(options);
}
}, [marker, options]);
return null;
};
export const GoogleMapMarker = React.memo(_GoogleMapMarker);

View File

@@ -0,0 +1,89 @@
import * as React from 'react';
import { isLatLngLiteral } from '@googlemaps/typescript-guards';
import { createCustomEqual } from 'fast-equals';
const deepCompareEqualsForMaps = createCustomEqual((deepEqual) =>
// @ts-ignore
(a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => {
if (
isLatLngLiteral(a) ||
a instanceof google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof google.maps.LatLng
) {
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
}
// @ts-ignore
return deepEqual(a, b);
},
);
const useDeepCompareMemoize = (value: unknown) => {
const ref = React.useRef<unknown>();
if (!deepCompareEqualsForMaps(value, ref.current)) {
ref.current = value;
}
return ref.current;
};
const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => {
React.useEffect(callback, dependencies.map(useDeepCompareMemoize));
};
export interface GoogleMapProps extends google.maps.MapOptions {
style: { [key: string]: string };
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
children?: React.ReactNode;
}
const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => {
const ref = React.useRef<HTMLDivElement>(null);
const [map, setMap] = React.useState<google.maps.Map>();
// because React does not do deep comparisons, a custom hook is used
useDeepCompareEffectForMaps(() => {
if (map) {
map.setOptions(options);
}
}, [map, options]);
React.useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
React.useEffect(() => {
if (map) {
['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName));
if (onClick) {
map.addListener('click', onClick);
}
if (onIdle) {
map.addListener('idle', () => onIdle(map));
}
}
}, [map, onClick, onIdle]);
return (
<>
<div ref={ref} style={style} />
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
// set the map prop on the child component
// @ts-ignore
return React.cloneElement(child, { map });
}
return null;
})}
</>
);
};
export const GoogleMap = React.memo(_GoogleMap);

View File

@@ -0,0 +1,83 @@
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { axiosSec } from 'constants/axiosInstances';
export type SecretName = 'google.maps.apikey' | string;
export type Secret = {
key: SecretName;
value: string;
};
export type SecretDictionaryValue = {
key: SecretName;
description: string;
};
const getSecret = async (context: QueryFunctionContext<string[], unknown>) =>
axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data);
export const useGetSystemSecret = ({ secret }: { secret: SecretName }) =>
useQuery(['secrets', secret], getSecret, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
});
const getAllSecrets = async () =>
axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets);
export const useGetAllSystemSecrets = () => {
const queryClient = useQueryClient();
return useQuery(['secrets'], getAllSecrets, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
onSuccess: (data) => {
for (const secret of data) {
queryClient.setQueryData(['secrets', secret.key], secret);
}
},
});
};
const getSecretsDictionary = async () =>
axiosSec
.get('/systemSecret/0?dictionary=true')
.then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys);
export const useGetSystemSecretsDictionary = () =>
useQuery(['secrets', 'dictionary'], getSecretsDictionary, {
staleTime: 1000 * 60 * 10,
refetchInterval: 1000 * 60 * 10,
});
const updateSecret = async ({ key, value }: { key: string; value: string }) =>
axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value });
export const useUpdateSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(updateSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};
export const useCreateSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(updateSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};
const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`);
export const useDeleteSystemSecret = () => {
const queryClient = useQueryClient();
return useMutation(deleteSecret, {
onSuccess: () => {
queryClient.invalidateQueries(['secrets']);
},
});
};

View File

@@ -1,4 +1,4 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { axiosGw } from 'constants/axiosInstances';
import { AxiosError } from 'models/Axios';
@@ -160,6 +160,11 @@ export type DeviceStatistics = {
};
};
};
gps?: {
elevation: string;
latitude: string;
longitude: string;
};
version?: number;
};
const getLastStats = (serialNumber?: string) =>
@@ -175,7 +180,7 @@ export const useGetDeviceLastStats = ({
onError?: (e: AxiosError) => void;
}) =>
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
enabled: serialNumber !== undefined && serialNumber !== '' && false,
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 1000 * 60,
onError,
});
@@ -195,24 +200,12 @@ export const useGetDeviceNewestStats = ({
serialNumber?: string;
limit: number;
onError?: (e: AxiosError) => void;
}) => {
const queryClient = useQueryClient();
return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
}) =>
useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
enabled: serialNumber !== undefined && serialNumber !== '',
staleTime: 1000 * 60,
onSuccess: (response) => {
const entry = response.data[0];
// If we have a valid entry, we prefill lastStats, if not we trigger a fetch of the last statistics
if (entry) {
queryClient.setQueryData(['device', serialNumber, 'last-statistics'], entry.data);
} else {
queryClient.fetchQuery(['device', serialNumber, 'last-statistics']);
}
},
onError,
});
};
const getOuis = (macs?: string[]) => async () =>
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{

View File

@@ -0,0 +1,75 @@
import * as React from 'react';
import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react';
import { Wrapper } from '@googlemaps/react-wrapper';
import { Globe } from 'phosphor-react';
import { useTranslation } from 'react-i18next';
import { GoogleMap } from 'components/Maps/GoogleMap';
import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker';
import { Modal } from 'components/Modals/Modal';
import { useGetSystemSecret } from 'hooks/Network/Secrets';
import { useGetDeviceLastStats } from 'hooks/Network/Statistics';
type Props = {
serialNumber: string;
};
const LocationDisplayButton = ({ serialNumber }: Props) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' });
const getLastStats = useGetDeviceLastStats({ serialNumber });
const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => {
if (!getLastStats.data?.gps) return undefined;
try {
return {
lat: Number.parseFloat(getLastStats.data.gps.latitude),
lng: Number.parseFloat(getLastStats.data.gps.longitude),
};
} catch (e) {
return undefined;
}
}, [getLastStats.data?.gps]);
if (!location) {
return null;
}
return (
<>
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
{t('locations.view_gps')}
</Button>
<Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}>
<Box w="100%" h="100%">
<Flex mb={4}>
<FormControl w="unset">
<FormLabel>{t('locations.lat')}</FormLabel>
<pre>{location.lat}</pre>
</FormControl>
<FormControl w="unset" mx={4}>
<FormLabel>{t('locations.longitude')}</FormLabel>
<pre>{location.lng}</pre>
</FormControl>
<FormControl w="unset">
<FormLabel>{t('locations.elevation')}</FormLabel>
<pre>{getLastStats.data?.gps?.elevation}</pre>
</FormControl>
</Flex>
{getGoogleApiKey.data ? (
<Box h="500px">
<Wrapper apiKey={getGoogleApiKey.data.value}>
<GoogleMap center={location} style={{ flexGrow: '1', height: '100%' }} zoom={10}>
<GoogleMapMarker position={location} />
</GoogleMap>
</Wrapper>
</Box>
) : null}
</Box>
</Modal>
</>
);
};
export default LocationDisplayButton;

View File

@@ -38,7 +38,7 @@ const ViewLastStatsModal = ({ serialNumber }: Props) => {
if (getLastStats.data) {
setValue(JSON.stringify(getLastStats.data, null, 2));
}
}, [getLastStats.data]);
}, [getLastStats.data, isOpen]);
return (
<>
<Tooltip label={t('statistics.last_stats')}>

View File

@@ -1,7 +1,8 @@
import * as React from 'react';
import { Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
import { Box, Flex, Grid, GridItem, Heading, Image, Tag } from '@chakra-ui/react';
import ReactCountryFlag from 'react-country-flag';
import { useTranslation } from 'react-i18next';
import LocationDisplayButton from './LocationDisplayButton';
import { Card } from 'components/Containers/Card';
import { CardBody } from 'components/Containers/Card/CardBody';
import FormattedDate from 'components/InformationDisplays/FormattedDate';
@@ -90,11 +91,12 @@ const DeviceSummary = ({ serialNumber }: Props) => {
{!getDevice.data?.locale || getDevice.data?.locale === '' ? (
'-'
) : (
<>
<Box mr={2}>
<ReactCountryFlag style={ICON_STYLE} countryCode={getDevice.data.locale} svg />
{COUNTRY_LIST.find(({ value }) => value === getDevice.data.locale)?.label}
</>
</Box>
)}
<LocationDisplayButton serialNumber={serialNumber} />
</GridItem>
<GridItem colSpan={1} alignContent="center" alignItems="center">
<Heading size="sm">{t('analytics.last_contact')}:</Heading>