mirror of
https://github.com/Telecominfraproject/wlan-cloud-owls-ui.git
synced 2025-10-29 17:52:20 +00:00
[WIFI-12516] Support for concurrent simulations
Signed-off-by: Charles <charles.bourque96@gmail.com>
This commit is contained in:
8369
package-lock.json
generated
8369
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owls-ui",
|
||||
"version": "2.8.0(2)",
|
||||
"version": "2.10.0(9)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
@@ -15,67 +15,71 @@
|
||||
"author": "",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
"@chakra-ui/icons": "^2.0.18",
|
||||
"@chakra-ui/react": "^2.3.6",
|
||||
"@chakra-ui/theme-tools": "^2.0.12",
|
||||
"@chakra-ui/utils": "^2.0.11",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@react-spring/web": "^9.5.5",
|
||||
"axios": "^1.1.3",
|
||||
"@chakra-ui/utils": "^2.0.14",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@phosphor-icons/react": "^2.0.8",
|
||||
"@react-spring/web": "^9.7.2",
|
||||
"@tanstack/react-query": "^4.29.3",
|
||||
"axios": "^1.3.5",
|
||||
"buffer": "^6.0.3",
|
||||
"chakra-react-select": "^4.3.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.6.1",
|
||||
"i18next": "^22.0.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"libphonenumber-js": "^1.10.14",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"framer-motion": "^10.12.3",
|
||||
"i18next": "^22.4.15",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"libphonenumber-js": "^1.10.28",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"chart.js": "^3.9.1",
|
||||
"react-country-flag": "^3.0.2",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-fast-compare": "^3.2.1",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-table": "^7.8.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.15",
|
||||
"react-window": "^1.8.9",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^3.1.8",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "^5.0.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.2.2",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.1.2"
|
||||
"zustand": "^4.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.2",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"eslint": "8.25.0",
|
||||
"vite-tsconfig-paths": "^3.5.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"prettier": "^2.7.1",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-no-inline-styles": "^1.0.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"lint-staged": "^13.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -79,7 +79,11 @@
|
||||
"live_view_help": "Hilfe zur Live-Ansicht",
|
||||
"memory": "Erinnerung",
|
||||
"memory_used": "Verwendeter Speicher",
|
||||
"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",
|
||||
@@ -90,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",
|
||||
@@ -174,6 +180,7 @@
|
||||
"other": "Befehle",
|
||||
"override_dfs": "DFS überschreiben",
|
||||
"reboot": "Starten Sie neu",
|
||||
"reboot_description": "Möchten Sie dieses Gerät neu starten?",
|
||||
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
|
||||
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
|
||||
"revision": "Revision",
|
||||
@@ -234,6 +241,7 @@
|
||||
"error_download": "Fehler beim Downloadversuch: {{e}}",
|
||||
"errors": "Fehler",
|
||||
"exit_fullscreen": "Ausgang",
|
||||
"export": "Export",
|
||||
"finished": "Fertig",
|
||||
"fullscreen": "Vollbildschirm",
|
||||
"general_error": "Fehler beim Verbinden mit dem Server. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"identification": "Identifizierung",
|
||||
"inherit": "Erben",
|
||||
"language": "Sprache",
|
||||
"last_use": "Zuletzt verwendeten",
|
||||
"lifetime": "Lebenszeit",
|
||||
"locale": "Gebietsschema",
|
||||
"logout": "Ausloggen",
|
||||
@@ -261,6 +270,7 @@
|
||||
"model": "Modell",
|
||||
"modified": "Geändert",
|
||||
"monthly": "Monatlich",
|
||||
"months": "Monate",
|
||||
"my_account": "Mein Konto",
|
||||
"name": "Name",
|
||||
"name_error": "Der Name muss weniger als 50 Zeichen lang sein",
|
||||
@@ -314,6 +324,7 @@
|
||||
"use_file": "Datei verwenden",
|
||||
"value": "Wert",
|
||||
"variable": "Variable",
|
||||
"view": "Aussicht",
|
||||
"view_details": "Details anzeigen",
|
||||
"view_in_gateway": "Im Controller anzeigen",
|
||||
"view_json": "JSON anzeigen",
|
||||
@@ -346,6 +357,7 @@
|
||||
"error_pushes_one": "Fehler (könnte an einer fehlerhaften Konfiguration liegen): {{count}}",
|
||||
"error_pushes_other": "Fehler (können auf eine fehlerhafte Konfiguration zurückzuführen sein): {{count}}",
|
||||
"expert_name": "Expertenmodus",
|
||||
"expert_name_explanation": "Sie können den Expertenmodus verwenden, um Ihre Konfiguration direkt zu ändern, einschließlich des Hinzufügens von Werten, die derzeit nicht von der Benutzeroberfläche unterstützt werden. Bitte verwenden Sie dieses Format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Erläuterung",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Firmware-Aktualisierung",
|
||||
@@ -367,7 +379,7 @@
|
||||
"push_configuration": "Push-Konfiguration",
|
||||
"push_configuration_error": "Fehler beim Versuch, die Konfiguration auf das Gerät zu übertragen: {{e}}",
|
||||
"push_configuration_explanation": "Konfiguration nicht übertragen, Fehlercode {{code}}",
|
||||
"push_success": "Konfiguration erfolgreich übertragen!",
|
||||
"push_success": "Die Konfiguration wurde verifiziert und ein \"Konfigurieren\"-Befehl wurde jetzt von der Steuerung initiiert!",
|
||||
"radio_limit": "Sie haben die maximale Anzahl an Funkgeräten (5) erreicht. Sie müssen eines der aktivierten Bänder löschen, um ein neues hinzuzufügen",
|
||||
"radios": "Radios",
|
||||
"rc_only": "Nur für Release-Kandidaten",
|
||||
@@ -387,6 +399,7 @@
|
||||
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
|
||||
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
|
||||
"weight": "Gewicht",
|
||||
"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden",
|
||||
"wifi_frames": "WiFi-Frames"
|
||||
},
|
||||
"contacts": {
|
||||
@@ -511,6 +524,7 @@
|
||||
"ouis_explanation": "OUIs von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||
"outdated_one": "Firmware {{count}} Tag alt",
|
||||
"outdated_other": "Firmware {{count}} Tage alt",
|
||||
"outdated_unknown": "Firmware unbekannten Alters",
|
||||
"release": "Veröffentlichung",
|
||||
"show_dev_releases": "Entwicklerversionen",
|
||||
"status_explanation": "Verbindungsstatus von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||
@@ -526,6 +540,14 @@
|
||||
"queue": {
|
||||
"title": "Ereigniswarteschlange"
|
||||
},
|
||||
"radius": {
|
||||
"calling_station_id": "Stations",
|
||||
"input_octets": "Eingang",
|
||||
"output_octets": "Ausgabe",
|
||||
"radius_clients": "Radius-Kunden",
|
||||
"session_time": "Sitzungszeit",
|
||||
"username": "Nutzername"
|
||||
},
|
||||
"stats": {
|
||||
"load": "Belastung (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Vor {{s}} Sekunden",
|
||||
@@ -596,6 +618,7 @@
|
||||
"certificate_expires_in": "Zertifikat läuft ab in",
|
||||
"certificate_expiry": "Zert. Läuft ab in",
|
||||
"connected": "In Verbindung gebracht",
|
||||
"crash_logs": "Absturzprotokolle",
|
||||
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
||||
"create_success": " Geräte erfolgreich erstellt",
|
||||
"current_firmware": "Aktuelle Firmware",
|
||||
@@ -609,13 +632,17 @@
|
||||
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
||||
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
||||
"logs_one": "Log",
|
||||
"new_devices": "Neue Geräte",
|
||||
"no_model_image": "Kein Modellbild gefunden",
|
||||
"not_connected": "Nicht verbunden",
|
||||
"not_found_gateway": "Fehler: Gerät hat sich noch nicht mit dem Gateway verbunden",
|
||||
"notifications": "Gerätebenachrichtigungen",
|
||||
"one": "Gerät",
|
||||
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
||||
"restricted": "Beschränkt",
|
||||
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
|
||||
"restrictions_overriden_title": "Dev-Modus",
|
||||
"sanity": "Gesundheit",
|
||||
"start_import": "Geräteimport starten",
|
||||
"test_batch": "Testen Sie Importdaten",
|
||||
@@ -666,7 +693,15 @@
|
||||
"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"
|
||||
},
|
||||
"firmware": {
|
||||
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
|
||||
"last_db_update_modal": "Firmware-Datenbank",
|
||||
"last_db_update_title": "Datenbank",
|
||||
"start_db_update": "Datenbankaktualisierung starten",
|
||||
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Unterstützt von",
|
||||
@@ -688,7 +723,7 @@
|
||||
"invalid_ipv6": "Ungültige IPv6-Adresse (Bsp.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Ungültige JSON-Zeichenfolge",
|
||||
"invalid_lease_time": "Ungültiger Lease-Time-Wert! Sie müssen im digitUnit-Format vorliegen. Zum Beispiel: 6d2h5m, was 6 Tage, 2 Stunden und 5 Minuten bedeutet. Hier sind die akzeptierten Einheiten: m, h, d. Wenn Sie eine Einheit nicht verwenden möchten, lassen Sie sie vollständig weg. Anstatt also 0d2h0m zu sagen, verwenden Sie 2h",
|
||||
"invalid_mac_uc": "Ungültiger UC-MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Ungültiger MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Ungültiges Passwort, bitte sehen Sie sich die Passwortrichtlinie an",
|
||||
"invalid_phone_number": "Ungültige Telefonnummer",
|
||||
"invalid_phone_numbers": "Mindestens eine der Telefonnummern ist ungültig. Bitte geben Sie sie ohne Symbole und Leerzeichen oder in diesem Format an: +1(123)123-1234",
|
||||
@@ -697,6 +732,8 @@
|
||||
"invalid_proto_6g": "Dieses Verschlüsselungsprotokoll kann nicht auf einer SSID verwendet werden, die 6G verwendet",
|
||||
"invalid_proto_passpoint": "Dieses Verschlüsselungsprotokoll kann nicht mit einer Passpoint-SSID verwendet werden. Bitte wählen Sie ein Protokoll aus, das Radius verwenden kann",
|
||||
"invalid_select_ports": "Inkompatible Werte zwischen Schnittstellen! Bitte stellen Sie sicher, dass es keine doppelte PORT/VLAN-ID-Kombination zwischen Ihren Schnittstellen gibt",
|
||||
"invalid_static_ipv4_d": "Ungültige Adresse, dieser Bereich ist für Multicasting reserviert (Klasse D). Das erste Oktett sollte 223 oder niedriger sein",
|
||||
"invalid_static_ipv4_e": "Ungültige Adresse, dieser Bereich ist für Experimente reserviert (Klasse E). Das erste Oktett sollte 223 oder niedriger sein",
|
||||
"invalid_third_party": "Ungültige Drittanbieter-JSON-Zeichenfolge. Bitte bestätigen Sie, dass Ihr Wert ein gültiges JSON ist",
|
||||
"key_file_explanation": "Bitte verwenden Sie eine .pem-Datei, die mit „-----BEGIN PRIVATE KEY-----“ beginnt und mit „-----END PRIVATE KEY-----“ endet.",
|
||||
"min_max_string": "Der Wert muss eine Länge zwischen {{min}} (einschließlich) und {{max}} (einschließlich) haben.",
|
||||
@@ -742,6 +779,15 @@
|
||||
"successful_macs": "Erfolgreiche MACs",
|
||||
"title": "Arbeitsplätze"
|
||||
},
|
||||
"keys": {
|
||||
"description_error": "Die Beschreibung muss weniger als 64 Zeichen lang sein",
|
||||
"expire_error": "Der Ablauf darf nicht mehr als ein Jahr in der Zukunft liegen",
|
||||
"expires": "Läuft ab",
|
||||
"max_keys": "Max. Schlüssel erreicht (10)",
|
||||
"name_error": "Der Name sollte eindeutig sein und aus 6 bis 20 alphanumerischen Zeichen bestehen",
|
||||
"one": "API-Schlüssel",
|
||||
"other": "API-Schlüssel"
|
||||
},
|
||||
"locations": {
|
||||
"address_line_one": "Adresszeile eins",
|
||||
"address_line_two": "Adresszeile zwei",
|
||||
@@ -749,13 +795,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",
|
||||
@@ -781,6 +831,7 @@
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"sign_in": "Einloggen",
|
||||
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
||||
"suspended_error": "Konto gesperrt, wenden Sie sich bitte an Ihren Administrator",
|
||||
"verification": "Bestätigen Sie Ihre Anmeldung",
|
||||
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
||||
"welcome_back": "Willkommen zurück!",
|
||||
@@ -788,6 +839,25 @@
|
||||
"your_new_password": "Dein neues 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": "Benachrichtigungsfilter",
|
||||
"security": "Sicherheit",
|
||||
"source": "Quelle",
|
||||
"thread": "Faden",
|
||||
"venue_config": "Aufbau",
|
||||
"venue_reboot": "Starten Sie neu",
|
||||
"venue_upgrade": "Aktualisierung"
|
||||
},
|
||||
"map": {
|
||||
"auto_align": "Automatisch ausrichten",
|
||||
"auto_map": "Automatische Karte",
|
||||
@@ -804,6 +874,10 @@
|
||||
"title": "Karte",
|
||||
"visibility": "Sichtweite"
|
||||
},
|
||||
"notification": {
|
||||
"one": "Benachrichtigung",
|
||||
"other": "Benachrichtigungen"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Möchten Sie diesen Operator wirklich löschen? Dieser Vorgang ist nicht umkehrbar",
|
||||
"delete_operator": "Betreiber löschen",
|
||||
@@ -818,11 +892,31 @@
|
||||
"my_organization": "Meine Organisation",
|
||||
"title": "Organisation"
|
||||
},
|
||||
"overrides": {
|
||||
"delete_source": "Alle Überschreibungen von {{source}}löschen",
|
||||
"ignore_overrides": "Konfigurationsüberschreibungen ignorieren",
|
||||
"name_error": "Der Parameter ist bereits von Ihrer Quelle definiert",
|
||||
"one": "Konfigurationsüberschreibung",
|
||||
"other": "Konfigurationsüberschreibungen",
|
||||
"param_name": "Parameter",
|
||||
"param_value": "Wert",
|
||||
"parameter": "Parameter",
|
||||
"reason": "Grund",
|
||||
"reason_error": "Ihr Grund muss weniger als 64 Zeichen lang sein. lang",
|
||||
"source": "Quelle",
|
||||
"tx_power_error": "Die Sendeleistung muss zwischen 1 und 32 liegen",
|
||||
"update_success": "Aktualisierte Konfigurationsüberschreibungen!",
|
||||
"value": "Wert"
|
||||
},
|
||||
"profile": {
|
||||
"about_me": "Über mich",
|
||||
"activate": "",
|
||||
"add_new_note": "Notiz hinzufügen",
|
||||
"deactivate": "Deaktivieren",
|
||||
"delete_account": "Mein Profil löschen",
|
||||
"delete_account_confirm": "Löschen Sie alle meine Informationen",
|
||||
"delete_warning": "Diese Aktion ist nicht umkehrbar. Alle Ihre Profilinformationen und Ihre API-Schlüssel werden entfernt",
|
||||
"deleted_success": "Ihr Profil ist jetzt gelöscht, wir melden Sie jetzt ab...",
|
||||
"disabled": "Deaktiviert",
|
||||
"enabled": "aktiviert",
|
||||
"manage_avatar": "Avatar verwalten",
|
||||
@@ -835,6 +929,20 @@
|
||||
"title": "Ressourcen",
|
||||
"variable": "Variable"
|
||||
},
|
||||
"restrictions": {
|
||||
"algo": "Signaturalgorithmus",
|
||||
"allowed": "dürfen",
|
||||
"countries": "erlaubte Länder",
|
||||
"developer": "Entwicklermodus",
|
||||
"dfs": "DFS-Überschreibung",
|
||||
"gw_commands": "Gateway-Befehle",
|
||||
"identifier": "Identifikator",
|
||||
"key_verification": "Signieren von Schlüsselinformationen",
|
||||
"restricted": "Beschränkt",
|
||||
"signed_upgrade": "Nur signiertes Upgrade",
|
||||
"title": "Beschränkungen",
|
||||
"tty": "TTY-Zugriff"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithmus",
|
||||
"algorithm_other": "Algorithmen",
|
||||
@@ -849,6 +957,33 @@
|
||||
"vendor": "Verkäufer",
|
||||
"version": "Ausführung"
|
||||
},
|
||||
"script": {
|
||||
"author": "Schöpfer",
|
||||
"automatic": "Automatik",
|
||||
"create_success": "Das Skript ist jetzt erstellt und einsatzbereit!",
|
||||
"custom_domain": "Benutzerdefinierten Domain",
|
||||
"deferred": "Aufgeschoben",
|
||||
"device_title": "Skript ausführen",
|
||||
"diagnostics": "Diagnose",
|
||||
"explanation": "Führen Sie ein benutzerdefiniertes Skript auf diesem Gerät aus und laden Sie die Ergebnisse herunter",
|
||||
"file_not_ready": "Das Ergebnis wurde noch nicht hochgeladen, bitte kommen Sie später wieder",
|
||||
"file_too_large": "Bitte wählen Sie eine Datei aus, die kleiner als 500 KB ist",
|
||||
"helper": "Dokumentation",
|
||||
"no_script_available": "Kein Skript für Ihre Benutzerrolle verfügbar",
|
||||
"now": "Jetzt",
|
||||
"one": "Skript",
|
||||
"other": "Skripte",
|
||||
"restricted": "Benutzer, die dieses Skript ausführen dürfen",
|
||||
"schedule_success": "Geplante Skriptausführung!",
|
||||
"signature": "Unterschrift",
|
||||
"started_execution": "Ausführung des Skripts gestartet, kommen Sie später für die Ergebnisse!",
|
||||
"timeout": "Auszeit",
|
||||
"update_success": "Skript aktualisiert!",
|
||||
"upload_destination": "Ergebnis-Upload-Ziel",
|
||||
"upload_file": "Datei hochladen",
|
||||
"visit_external_website": "Dokumentation ansehen",
|
||||
"when": "Ausführung planen"
|
||||
},
|
||||
"service": {
|
||||
"billing_code": "Abrechnungscode",
|
||||
"billing_frequency": "Abrechnungshäufigkeit",
|
||||
@@ -866,6 +1001,8 @@
|
||||
"concurrent_devices": "Gleichzeitige Geräte",
|
||||
"controller": "Regler",
|
||||
"current_live_devices": "Aktuelle Live-Geräte",
|
||||
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
|
||||
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
|
||||
"delete_success": "Gelöschte Simulation!",
|
||||
"duration": "Dauer",
|
||||
"error_devices": "Fehler Geräte",
|
||||
@@ -889,6 +1026,7 @@
|
||||
"rx_messages": "Rx-Meldungen",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" läuft",
|
||||
"sim_history": "{{sim}} Vorherige Läufe",
|
||||
"simulated": "Simuliert",
|
||||
"start": "Simulation starten",
|
||||
"start_success": "Simulationslauf gestartet!",
|
||||
"state_interval": "Zustandsintervall",
|
||||
@@ -922,6 +1060,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Back-End-Protokolle",
|
||||
"configuration": "Aufbau",
|
||||
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
||||
"endpoint": "Endpunkt",
|
||||
"hostname": "Hostname",
|
||||
@@ -932,6 +1071,10 @@
|
||||
"os": "Betriebssystem",
|
||||
"processors": "Prozessoren",
|
||||
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
|
||||
"secrets": "Geheimnisse",
|
||||
"secrets_create": "Geheimnis erstellen",
|
||||
"secrets_one": "Geheimnis",
|
||||
"services": "dienstleistungen",
|
||||
"start": "Start",
|
||||
"subsystems": "Subsysteme",
|
||||
"success_reload": "Reload-Befehl erfolgreich gesendet!",
|
||||
@@ -943,19 +1086,31 @@
|
||||
"version": "Ausführung"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Säulen",
|
||||
"columns_hidden_one": "{{count}} Spalte ausgeblendet",
|
||||
"columns_hidden_other": "{{count}} Spalten ausgeblendet",
|
||||
"display_column": "Anzeige",
|
||||
"drag_always_show": "Sie können diese Spalte nicht ausblenden, aber ihre Position ändern",
|
||||
"drag_explanation": "Ziehen und Ablegen zum Neuordnen und Ändern der Spaltensichtbarkeit",
|
||||
"drag_locked": "Diese Säule ist in ihrer Position arretiert",
|
||||
"export_current_page": "Nur aktuelle Seite",
|
||||
"first_page": "Erste Seite",
|
||||
"go_to_page": "Zur Seite gehen",
|
||||
"hide_column": "verbergen",
|
||||
"last_page": "Letzte Seite",
|
||||
"next_page": "Nächste Seite",
|
||||
"page": "Seite",
|
||||
"previous_page": "Vorherige Seite"
|
||||
"preferences": "Tabelleneinstellungen",
|
||||
"previous_page": "Vorherige Seite",
|
||||
"reset": "Einstellungen zurücksetzen",
|
||||
"settings": "die Einstellungen"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "E-Mail nicht validiert",
|
||||
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
||||
"password": "Passwort",
|
||||
"role": "Rolle",
|
||||
"suspended": "Suspendiert",
|
||||
"title": "Nutzer"
|
||||
},
|
||||
"users": {
|
||||
@@ -1000,9 +1155,11 @@
|
||||
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
||||
"title": "Veranstaltungsorte",
|
||||
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
||||
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
|
||||
"update_success": "Veranstaltungsort aktualisiert!",
|
||||
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
|
||||
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
|
||||
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
|
||||
"upgrade_options_available": "Hier sind alle verfügbaren Revisionen, bitte wählen Sie diejenige aus, auf die ALLE Geräte dieses Veranstaltungsortes aktualisiert werden sollen",
|
||||
"use_existing": "Benutze existierendes",
|
||||
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
|
||||
"use_this_contact": "Verwenden Sie diesen Kontakt"
|
||||
|
||||
@@ -79,7 +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. 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",
|
||||
@@ -90,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",
|
||||
@@ -174,6 +180,7 @@
|
||||
"other": "Commands",
|
||||
"override_dfs": "Override DFS",
|
||||
"reboot": "Reboot",
|
||||
"reboot_description": "Do you want to reboot this device?",
|
||||
"reboot_error": "Error while sending reboot command: {{e}}",
|
||||
"reboot_success": "Successfully sent reboot command!",
|
||||
"revision": "Revision",
|
||||
@@ -234,6 +241,7 @@
|
||||
"error_download": "Error while trying to download: {{e}}",
|
||||
"errors": "Errors",
|
||||
"exit_fullscreen": "Exit",
|
||||
"export": "Export",
|
||||
"finished": "Finished",
|
||||
"fullscreen": "Fullscreen",
|
||||
"general_error": "Error connecting to the server. Please consult your administrator.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"identification": "Identification",
|
||||
"inherit": "Inherit",
|
||||
"language": "Language",
|
||||
"last_use": "Last Use",
|
||||
"lifetime": "Lifetime",
|
||||
"locale": "Locale",
|
||||
"logout": "Logout",
|
||||
@@ -261,6 +270,7 @@
|
||||
"model": "Model",
|
||||
"modified": "Modified",
|
||||
"monthly": "Monthly",
|
||||
"months": "Months",
|
||||
"my_account": "My Account",
|
||||
"name": "Name",
|
||||
"name_error": "Name must be less than 50 characters long",
|
||||
@@ -314,6 +324,7 @@
|
||||
"use_file": "Use File",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"view": "View",
|
||||
"view_details": "View Details",
|
||||
"view_in_gateway": "View In Controller",
|
||||
"view_json": "View JSON",
|
||||
@@ -346,6 +357,7 @@
|
||||
"error_pushes_one": "Error (could be because of bad configuration): {{count}}",
|
||||
"error_pushes_other": "Errors (could be because of bad configuration): {{count}}",
|
||||
"expert_name": "Expert Mode",
|
||||
"expert_name_explanation": "You can use expert mode to directly modify your configuration, including adding values that are not currently supported by the UI. Please use this format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explanation",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Firmware Upgrade",
|
||||
@@ -367,7 +379,7 @@
|
||||
"push_configuration": "Push Configuration",
|
||||
"push_configuration_error": "Error while trying to push configuration to device: {{e}}",
|
||||
"push_configuration_explanation": "Configuration not pushed, error code {{code}}",
|
||||
"push_success": "Configuration Successfully Pushed!",
|
||||
"push_success": "Configuration was verified and a \"Configure\" command was now initiated by the controller!",
|
||||
"radio_limit": "You have reached the maximum amount of radios (5). You need to delete one of the activated bands to add a new one",
|
||||
"radios": "Radios",
|
||||
"rc_only": "Release Candidates Only",
|
||||
@@ -387,6 +399,7 @@
|
||||
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
|
||||
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
|
||||
"weight": "Weight",
|
||||
"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band",
|
||||
"wifi_frames": "WiFi Frames"
|
||||
},
|
||||
"contacts": {
|
||||
@@ -511,6 +524,7 @@
|
||||
"ouis_explanation": "OUIs of devices that have connected to this firmware server",
|
||||
"outdated_one": "Firmware {{count}} day old",
|
||||
"outdated_other": "Firmware {{count}} days old",
|
||||
"outdated_unknown": "Firmware of unknown age",
|
||||
"release": "Release",
|
||||
"show_dev_releases": "Dev Releases",
|
||||
"status_explanation": "Connection status of devices that have connected to this firmware server",
|
||||
@@ -526,6 +540,14 @@
|
||||
"queue": {
|
||||
"title": "Event Queue"
|
||||
},
|
||||
"radius": {
|
||||
"calling_station_id": "Station",
|
||||
"input_octets": "Input",
|
||||
"output_octets": "Output",
|
||||
"radius_clients": "Radius Clients",
|
||||
"session_time": "Session Time",
|
||||
"username": "Username"
|
||||
},
|
||||
"stats": {
|
||||
"load": "Load (1 | 5 | 15 m.)",
|
||||
"seconds_ago": "{{s}} seconds ago",
|
||||
@@ -596,6 +618,7 @@
|
||||
"certificate_expires_in": "Certificate Expiry",
|
||||
"certificate_expiry": "Cert. Expires In",
|
||||
"connected": "Connected",
|
||||
"crash_logs": "Crash Logs",
|
||||
"create_errors": "errors while trying to create devices",
|
||||
"create_success": " devices successfully created",
|
||||
"current_firmware": "Current Firmware",
|
||||
@@ -609,13 +632,17 @@
|
||||
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
||||
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
||||
"logs_one": "Log",
|
||||
"new_devices": "new devices",
|
||||
"no_model_image": "No Model Image Found",
|
||||
"not_connected": "Not Connected",
|
||||
"not_found_gateway": "Error: device has not yet connected to the controller",
|
||||
"notifications": "Device Notifications",
|
||||
"one": "Device",
|
||||
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
||||
"restricted": "Restricted",
|
||||
"restricted_overriden": "This is a restricted device, but it is in development mode. All restrictions are currently ignored",
|
||||
"restrictions_overriden_title": "Dev Mode",
|
||||
"sanity": "Sanity",
|
||||
"start_import": "Start Device Importation",
|
||||
"test_batch": "Test Import Data",
|
||||
@@ -666,7 +693,15 @@
|
||||
"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"
|
||||
},
|
||||
"firmware": {
|
||||
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
|
||||
"last_db_update_modal": "Firmware Database",
|
||||
"last_db_update_title": "Database",
|
||||
"start_db_update": "Start Database Update",
|
||||
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Powered By",
|
||||
@@ -688,7 +723,7 @@
|
||||
"invalid_ipv6": "Invalid IPv6 address (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Invalid JSON string",
|
||||
"invalid_lease_time": "Invalid lease time value! They need to be in the digitUnit format. For example: 6d2h5m, which means 6 days, 2 hours and 5 minutes. Here are the accepted units: m, h, d. If you do not want to use a unit, omit it completely. So instead of saying 0d2h0m, use 2h",
|
||||
"invalid_mac_uc": "Invalid UC-MAC value, for example: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Invalid MAC value, for example: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Invalid password, please look at the password policy",
|
||||
"invalid_phone_number": "Invalid Phone Number",
|
||||
"invalid_phone_numbers": "One or more of the phone numbers are invalid. Please provide them without symbols and spaces, or in this format: +1(123)123-1234",
|
||||
@@ -697,6 +732,8 @@
|
||||
"invalid_proto_6g": "This encryption protocol cannot be used on an SSID which uses 6G",
|
||||
"invalid_proto_passpoint": "",
|
||||
"invalid_select_ports": "Incompatible values between interfaces! Please make sure that there is no duplicate PORT/VLAN ID combination between your interfaces",
|
||||
"invalid_static_ipv4_d": "Invalid address, this range reserved for multicasting (class D). The first octet should be 223 or lower",
|
||||
"invalid_static_ipv4_e": "Invalid address, this range reserved for experiments (class E). The first octet should be 223 or lower",
|
||||
"invalid_third_party": "Invalid Third Party JSON string. Please confirm that your value is a valid JSON",
|
||||
"key_file_explanation": "Please use a .pem file that starts with \"-----BEGIN PRIVATE KEY-----\" and ends with \"-----END PRIVATE KEY-----\"",
|
||||
"min_max_string": "Value needs to be of a length between {{min}} (inclusive) and {{max}} (inclusive)",
|
||||
@@ -727,8 +764,8 @@
|
||||
"success_remove_claim": "Successfully removed claim on: {{serial}}",
|
||||
"successful_reboots": "Started Rebooting: {{count}}",
|
||||
"successful_upgrades": "Successful upgrades: {{count}}",
|
||||
"tag_one": "Tag",
|
||||
"tags": "Inventory Tags",
|
||||
"tag_one": "Device",
|
||||
"tags": "Inventory Devices",
|
||||
"title": "Inventory",
|
||||
"warning_reboots": "Not connected: {{count}}",
|
||||
"warning_upgrades": "Devices not connected: {{count}}"
|
||||
@@ -742,6 +779,15 @@
|
||||
"successful_macs": "Successful MACs",
|
||||
"title": "Jobs"
|
||||
},
|
||||
"keys": {
|
||||
"description_error": "Description needs to be less than 64 characters long",
|
||||
"expire_error": "The expiry cannot be more than one year in the future",
|
||||
"expires": "Expires",
|
||||
"max_keys": "Max keys reached (10)",
|
||||
"name_error": "Name should be unique and be between 6 and 20 alphanumeric characters",
|
||||
"one": "API Key",
|
||||
"other": "API Keys"
|
||||
},
|
||||
"locations": {
|
||||
"address_line_one": "Address Line One",
|
||||
"address_line_two": "Address Line Two",
|
||||
@@ -749,13 +795,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",
|
||||
@@ -781,6 +831,7 @@
|
||||
"reset_password": "Reset Password",
|
||||
"sign_in": "Sign In",
|
||||
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
||||
"suspended_error": "Suspended account, please contact your administrator",
|
||||
"verification": "Verify your login",
|
||||
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
||||
"welcome_back": "Welcome Back!",
|
||||
@@ -788,6 +839,25 @@
|
||||
"your_new_password": "Your new 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": "Notifications Filter",
|
||||
"security": "Security",
|
||||
"source": "Source",
|
||||
"thread": "Thread",
|
||||
"venue_config": "Configuration",
|
||||
"venue_reboot": "Reboot",
|
||||
"venue_upgrade": "Upgrade"
|
||||
},
|
||||
"map": {
|
||||
"auto_align": "Auto Align",
|
||||
"auto_map": "Auto Map",
|
||||
@@ -804,6 +874,10 @@
|
||||
"title": "Map",
|
||||
"visibility": "Visibility"
|
||||
},
|
||||
"notification": {
|
||||
"one": "Notification",
|
||||
"other": "Notifications"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Are you sure you want to delete this operator? This operation is not reversible",
|
||||
"delete_operator": "Delete Operator",
|
||||
@@ -818,11 +892,31 @@
|
||||
"my_organization": "My Organization",
|
||||
"title": "Organization"
|
||||
},
|
||||
"overrides": {
|
||||
"delete_source": "Delete all overrides from {{source}}",
|
||||
"ignore_overrides": "Ignore Configuration Overrides",
|
||||
"name_error": "Parameter is already defined by your source",
|
||||
"one": "Configuration Override",
|
||||
"other": "Configuration Overrides",
|
||||
"param_name": "Parameter",
|
||||
"param_value": "Value",
|
||||
"parameter": "Parameter",
|
||||
"reason": "Reason",
|
||||
"reason_error": "Your reason needs to be less than 64 chars. long",
|
||||
"source": "Source",
|
||||
"tx_power_error": "Tx power needs to be between 1 and 32",
|
||||
"update_success": "Updated Configuration Overrides!",
|
||||
"value": "Value"
|
||||
},
|
||||
"profile": {
|
||||
"about_me": "About Me",
|
||||
"activate": "Activate",
|
||||
"add_new_note": "Add Note",
|
||||
"deactivate": "Deactivate",
|
||||
"delete_account": "Delete my Profile",
|
||||
"delete_account_confirm": "Delete all of my information",
|
||||
"delete_warning": "This action is non-reversible. All of your profile information and your API keys will be removed",
|
||||
"deleted_success": "Your profile is now deleted, we will now log you out...",
|
||||
"disabled": "Disabled",
|
||||
"enabled": "Enabled",
|
||||
"manage_avatar": "Manage Avatar",
|
||||
@@ -835,6 +929,20 @@
|
||||
"title": "Resources",
|
||||
"variable": "Variable"
|
||||
},
|
||||
"restrictions": {
|
||||
"algo": "Signing Algorithm",
|
||||
"allowed": "Allowed",
|
||||
"countries": "Allowed Countries",
|
||||
"developer": "Developer Mode",
|
||||
"dfs": "DFS Override",
|
||||
"gw_commands": "Gateway Commands",
|
||||
"identifier": "Identifier",
|
||||
"key_verification": "Signing Key Information",
|
||||
"restricted": "Restricted",
|
||||
"signed_upgrade": "Signed Upgrade Only",
|
||||
"title": "Restrictions",
|
||||
"tty": "TTY Access"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithm",
|
||||
"algorithm_other": "Algorithms",
|
||||
@@ -849,6 +957,33 @@
|
||||
"vendor": "Vendor",
|
||||
"version": "Version"
|
||||
},
|
||||
"script": {
|
||||
"author": "Creator",
|
||||
"automatic": "Automatic",
|
||||
"create_success": "Script is now created and ready to use!",
|
||||
"custom_domain": "Custom Domain",
|
||||
"deferred": "Deferred",
|
||||
"device_title": "Run Script",
|
||||
"diagnostics": "Diagnostics",
|
||||
"explanation": "Run a custom script on this device and download its results",
|
||||
"file_not_ready": "Result is not uploaded yet, please come back later",
|
||||
"file_too_large": "Please select a file that is less than 500KB",
|
||||
"helper": "Documentation",
|
||||
"no_script_available": "No script available for your user role",
|
||||
"now": "Now",
|
||||
"one": "Script",
|
||||
"other": "Scripts",
|
||||
"restricted": "Users allowed to run this script",
|
||||
"schedule_success": "Scheduled script execution!",
|
||||
"signature": "Signature",
|
||||
"started_execution": "Started script execution, come later for the results!",
|
||||
"timeout": "Timeout",
|
||||
"update_success": "Script updated!",
|
||||
"upload_destination": "Results Upload Destination",
|
||||
"upload_file": "Upload File",
|
||||
"visit_external_website": "View Documentation",
|
||||
"when": "Schedule Execution"
|
||||
},
|
||||
"service": {
|
||||
"billing_code": "Billing Code",
|
||||
"billing_frequency": "Billing Frequency",
|
||||
@@ -866,6 +1001,8 @@
|
||||
"concurrent_devices": "Concurrent Devices",
|
||||
"controller": "Controller",
|
||||
"current_live_devices": "Current Live Devices",
|
||||
"currently_running_one": "There is currently {{count}} simulation running",
|
||||
"currently_running_other": "There are currently {{count}} simulations running",
|
||||
"delete_success": "Deleted Simulation!",
|
||||
"duration": "Duration",
|
||||
"error_devices": "Error Devices",
|
||||
@@ -889,6 +1026,7 @@
|
||||
"rx_messages": "Rx Messages",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" in progress",
|
||||
"sim_history": "{{sim}} Previous Runs",
|
||||
"simulated": "Simulated",
|
||||
"start": "Start Simulation",
|
||||
"start_success": "Started simulation run!",
|
||||
"state_interval": "State Interval",
|
||||
@@ -922,6 +1060,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Back-End Logs",
|
||||
"configuration": "Configuration",
|
||||
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
||||
"endpoint": "Endpoint",
|
||||
"hostname": "Host Name",
|
||||
@@ -932,6 +1071,10 @@
|
||||
"os": "Operating System",
|
||||
"processors": "Processors",
|
||||
"reload_chosen_subsystems": "Reload Chosen Subsystems",
|
||||
"secrets": "Secrets",
|
||||
"secrets_create": "Create Secret",
|
||||
"secrets_one": "Secret",
|
||||
"services": "Services",
|
||||
"start": "Start",
|
||||
"subsystems": "Subsystems",
|
||||
"success_reload": "Successfully sent reload command!",
|
||||
@@ -943,19 +1086,31 @@
|
||||
"version": "Version"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Columns",
|
||||
"columns_hidden_one": "{{count}} Column Hidden",
|
||||
"columns_hidden_other": "{{count}} Columns Hidden",
|
||||
"display_column": "Display",
|
||||
"drag_always_show": "You cannot hide this column but can change its position ",
|
||||
"drag_explanation": "Drag and drop to reorder and change column visibility",
|
||||
"drag_locked": "This column is locked in its position",
|
||||
"export_current_page": "Current Page Only",
|
||||
"first_page": "First Page",
|
||||
"go_to_page": "Go to page",
|
||||
"hide_column": "Hide",
|
||||
"last_page": "Last Page",
|
||||
"next_page": "Next Page",
|
||||
"page": "Page",
|
||||
"previous_page": "Previous Page"
|
||||
"preferences": "Table Preferences",
|
||||
"previous_page": "Previous Page",
|
||||
"reset": "Reset Preferences",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "email not validated",
|
||||
"error_fetching": "Error fetching user information: {{e}}",
|
||||
"password": "Password",
|
||||
"role": "Role",
|
||||
"suspended": "suspended",
|
||||
"title": "User"
|
||||
},
|
||||
"users": {
|
||||
@@ -1000,9 +1155,11 @@
|
||||
"successfully_update_devices": "Updating {{num}} devices!",
|
||||
"title": "Venues",
|
||||
"update_all_devices": "Update All Device Configurations",
|
||||
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
|
||||
"update_success": "Venue updated!",
|
||||
"upgrade_all_devices": "Upgrade All Devices Firmware",
|
||||
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
|
||||
"upgrade_all_devices_success": "Successfully started upgrading devices!",
|
||||
"upgrade_options_available": "Here are all available revisions, please select the one you want ALL of this venue's devices to be upgrade to",
|
||||
"use_existing": "Use Existing",
|
||||
"use_existing_contacts": "Use Existing Contacts",
|
||||
"use_this_contact": "Use this contact"
|
||||
|
||||
@@ -79,7 +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. 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",
|
||||
@@ -90,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",
|
||||
@@ -174,6 +180,7 @@
|
||||
"other": "comandos",
|
||||
"override_dfs": "Anular DFS",
|
||||
"reboot": "Reiniciar",
|
||||
"reboot_description": "¿Quieres reiniciar este dispositivo?",
|
||||
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
|
||||
"reboot_success": "¡Comando de reinicio enviado con éxito!",
|
||||
"revision": "revisión",
|
||||
@@ -234,6 +241,7 @@
|
||||
"error_download": "Error al intentar descargar: {{e}}",
|
||||
"errors": "Los errores",
|
||||
"exit_fullscreen": "salida",
|
||||
"export": "Exportar",
|
||||
"finished": "terminado",
|
||||
"fullscreen": "Pantalla Completa",
|
||||
"general_error": "Error al conectar con el servidor. Consulte a su administrador.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"identification": "identificación",
|
||||
"inherit": "Heredar",
|
||||
"language": "idioma",
|
||||
"last_use": "Utilizado por última vez",
|
||||
"lifetime": "Toda la vida",
|
||||
"locale": "lugar",
|
||||
"logout": "Cerrar sesión",
|
||||
@@ -261,6 +270,7 @@
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
"monthly": "Mensual",
|
||||
"months": "Meses",
|
||||
"my_account": "Mi cuenta",
|
||||
"name": "Nombre",
|
||||
"name_error": "El nombre debe tener menos de 50 caracteres",
|
||||
@@ -314,6 +324,7 @@
|
||||
"use_file": "Usar archivo",
|
||||
"value": "Valor",
|
||||
"variable": "Variable",
|
||||
"view": "Ver",
|
||||
"view_details": "Ver detalles",
|
||||
"view_in_gateway": "Ver en controlador",
|
||||
"view_json": "Ver JSON",
|
||||
@@ -346,6 +357,7 @@
|
||||
"error_pushes_one": "Error (podría deberse a una mala configuración): {{count}}",
|
||||
"error_pushes_other": "Errores (pueden deberse a una mala configuración): {{count}}",
|
||||
"expert_name": "Modo experto",
|
||||
"expert_name_explanation": "Puede usar el modo experto para modificar directamente su configuración, incluida la adición de valores que actualmente no son compatibles con la interfaz de usuario. Utilice este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explicación",
|
||||
"firewall": "cortafuegos",
|
||||
"firmware_upgrade": "Actualización de firmware",
|
||||
@@ -367,7 +379,7 @@
|
||||
"push_configuration": "Configuración de inserción",
|
||||
"push_configuration_error": "Error al intentar enviar la configuración al dispositivo: {{e}}",
|
||||
"push_configuration_explanation": "Configuración no enviada, código de error {{code}}",
|
||||
"push_success": "¡Configuración presionada con éxito!",
|
||||
"push_success": "¡Se verificó la configuración y ahora el controlador inició un comando \"Configurar\"!",
|
||||
"radio_limit": "Has alcanzado la cantidad máxima de radios (5). Necesita eliminar una de las bandas activadas para agregar una nueva",
|
||||
"radios": "Radios",
|
||||
"rc_only": "Solo candidatos de lanzamiento",
|
||||
@@ -387,6 +399,7 @@
|
||||
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||
"weight": "Peso",
|
||||
"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi",
|
||||
"wifi_frames": "Marcos WiFi"
|
||||
},
|
||||
"contacts": {
|
||||
@@ -511,6 +524,7 @@
|
||||
"ouis_explanation": "OUI de dispositivos que se han conectado a este servidor de firmware",
|
||||
"outdated_one": "Firmware {{count}} día de antigüedad",
|
||||
"outdated_other": "Firmware de {{count}} días de antigüedad",
|
||||
"outdated_unknown": "Firmware de antigüedad desconocida",
|
||||
"release": "Lanzamiento",
|
||||
"show_dev_releases": "Lanzamientos de desarrollo",
|
||||
"status_explanation": "Estado de conexión de los dispositivos que se han conectado a este servidor de firmware",
|
||||
@@ -526,6 +540,14 @@
|
||||
"queue": {
|
||||
"title": "Cola de eventos"
|
||||
},
|
||||
"radius": {
|
||||
"calling_station_id": "Estación",
|
||||
"input_octets": "entrada",
|
||||
"output_octets": "salida",
|
||||
"radius_clients": "Clientes de radio",
|
||||
"session_time": "Tiempo de sesión",
|
||||
"username": "Nombre de usuario"
|
||||
},
|
||||
"stats": {
|
||||
"load": "Carga (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Hace {{s}} segundos",
|
||||
@@ -596,6 +618,7 @@
|
||||
"certificate_expires_in": "El certificado caduca en",
|
||||
"certificate_expiry": "Cert. Expira en",
|
||||
"connected": "Conectado",
|
||||
"crash_logs": "Registros de fallas",
|
||||
"create_errors": "errores al intentar crear dispositivos",
|
||||
"create_success": " dispositivos creados con éxito",
|
||||
"current_firmware": "Firmware actual",
|
||||
@@ -609,13 +632,17 @@
|
||||
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
||||
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
||||
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
||||
"logs_one": "Iniciar sesión",
|
||||
"new_devices": "Nuevos dispositivos",
|
||||
"no_model_image": "No se encontró ninguna imagen de modelo",
|
||||
"not_connected": "No conectado",
|
||||
"not_found_gateway": "Error: el dispositivo aún no se ha conectado a la puerta de enlace",
|
||||
"notifications": "notificaciones de dispositivos",
|
||||
"one": "Dispositivo",
|
||||
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
||||
"restricted": "Restringido",
|
||||
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
|
||||
"restrictions_overriden_title": "MODO DE DESARROLLO",
|
||||
"sanity": "Cordura",
|
||||
"start_import": "Iniciar la importación de dispositivos",
|
||||
"test_batch": "Datos de importación de prueba",
|
||||
@@ -666,7 +693,15 @@
|
||||
"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"
|
||||
},
|
||||
"firmware": {
|
||||
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
|
||||
"last_db_update_modal": "Base de datos de firmware",
|
||||
"last_db_update_title": "Base de datos",
|
||||
"start_db_update": "Iniciar actualización de la base de datos",
|
||||
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "energizado por",
|
||||
@@ -688,7 +723,7 @@
|
||||
"invalid_ipv6": "Dirección IPv6 no válida (ej.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Cadena JSON no válida",
|
||||
"invalid_lease_time": "¡Valor de tiempo de arrendamiento no válido! Deben estar en el formato digitUnit. Por ejemplo: 6d2h5m, lo que significa 6 días, 2 horas y 5 minutos. Estas son las unidades aceptadas: m, h, d. Si no desea utilizar una unidad, omítala por completo. Entonces, en lugar de decir 0d2h0m, usa 2h",
|
||||
"invalid_mac_uc": "Valor de UC-MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valor de MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Contraseña no válida, consulte la política de contraseñas",
|
||||
"invalid_phone_number": "Numero de telefono invalido",
|
||||
"invalid_phone_numbers": "Uno o más de los números de teléfono no son válidos. Proporciónelos sin símbolos ni espacios, o en este formato: +1(123)123-1234",
|
||||
@@ -697,6 +732,8 @@
|
||||
"invalid_proto_6g": "Este protocolo de encriptación no se puede usar en un SSID que usa 6G",
|
||||
"invalid_proto_passpoint": "Este protocolo de cifrado no se puede utilizar con un SSID de punto de acceso. Seleccione un protocolo que pueda usar Radius",
|
||||
"invalid_select_ports": "¡Valores incompatibles entre interfaces! Asegúrese de que no haya una combinación de ID de VLAN/PUERTO duplicada entre sus interfaces",
|
||||
"invalid_static_ipv4_d": "Dirección no válida, este rango está reservado para multidifusión (clase D). El primer octeto debe ser 223 o inferior",
|
||||
"invalid_static_ipv4_e": "Dirección no válida, este rango reservado para experimentos (clase E). El primer octeto debe ser 223 o inferior",
|
||||
"invalid_third_party": "Cadena JSON de terceros no válida. Confirme que su valor es un JSON válido",
|
||||
"key_file_explanation": "Utilice un archivo .pem que comience con \"-----BEGIN PRIVATE KEY-----\" y termine con \"-----END PRIVATE KEY-----\"",
|
||||
"min_max_string": "El valor debe tener una longitud entre {{min}} (inclusive) y {{max}} (inclusive)",
|
||||
@@ -742,6 +779,15 @@
|
||||
"successful_macs": "MAC exitosos",
|
||||
"title": "Trabajos"
|
||||
},
|
||||
"keys": {
|
||||
"description_error": "La descripción debe tener menos de 64 caracteres",
|
||||
"expire_error": "El vencimiento no puede ser más de un año en el futuro",
|
||||
"expires": "Vence",
|
||||
"max_keys": "Número máximo de claves alcanzado (10)",
|
||||
"name_error": "El nombre debe ser único y tener entre 6 y 20 caracteres alfanuméricos",
|
||||
"one": "Clave API",
|
||||
"other": "Claves de api"
|
||||
},
|
||||
"locations": {
|
||||
"address_line_one": "Dirección Línea Uno",
|
||||
"address_line_two": "Dirección línea dos",
|
||||
@@ -749,13 +795,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",
|
||||
@@ -781,6 +831,7 @@
|
||||
"reset_password": "Restablecer la contraseña",
|
||||
"sign_in": "Registrarse",
|
||||
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
||||
"suspended_error": "Cuenta suspendida, comuníquese con su administrador",
|
||||
"verification": "Verifica tu inicio de sesión",
|
||||
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
||||
"welcome_back": "¡Dar una buena acogida!",
|
||||
@@ -788,6 +839,25 @@
|
||||
"your_new_password": "Tu nueva 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": "Filtro de notificaciones",
|
||||
"security": "SEGURIDAD",
|
||||
"source": "Fuente",
|
||||
"thread": "Hilo",
|
||||
"venue_config": "Configuración",
|
||||
"venue_reboot": "Reiniciar",
|
||||
"venue_upgrade": "Mejorar"
|
||||
},
|
||||
"map": {
|
||||
"auto_align": "Alineación automática",
|
||||
"auto_map": "Mapa automático",
|
||||
@@ -804,6 +874,10 @@
|
||||
"title": "Mapa",
|
||||
"visibility": "Visibilidad"
|
||||
},
|
||||
"notification": {
|
||||
"one": "Notificación",
|
||||
"other": "Notificaciones"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "¿Está seguro de que desea eliminar este operador? Esta operación no es reversible.",
|
||||
"delete_operator": "Eliminar operador",
|
||||
@@ -818,11 +892,31 @@
|
||||
"my_organization": "MI ORGANIZACION",
|
||||
"title": "Organización"
|
||||
},
|
||||
"overrides": {
|
||||
"delete_source": "Eliminar todas las anulaciones de {{source}}",
|
||||
"ignore_overrides": "Ignorar anulaciones de configuración",
|
||||
"name_error": "El parámetro ya está definido por su fuente",
|
||||
"one": "Anulación de configuración",
|
||||
"other": "Anulaciones de configuración",
|
||||
"param_name": "parámetro",
|
||||
"param_value": "Valor",
|
||||
"parameter": "parámetro",
|
||||
"reason": "Razón",
|
||||
"reason_error": "Su motivo debe tener menos de 64 caracteres. largo",
|
||||
"source": "Fuente",
|
||||
"tx_power_error": "La potencia Tx debe estar entre 1 y 32",
|
||||
"update_success": "¡Anulaciones de configuración actualizadas!",
|
||||
"value": "Valor"
|
||||
},
|
||||
"profile": {
|
||||
"about_me": "Sobre mí",
|
||||
"activate": "",
|
||||
"add_new_note": "Añadir la nota",
|
||||
"deactivate": "Desactivar",
|
||||
"delete_account": "Eliminar mi perfil",
|
||||
"delete_account_confirm": "Eliminar toda mi información",
|
||||
"delete_warning": "Esta acción no es reversible. Toda la información de su perfil y sus claves API serán eliminadas",
|
||||
"deleted_success": "Su perfil ahora está eliminado, ahora cerraremos su sesión...",
|
||||
"disabled": "Discapacitado",
|
||||
"enabled": "Habilitado",
|
||||
"manage_avatar": "Administrar avatar",
|
||||
@@ -835,6 +929,20 @@
|
||||
"title": "Recursos",
|
||||
"variable": "Variable"
|
||||
},
|
||||
"restrictions": {
|
||||
"algo": "Algoritmo de firma",
|
||||
"allowed": "Permitido",
|
||||
"countries": "países permitidos",
|
||||
"developer": "Modo desarrollador",
|
||||
"dfs": "Anulación de DFS",
|
||||
"gw_commands": "Comandos de puerta de enlace",
|
||||
"identifier": "Identificador",
|
||||
"key_verification": "Información clave de firma",
|
||||
"restricted": "Restringido",
|
||||
"signed_upgrade": "Solo actualización firmada",
|
||||
"title": "Las restricciones",
|
||||
"tty": "Acceso TTY"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
@@ -849,6 +957,33 @@
|
||||
"vendor": "Vendedor",
|
||||
"version": "Versión"
|
||||
},
|
||||
"script": {
|
||||
"author": "Creador",
|
||||
"automatic": "Automático",
|
||||
"create_success": "¡El script ahora está creado y listo para usar!",
|
||||
"custom_domain": "Dominio personalizado",
|
||||
"deferred": "Diferido",
|
||||
"device_title": "Ejecutar guión",
|
||||
"diagnostics": "Diagnósticos",
|
||||
"explanation": "Ejecute un script personalizado en este dispositivo y descargue sus resultados",
|
||||
"file_not_ready": "El resultado aún no se ha subido, vuelva más tarde",
|
||||
"file_too_large": "Seleccione un archivo que tenga menos de 500 KB",
|
||||
"helper": "Documentación",
|
||||
"no_script_available": "No hay script disponible para su rol de usuario",
|
||||
"now": "ahora",
|
||||
"one": "Guión",
|
||||
"other": "Guiones",
|
||||
"restricted": "Usuarios autorizados a ejecutar este script",
|
||||
"schedule_success": "¡Ejecución de script programada!",
|
||||
"signature": "Firma",
|
||||
"started_execution": "Comenzó la ejecución del script, ¡venga más tarde para conocer los resultados!",
|
||||
"timeout": "Se acabó el tiempo",
|
||||
"update_success": "Guión actualizado!",
|
||||
"upload_destination": "Destino de carga de resultados",
|
||||
"upload_file": "Subir archivo",
|
||||
"visit_external_website": "VER DOCUMENTACIÓN",
|
||||
"when": "Programar Ejecucion"
|
||||
},
|
||||
"service": {
|
||||
"billing_code": "Código de facturación",
|
||||
"billing_frequency": "Frecuencia de facturación",
|
||||
@@ -866,6 +1001,8 @@
|
||||
"concurrent_devices": "Dispositivos concurrentes",
|
||||
"controller": "Controlador",
|
||||
"current_live_devices": "Dispositivos activos actuales",
|
||||
"currently_running_one": "Actualmente hay {{count}} simulación en ejecución",
|
||||
"currently_running_other": "Actualmente hay {{count}} simulaciones ejecutándose",
|
||||
"delete_success": "¡Simulación eliminada!",
|
||||
"duration": "Duración",
|
||||
"error_devices": "Dispositivos de error",
|
||||
@@ -889,6 +1026,7 @@
|
||||
"rx_messages": "Mensajes prescritos",
|
||||
"sim_currently_running": "Simulación \"{{sim}}\" en curso",
|
||||
"sim_history": "{{sim}} ejecuciones anteriores",
|
||||
"simulated": "Simulado",
|
||||
"start": "Iniciar simulación",
|
||||
"start_success": "¡Ejecución de simulación iniciada!",
|
||||
"state_interval": "Intervalo de estado",
|
||||
@@ -922,6 +1060,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Registros de back-end",
|
||||
"configuration": "Configuración",
|
||||
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
||||
"endpoint": "punto final",
|
||||
"hostname": "Nombre de host",
|
||||
@@ -932,6 +1071,10 @@
|
||||
"os": "sistema operativo",
|
||||
"processors": "Procesadores",
|
||||
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
|
||||
"secrets": "Misterios",
|
||||
"secrets_create": "Crear secreto",
|
||||
"secrets_one": "secreto",
|
||||
"services": "Servicios",
|
||||
"start": "comienzo",
|
||||
"subsystems": "Subsistemas",
|
||||
"success_reload": "¡Comando de recarga enviado con éxito!",
|
||||
@@ -943,19 +1086,31 @@
|
||||
"version": "Versión"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Columnas",
|
||||
"columns_hidden_one": "{{count}} columna oculta",
|
||||
"columns_hidden_other": "{{count}} columnas ocultas",
|
||||
"display_column": "Monitor",
|
||||
"drag_always_show": "No puede ocultar esta columna pero puede cambiar su posición",
|
||||
"drag_explanation": "Arrastre y suelte para reordenar y cambiar la visibilidad de las columnas",
|
||||
"drag_locked": "Esta columna está bloqueada en su posición.",
|
||||
"export_current_page": "Solo página actual",
|
||||
"first_page": "Primera pagina",
|
||||
"go_to_page": "Ir a la página",
|
||||
"hide_column": "Esconder",
|
||||
"last_page": "Ultima pagina",
|
||||
"next_page": "Siguiente página",
|
||||
"page": "Página",
|
||||
"previous_page": "Página anterior"
|
||||
"preferences": "Preferencias de mesa",
|
||||
"previous_page": "Página anterior",
|
||||
"reset": "Reiniciar preferencias",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "correo electrónico no validado",
|
||||
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
||||
"password": "Contraseña",
|
||||
"role": "papel",
|
||||
"suspended": "Suspendido",
|
||||
"title": "Usuario"
|
||||
},
|
||||
"users": {
|
||||
@@ -1000,9 +1155,11 @@
|
||||
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
||||
"title": "Sedes",
|
||||
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
||||
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
|
||||
"update_success": "Lugar actualizado!",
|
||||
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
|
||||
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
|
||||
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
|
||||
"upgrade_options_available": "Aquí están todas las revisiones disponibles, seleccione la que desea que TODOS los dispositivos de este lugar se actualicen",
|
||||
"use_existing": "Utilizar existente",
|
||||
"use_existing_contacts": "Usar contactos existentes",
|
||||
"use_this_contact": "Usa este contacto"
|
||||
|
||||
@@ -79,7 +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 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",
|
||||
@@ -90,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",
|
||||
@@ -174,6 +180,7 @@
|
||||
"other": "Les commandes",
|
||||
"override_dfs": "Remplacer DFS",
|
||||
"reboot": "Redémarrer",
|
||||
"reboot_description": "Voulez-vous redémarrer cet appareil ?",
|
||||
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
|
||||
"reboot_success": "Commande de redémarrage envoyée avec succès !",
|
||||
"revision": "Révision",
|
||||
@@ -234,6 +241,7 @@
|
||||
"error_download": "Erreur lors de la tentative de téléchargement : {{e}}",
|
||||
"errors": "les erreurs",
|
||||
"exit_fullscreen": "Sortie",
|
||||
"export": "Exportation",
|
||||
"finished": "fini",
|
||||
"fullscreen": "Plein écran",
|
||||
"general_error": "Erreur de connexion au serveur. Veuillez consulter votre administrateur.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"identification": "Identification",
|
||||
"inherit": "Hériter",
|
||||
"language": "La langue",
|
||||
"last_use": "Dernière utilisation",
|
||||
"lifetime": "durée de vie",
|
||||
"locale": "lieu",
|
||||
"logout": "Connectez - Out",
|
||||
@@ -261,6 +270,7 @@
|
||||
"model": "Modèle",
|
||||
"modified": "Modifié",
|
||||
"monthly": "Mensuel",
|
||||
"months": "mois",
|
||||
"my_account": "Mon compte",
|
||||
"name": "Prénom",
|
||||
"name_error": "Le nom doit comporter moins de 50 caractères",
|
||||
@@ -314,6 +324,7 @@
|
||||
"use_file": "Utiliser le fichier",
|
||||
"value": "Valeur",
|
||||
"variable": "Variable",
|
||||
"view": "Vue",
|
||||
"view_details": "Voir les détails",
|
||||
"view_in_gateway": "Afficher dans le contrôleur",
|
||||
"view_json": "Afficher JSON",
|
||||
@@ -346,6 +357,7 @@
|
||||
"error_pushes_one": "Erreur (peut être due à une mauvaise configuration) : {{count}}",
|
||||
"error_pushes_other": "Erreurs (peut-être dues à une mauvaise configuration) : {{count}}",
|
||||
"expert_name": "Mode expert",
|
||||
"expert_name_explanation": "Vous pouvez utiliser le mode expert pour modifier directement votre configuration, notamment en ajoutant des valeurs qui ne sont pas actuellement prises en charge par l'interface utilisateur. Veuillez utiliser ce format : { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explication",
|
||||
"firewall": "Pare-feu",
|
||||
"firmware_upgrade": "Mise à jour du firmware",
|
||||
@@ -367,7 +379,7 @@
|
||||
"push_configuration": "Pousser la configuration",
|
||||
"push_configuration_error": "Erreur lors de la tentative d'envoi de la configuration sur l'appareil : {{e}}",
|
||||
"push_configuration_explanation": "Configuration non poussée, code d'erreur {{code}}",
|
||||
"push_success": "Configuration poussée avec succès !",
|
||||
"push_success": "La configuration a été vérifiée et une commande \"Configurer\" a maintenant été lancée par le contrôleur !",
|
||||
"radio_limit": "Vous avez atteint le nombre maximum de radios (5). Vous devez supprimer une des bandes activées pour en ajouter une nouvelle",
|
||||
"radios": "Radios",
|
||||
"rc_only": "Libérer les candidats uniquement",
|
||||
@@ -387,6 +399,7 @@
|
||||
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
|
||||
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
|
||||
"weight": "Poids",
|
||||
"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi",
|
||||
"wifi_frames": "Cadres Wi-Fi"
|
||||
},
|
||||
"contacts": {
|
||||
@@ -511,6 +524,7 @@
|
||||
"ouis_explanation": "OUI des appareils qui se sont connectés à ce serveur de firmware",
|
||||
"outdated_one": "Micrologiciel vieux de {{count}} jours",
|
||||
"outdated_other": "Micrologiciel vieux de {{count}} jours",
|
||||
"outdated_unknown": "Firmware d'âge inconnu",
|
||||
"release": "libération",
|
||||
"show_dev_releases": "Versions de développement",
|
||||
"status_explanation": "État de connexion des appareils qui se sont connectés à ce serveur de micrologiciel",
|
||||
@@ -526,6 +540,14 @@
|
||||
"queue": {
|
||||
"title": "File d'attente d'événements"
|
||||
},
|
||||
"radius": {
|
||||
"calling_station_id": "Station",
|
||||
"input_octets": "Contribution",
|
||||
"output_octets": "Sortie",
|
||||
"radius_clients": "Clients rayon",
|
||||
"session_time": "Temps de session",
|
||||
"username": "Nom d'utilisateur"
|
||||
},
|
||||
"stats": {
|
||||
"load": "Charge (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Il y a {{s}} secondes",
|
||||
@@ -596,6 +618,7 @@
|
||||
"certificate_expires_in": "Le certificat expire dans",
|
||||
"certificate_expiry": "Cert. Expire dans",
|
||||
"connected": "Connecté",
|
||||
"crash_logs": "Journaux des plantages",
|
||||
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
||||
"create_success": " appareils créés avec succès",
|
||||
"current_firmware": "Firmware actuel",
|
||||
@@ -609,13 +632,17 @@
|
||||
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
||||
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
||||
"logs_one": "Bûche",
|
||||
"new_devices": "nouveaux appareils",
|
||||
"no_model_image": "Aucune image de modèle trouvée",
|
||||
"not_connected": "Pas connecté",
|
||||
"not_found_gateway": "Erreur : l'appareil n'est pas encore connecté à la passerelle",
|
||||
"notifications": "notifications de l'appareil",
|
||||
"one": "Dispositif",
|
||||
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
||||
"restricted": "Limité",
|
||||
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
|
||||
"restrictions_overriden_title": "Mode développement",
|
||||
"sanity": "Santé mentale",
|
||||
"start_import": "Démarrer l'importation de l'appareil",
|
||||
"test_batch": "Tester les données d'importation",
|
||||
@@ -666,7 +693,15 @@
|
||||
"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"
|
||||
},
|
||||
"firmware": {
|
||||
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
|
||||
"last_db_update_modal": "Base de données du micrologiciel",
|
||||
"last_db_update_title": "Base de données",
|
||||
"start_db_update": "Démarrer la mise à jour de la base de données",
|
||||
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Alimenté par",
|
||||
@@ -688,7 +723,7 @@
|
||||
"invalid_ipv6": "Adresse IPv6 invalide (ex. : 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Chaîne JSON non valide",
|
||||
"invalid_lease_time": "Valeur de durée de bail non valide ! Ils doivent être au format digitUnit. Par exemple : 6d2h5m, ce qui signifie 6 jours, 2 heures et 5 minutes. Voici les unités acceptées : m, h, d. Si vous ne voulez pas utiliser une unité, omettez-la complètement. Donc au lieu de dire 0d2h0m, utilisez 2h",
|
||||
"invalid_mac_uc": "Valeur UC-MAC non valide, par exemple : 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valeur MAC non valide, par exemple : 00:00:5e:00:53:af",
|
||||
"invalid_password": "Mot de passe invalide, veuillez consulter la politique de mot de passe",
|
||||
"invalid_phone_number": "Numéro de téléphone invalide",
|
||||
"invalid_phone_numbers": "Un ou plusieurs des numéros de téléphone sont invalides. Veuillez les fournir sans symboles ni espaces, ou dans ce format : +1(123)123-1234",
|
||||
@@ -697,6 +732,8 @@
|
||||
"invalid_proto_6g": "Ce protocole de cryptage ne peut pas être utilisé sur un SSID qui utilise la 6G",
|
||||
"invalid_proto_passpoint": "Ce protocole de cryptage ne peut pas être utilisé avec un SSID de point de passe. Veuillez sélectionner un protocole qui peut utiliser Radius",
|
||||
"invalid_select_ports": "Valeurs incompatibles entre les interfaces ! Veuillez vous assurer qu'il n'y a pas de combinaison PORT/VLAN ID en double entre vos interfaces",
|
||||
"invalid_static_ipv4_d": "Adresse invalide, cette plage est réservée à la multidiffusion (classe D). Le premier octet doit être 223 ou moins",
|
||||
"invalid_static_ipv4_e": "Adresse invalide, cette plage est réservée aux expérimentations (classe E). Le premier octet doit être 223 ou moins",
|
||||
"invalid_third_party": "Chaîne JSON tierce non valide. Veuillez confirmer que votre valeur est un JSON valide",
|
||||
"key_file_explanation": "Veuillez utiliser un fichier .pem qui commence par \"-----BEGIN PRIVATE KEY-----\" et se termine par \"-----END PRIVATE KEY-----\"",
|
||||
"min_max_string": "La valeur doit être d'une longueur comprise entre {{min}} (inclus) et {{max}} (inclus)",
|
||||
@@ -742,6 +779,15 @@
|
||||
"successful_macs": "MAC réussis",
|
||||
"title": "Emplois"
|
||||
},
|
||||
"keys": {
|
||||
"description_error": "La description doit comporter moins de 64 caractères",
|
||||
"expire_error": "L'expiration ne peut pas être supérieure à un an dans le futur",
|
||||
"expires": "EXPIRÉ",
|
||||
"max_keys": "Max de clés atteint (10)",
|
||||
"name_error": "Le nom doit être unique et comporter entre 6 et 20 caractères alphanumériques",
|
||||
"one": "Clé API",
|
||||
"other": "Clés API"
|
||||
},
|
||||
"locations": {
|
||||
"address_line_one": "Adresse Ligne 1",
|
||||
"address_line_two": "Adresse ligne deux",
|
||||
@@ -749,13 +795,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",
|
||||
@@ -781,6 +831,7 @@
|
||||
"reset_password": "Réinitialiser le mot de passe",
|
||||
"sign_in": "se connecter",
|
||||
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
||||
"suspended_error": "Compte suspendu, veuillez contacter votre administrateur",
|
||||
"verification": "Vérifiez votre connexion",
|
||||
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
||||
"welcome_back": "Nous saluons le retour!",
|
||||
@@ -788,6 +839,25 @@
|
||||
"your_new_password": "Votre nouveau 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": "Filtre de notification",
|
||||
"security": "SÉCURITÉ",
|
||||
"source": "La source",
|
||||
"thread": "Fil de discussion",
|
||||
"venue_config": "Configuration",
|
||||
"venue_reboot": "Redémarrer",
|
||||
"venue_upgrade": "Améliorer"
|
||||
},
|
||||
"map": {
|
||||
"auto_align": "Alignement automatique",
|
||||
"auto_map": "Carte automatique",
|
||||
@@ -804,6 +874,10 @@
|
||||
"title": "Carte",
|
||||
"visibility": "Visibilité"
|
||||
},
|
||||
"notification": {
|
||||
"one": "Notification",
|
||||
"other": "Les notifications"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Voulez-vous vraiment supprimer cet opérateur ? Cette opération n'est pas réversible",
|
||||
"delete_operator": "Supprimer l'opérateur",
|
||||
@@ -818,11 +892,31 @@
|
||||
"my_organization": "Mon organisation",
|
||||
"title": "Organisation"
|
||||
},
|
||||
"overrides": {
|
||||
"delete_source": "Supprimer tous les remplacements de {{source}}",
|
||||
"ignore_overrides": "Ignorer les remplacements de configuration",
|
||||
"name_error": "Le paramètre est déjà défini par votre source",
|
||||
"one": "Remplacement de la configuration",
|
||||
"other": "Remplacements de configuration",
|
||||
"param_name": "paramètre",
|
||||
"param_value": "Valeur",
|
||||
"parameter": "paramètre",
|
||||
"reason": "raison",
|
||||
"reason_error": "Votre raison doit être inférieure à 64 caractères. long",
|
||||
"source": "La source",
|
||||
"tx_power_error": "La puissance de transmission doit être comprise entre 1 et 32",
|
||||
"update_success": "Remplacements de configuration mis à jour !",
|
||||
"value": "Valeur"
|
||||
},
|
||||
"profile": {
|
||||
"about_me": "À propos de moi",
|
||||
"activate": "",
|
||||
"add_new_note": "Ajouter une note",
|
||||
"deactivate": "Désactiver",
|
||||
"delete_account": "Supprimer mon profil",
|
||||
"delete_account_confirm": "Supprimer toutes mes informations",
|
||||
"delete_warning": "Cette action est irréversible. Toutes les informations de votre profil et vos clés API seront supprimées",
|
||||
"deleted_success": "Votre profil est maintenant supprimé, nous allons maintenant vous déconnecter...",
|
||||
"disabled": "Désactivé",
|
||||
"enabled": "Activée",
|
||||
"manage_avatar": "Gérer l'avatar",
|
||||
@@ -835,6 +929,20 @@
|
||||
"title": "Ressources",
|
||||
"variable": "Variable"
|
||||
},
|
||||
"restrictions": {
|
||||
"algo": "Algorithme de signature",
|
||||
"allowed": "Permis",
|
||||
"countries": "Pays autorisés",
|
||||
"developer": "Mode développeur",
|
||||
"dfs": "Remplacement DFS",
|
||||
"gw_commands": "Commandes de passerelle",
|
||||
"identifier": "Identifiant",
|
||||
"key_verification": "Signature des informations clés",
|
||||
"restricted": "Limité",
|
||||
"signed_upgrade": "Mise à niveau signée uniquement",
|
||||
"title": "Restrictions",
|
||||
"tty": "Accès ATS"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algorithme",
|
||||
"algorithm_other": "Algorithmes",
|
||||
@@ -849,6 +957,33 @@
|
||||
"vendor": "vendeur",
|
||||
"version": "Version"
|
||||
},
|
||||
"script": {
|
||||
"author": "Créateur",
|
||||
"automatic": "Automatique",
|
||||
"create_success": "Le script est maintenant créé et prêt à être utilisé !",
|
||||
"custom_domain": "Domaine personnalisé",
|
||||
"deferred": "Différé",
|
||||
"device_title": "Script de lancement",
|
||||
"diagnostics": "Diagnostics",
|
||||
"explanation": "Exécutez un script personnalisé sur cet appareil et téléchargez ses résultats",
|
||||
"file_not_ready": "Le résultat n'est pas encore téléchargé, veuillez revenir plus tard",
|
||||
"file_too_large": "Veuillez sélectionner un fichier de moins de 500 Ko",
|
||||
"helper": "Documentation",
|
||||
"no_script_available": "Aucun script disponible pour votre rôle d'utilisateur",
|
||||
"now": "À présent",
|
||||
"one": "Scénario",
|
||||
"other": "scripts",
|
||||
"restricted": "Utilisateurs autorisés à exécuter ce script",
|
||||
"schedule_success": "Exécution du script planifié !",
|
||||
"signature": "signature",
|
||||
"started_execution": "Lancement de l'exécution du script, venez plus tard pour les résultats !",
|
||||
"timeout": "Temps libre",
|
||||
"update_success": "Scénario mis à jour !",
|
||||
"upload_destination": "Destination de téléchargement des résultats",
|
||||
"upload_file": "Téléverser un fichier",
|
||||
"visit_external_website": "Afficher la documentation",
|
||||
"when": "Planifier l'exécution"
|
||||
},
|
||||
"service": {
|
||||
"billing_code": "Code de facturation",
|
||||
"billing_frequency": "Fréquence de facturation",
|
||||
@@ -866,6 +1001,8 @@
|
||||
"concurrent_devices": "Périphériques simultanés",
|
||||
"controller": "Manette",
|
||||
"current_live_devices": "Appareils en direct actuels",
|
||||
"currently_running_one": "Il y a actuellement {{count}} simulation en cours",
|
||||
"currently_running_other": "Il y a actuellement {{count}} simulations en cours d'exécution",
|
||||
"delete_success": "Simulation supprimée !",
|
||||
"duration": "Durée",
|
||||
"error_devices": "Périphériques d'erreur",
|
||||
@@ -889,6 +1026,7 @@
|
||||
"rx_messages": "Messages reçus",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" en cours",
|
||||
"sim_history": "{{sim}} courses précédentes",
|
||||
"simulated": "Simulé",
|
||||
"start": "Démarrer la simulation",
|
||||
"start_success": "Lancement de la simulation !",
|
||||
"state_interval": "Intervalle d'état",
|
||||
@@ -922,6 +1060,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Journaux principaux",
|
||||
"configuration": "Configuration",
|
||||
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
||||
"endpoint": "Point final",
|
||||
"hostname": "nom d'hôte",
|
||||
@@ -932,6 +1071,10 @@
|
||||
"os": "Système opérateur",
|
||||
"processors": "Processeurs",
|
||||
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
|
||||
"secrets": "Secrets",
|
||||
"secrets_create": "Créer un secret",
|
||||
"secrets_one": "Secret",
|
||||
"services": "Prestations de service",
|
||||
"start": "Début",
|
||||
"subsystems": "Sous-systèmes",
|
||||
"success_reload": "Commande de rechargement envoyée avec succès !",
|
||||
@@ -943,19 +1086,31 @@
|
||||
"version": "Version"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Les colonnes",
|
||||
"columns_hidden_one": "{{count}} Colonne masquée",
|
||||
"columns_hidden_other": "{{count}} colonnes masquées",
|
||||
"display_column": "Afficher",
|
||||
"drag_always_show": "Vous ne pouvez pas masquer cette colonne, mais vous pouvez modifier sa position",
|
||||
"drag_explanation": "Glisser-déposer pour réorganiser et modifier la visibilité des colonnes",
|
||||
"drag_locked": "Cette colonne est verrouillée dans sa position",
|
||||
"export_current_page": "Page actuelle uniquement",
|
||||
"first_page": "Première page",
|
||||
"go_to_page": "Aller à la page",
|
||||
"hide_column": "Cacher",
|
||||
"last_page": "Dernière page",
|
||||
"next_page": "Page suivante",
|
||||
"page": "Page",
|
||||
"previous_page": "Page précédente"
|
||||
"preferences": "Préférences de tableau",
|
||||
"previous_page": "Page précédente",
|
||||
"reset": "Remettre à zéro les préférences",
|
||||
"settings": "Réglages"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "Mail non valide",
|
||||
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
||||
"password": "Mot de passe",
|
||||
"role": "Rôle",
|
||||
"suspended": "Suspendu",
|
||||
"title": "Utilisateur"
|
||||
},
|
||||
"users": {
|
||||
@@ -1000,9 +1155,11 @@
|
||||
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
||||
"title": "Les lieux",
|
||||
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
||||
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
|
||||
"update_success": "Lieu mis à jour !",
|
||||
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
|
||||
"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 !",
|
||||
"upgrade_options_available": "Voici toutes les révisions disponibles, veuillez sélectionner celle vers laquelle vous souhaitez que TOUS les appareils de ce lieu soient mis à niveau",
|
||||
"use_existing": "Utiliser l'existant",
|
||||
"use_existing_contacts": "Utiliser les contacts existants",
|
||||
"use_this_contact": "Utilisez ce contact"
|
||||
|
||||
@@ -79,7 +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. 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",
|
||||
@@ -90,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",
|
||||
@@ -174,6 +180,7 @@
|
||||
"other": "comandos",
|
||||
"override_dfs": "Substituir DFS",
|
||||
"reboot": "Reiniciar",
|
||||
"reboot_description": "Deseja reiniciar este dispositivo?",
|
||||
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
|
||||
"reboot_success": "Comando de reinicialização enviado com sucesso!",
|
||||
"revision": "revisão",
|
||||
@@ -234,6 +241,7 @@
|
||||
"error_download": "Erro ao tentar fazer o download: {{e}}",
|
||||
"errors": "Erros",
|
||||
"exit_fullscreen": "Saída",
|
||||
"export": "Exportar",
|
||||
"finished": "acabado",
|
||||
"fullscreen": "Tela cheia",
|
||||
"general_error": "Erro ao se conectar ao servidor. Consulte seu administrador.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"identification": "Identificação",
|
||||
"inherit": "Herdar",
|
||||
"language": "Língua",
|
||||
"last_use": "Usado por último",
|
||||
"lifetime": "Tempo de vida",
|
||||
"locale": "Localidade",
|
||||
"logout": "Sair",
|
||||
@@ -261,6 +270,7 @@
|
||||
"model": "Modelo",
|
||||
"modified": "Modificado",
|
||||
"monthly": "Por mês",
|
||||
"months": "Meses",
|
||||
"my_account": "Minha conta",
|
||||
"name": "Nome",
|
||||
"name_error": "O nome deve ter menos de 50 caracteres",
|
||||
@@ -314,6 +324,7 @@
|
||||
"use_file": "Usar arquivo",
|
||||
"value": "Valor",
|
||||
"variable": "Variável",
|
||||
"view": "Visão",
|
||||
"view_details": "VER DETALHES",
|
||||
"view_in_gateway": "Ver no controlador",
|
||||
"view_json": "Ver JSON",
|
||||
@@ -346,6 +357,7 @@
|
||||
"error_pushes_one": "Erro (pode ser devido à configuração incorreta): {{count}}",
|
||||
"error_pushes_other": "Erros (podem ser devido à configuração incorreta): {{count}}",
|
||||
"expert_name": "MODO EXPERT",
|
||||
"expert_name_explanation": "Você pode usar o modo especialista para modificar diretamente sua configuração, incluindo a adição de valores que não são atualmente suportados pela interface do usuário. Use este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explicação",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Atualização de firmware",
|
||||
@@ -367,7 +379,7 @@
|
||||
"push_configuration": "Configuração de envio",
|
||||
"push_configuration_error": "Erro ao tentar enviar a configuração para o dispositivo: {{e}}",
|
||||
"push_configuration_explanation": "Configuração não enviada, código de erro {{code}}",
|
||||
"push_success": "Configuração enviada com sucesso!",
|
||||
"push_success": "A configuração foi verificada e um comando \"Configure\" foi iniciado pelo controlador!",
|
||||
"radio_limit": "Você atingiu a quantidade máxima de rádios (5). Você precisa excluir uma das bandas ativadas para adicionar uma nova",
|
||||
"radios": "Rádios",
|
||||
"rc_only": "Liberar apenas candidatos",
|
||||
@@ -387,6 +399,7 @@
|
||||
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||
"weight": "Peso",
|
||||
"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi",
|
||||
"wifi_frames": "Quadros WiFi"
|
||||
},
|
||||
"contacts": {
|
||||
@@ -511,6 +524,7 @@
|
||||
"ouis_explanation": "OUIs de dispositivos que se conectaram a este servidor de firmware",
|
||||
"outdated_one": "Firmware com {{count}} dias",
|
||||
"outdated_other": "Firmware com {{count}} dias",
|
||||
"outdated_unknown": "Firmware de idade desconhecida",
|
||||
"release": "LANÇAMENTO",
|
||||
"show_dev_releases": "Lançamentos do desenvolvedor",
|
||||
"status_explanation": "Status da conexão dos dispositivos que se conectaram a este servidor de firmware",
|
||||
@@ -526,6 +540,14 @@
|
||||
"queue": {
|
||||
"title": "Fila de Eventos"
|
||||
},
|
||||
"radius": {
|
||||
"calling_station_id": "estação",
|
||||
"input_octets": "Entrada",
|
||||
"output_octets": "Saída",
|
||||
"radius_clients": "Clientes Radius",
|
||||
"session_time": "Tempo de sessão",
|
||||
"username": "Nome de usuário"
|
||||
},
|
||||
"stats": {
|
||||
"load": "Carga (1 | 5 | 15 m.)",
|
||||
"seconds_ago": "{{s}} segundos atrás",
|
||||
@@ -596,6 +618,7 @@
|
||||
"certificate_expires_in": "Certificado expira em",
|
||||
"certificate_expiry": "Certificado expira em",
|
||||
"connected": "Conectado",
|
||||
"crash_logs": "Registros de falhas",
|
||||
"create_errors": "erros ao tentar criar dispositivos",
|
||||
"create_success": " dispositivos criados com sucesso",
|
||||
"current_firmware": "Firmware atual",
|
||||
@@ -609,13 +632,17 @@
|
||||
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
||||
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
||||
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
||||
"logs_one": "Registro",
|
||||
"new_devices": "novos dispositivos",
|
||||
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
||||
"not_connected": "Não conectado",
|
||||
"not_found_gateway": "Erro: o dispositivo ainda não se conectou ao gateway",
|
||||
"notifications": "Notificações do dispositivo",
|
||||
"one": "Dispositivo",
|
||||
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
||||
"restricted": "Restrito",
|
||||
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
|
||||
"restrictions_overriden_title": "Modo de desenvolvedor",
|
||||
"sanity": "Sanidade",
|
||||
"start_import": "Iniciar importação de dispositivos",
|
||||
"test_batch": "Dados de importação de teste",
|
||||
@@ -666,7 +693,15 @@
|
||||
"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"
|
||||
},
|
||||
"firmware": {
|
||||
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
|
||||
"last_db_update_modal": "banco de dados de firmware",
|
||||
"last_db_update_title": "base de dados",
|
||||
"start_db_update": "Iniciar atualização do banco de dados",
|
||||
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
|
||||
},
|
||||
"footer": {
|
||||
"powered_by": "Distribuído por",
|
||||
@@ -688,7 +723,7 @@
|
||||
"invalid_ipv6": "Endereço IPv6 inválido (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Sequência JSON inválida",
|
||||
"invalid_lease_time": "Valor de tempo de locação inválido! Eles precisam estar no formato digitUnit. Por exemplo: 6d2h5m, que significa 6 dias, 2 horas e 5 minutos. Aqui estão as unidades aceitas: m, h, d. Se você não quiser usar uma unidade, omita-a completamente. Então, em vez de dizer 0d2h0m, use 2h",
|
||||
"invalid_mac_uc": "Valor UC-MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valor MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Senha inválida, consulte a política de senha",
|
||||
"invalid_phone_number": "Número de telefone inválido",
|
||||
"invalid_phone_numbers": "Um ou mais números de telefone são inválidos. Forneça-os sem símbolos e espaços ou neste formato: +1(123)123-1234",
|
||||
@@ -697,6 +732,8 @@
|
||||
"invalid_proto_6g": "Este protocolo de criptografia não pode ser usado em um SSID que usa 6G",
|
||||
"invalid_proto_passpoint": "Este protocolo de criptografia não pode ser usado com um SSID de ponto de acesso. Por favor, selecione um protocolo que pode usar Radius",
|
||||
"invalid_select_ports": "Valores incompatíveis entre interfaces! Certifique-se de que não há combinação duplicada de PORT/VLAN ID entre suas interfaces",
|
||||
"invalid_static_ipv4_d": "Endereço inválido, este intervalo está reservado para multicasting (classe D). O primeiro octeto deve ser 223 ou inferior",
|
||||
"invalid_static_ipv4_e": "Endereço inválido, este intervalo é reservado para experimentos (classe E). O primeiro octeto deve ser 223 ou inferior",
|
||||
"invalid_third_party": "String JSON de terceiros inválida. Confirme se seu valor é um JSON válido",
|
||||
"key_file_explanation": "Use um arquivo .pem que comece com \"-----BEGIN PRIVATE KEY-----\" e termine com \"-----END PRIVATE KEY-----\"",
|
||||
"min_max_string": "O valor precisa ter um comprimento entre {{min}} (inclusive) e {{max}} (inclusive)",
|
||||
@@ -742,6 +779,15 @@
|
||||
"successful_macs": "MACs de sucesso",
|
||||
"title": "Empregos"
|
||||
},
|
||||
"keys": {
|
||||
"description_error": "A descrição precisa ter menos de 64 caracteres",
|
||||
"expire_error": "A expiração não pode ser superior a um ano no futuro",
|
||||
"expires": "expira",
|
||||
"max_keys": "Teclas máximas alcançadas (10)",
|
||||
"name_error": "O nome deve ser único e ter entre 6 e 20 caracteres alfanuméricos",
|
||||
"one": "Chave API",
|
||||
"other": "Chaves de Api"
|
||||
},
|
||||
"locations": {
|
||||
"address_line_one": "Linha de endereço um",
|
||||
"address_line_two": "Linha de endereço dois",
|
||||
@@ -749,13 +795,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",
|
||||
@@ -781,6 +831,7 @@
|
||||
"reset_password": "Redefinir senha",
|
||||
"sign_in": "assinar em",
|
||||
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
||||
"suspended_error": "Conta suspensa, entre em contato com seu administrador",
|
||||
"verification": "Verifique seu login",
|
||||
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
||||
"welcome_back": "Bem vindo de volta!",
|
||||
@@ -788,6 +839,25 @@
|
||||
"your_new_password": "Sua nova 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": "Filtro de notificações",
|
||||
"security": "SEGURANÇA",
|
||||
"source": "Fonte",
|
||||
"thread": "FIO",
|
||||
"venue_config": "Configuração",
|
||||
"venue_reboot": "Reiniciar",
|
||||
"venue_upgrade": "Melhorar"
|
||||
},
|
||||
"map": {
|
||||
"auto_align": "Alinhamento Automático",
|
||||
"auto_map": "Mapa automático",
|
||||
@@ -804,6 +874,10 @@
|
||||
"title": "Mapa",
|
||||
"visibility": "visibilidade"
|
||||
},
|
||||
"notification": {
|
||||
"one": "Notificação",
|
||||
"other": "Notificações"
|
||||
},
|
||||
"operator": {
|
||||
"delete_explanation": "Tem certeza de que deseja excluir este operador? Esta operação não é reversível",
|
||||
"delete_operator": "Excluir operador",
|
||||
@@ -818,11 +892,31 @@
|
||||
"my_organization": "Minha organização",
|
||||
"title": "Organização"
|
||||
},
|
||||
"overrides": {
|
||||
"delete_source": "Excluir todas as substituições de {{source}}",
|
||||
"ignore_overrides": "Ignorar substituições de configuração",
|
||||
"name_error": "O parâmetro já está definido pela sua fonte",
|
||||
"one": "Substituição de configuração",
|
||||
"other": "Substituições de configuração",
|
||||
"param_name": "parâmetro",
|
||||
"param_value": "Valor",
|
||||
"parameter": "parâmetro",
|
||||
"reason": "RAZÃO",
|
||||
"reason_error": "Seu motivo precisa ter menos de 64 caracteres. grandes",
|
||||
"source": "Fonte",
|
||||
"tx_power_error": "A potência Tx precisa estar entre 1 e 32",
|
||||
"update_success": "Substituições de configuração atualizadas!",
|
||||
"value": "Valor"
|
||||
},
|
||||
"profile": {
|
||||
"about_me": "Sobre mim",
|
||||
"activate": "",
|
||||
"add_new_note": "Adicionar nota",
|
||||
"deactivate": "Desativar",
|
||||
"delete_account": "Excluir meu perfil",
|
||||
"delete_account_confirm": "Excluir todas as minhas informações",
|
||||
"delete_warning": "Esta ação é irreversível. Todas as suas informações de perfil e suas chaves de API serão removidas",
|
||||
"deleted_success": "Seu perfil agora foi excluído, agora vamos desconectar você...",
|
||||
"disabled": "Desativado",
|
||||
"enabled": "ativado",
|
||||
"manage_avatar": "Gerenciar Avatar",
|
||||
@@ -835,6 +929,20 @@
|
||||
"title": "Recursos",
|
||||
"variable": "Variável"
|
||||
},
|
||||
"restrictions": {
|
||||
"algo": "Algoritmo de Assinatura",
|
||||
"allowed": "Permitido",
|
||||
"countries": "países permitidos",
|
||||
"developer": "Modo de desenvolvedor",
|
||||
"dfs": "Substituição DFS",
|
||||
"gw_commands": "Comandos de gateway",
|
||||
"identifier": "Identificador",
|
||||
"key_verification": "Informações Chave de Assinatura",
|
||||
"restricted": "Restrito",
|
||||
"signed_upgrade": "Somente atualização assinada",
|
||||
"title": "RESTRIÇÕES",
|
||||
"tty": "Acesso TTY"
|
||||
},
|
||||
"rrm": {
|
||||
"algorithm": "Algoritmo",
|
||||
"algorithm_other": "Algoritmos",
|
||||
@@ -849,6 +957,33 @@
|
||||
"vendor": "fornecedor",
|
||||
"version": "Versão"
|
||||
},
|
||||
"script": {
|
||||
"author": "O Criador",
|
||||
"automatic": "Automático",
|
||||
"create_success": "O script agora está criado e pronto para uso!",
|
||||
"custom_domain": "Domínio personalizado",
|
||||
"deferred": "Diferido",
|
||||
"device_title": "Executar script",
|
||||
"diagnostics": "Diagnóstico",
|
||||
"explanation": "Execute um script personalizado neste dispositivo e baixe seus resultados",
|
||||
"file_not_ready": "O resultado ainda não foi carregado, volte mais tarde",
|
||||
"file_too_large": "Selecione um arquivo com menos de 500 KB",
|
||||
"helper": "Documentação",
|
||||
"no_script_available": "Nenhum script disponível para sua função de usuário",
|
||||
"now": "agora",
|
||||
"one": "Roteiro",
|
||||
"other": "Scripts",
|
||||
"restricted": "Usuários autorizados a executar este script",
|
||||
"schedule_success": "Execução de script agendada!",
|
||||
"signature": "Assinatura",
|
||||
"started_execution": "Execução do script iniciada, venha mais tarde para os resultados!",
|
||||
"timeout": "Tempo esgotado",
|
||||
"update_success": "Roteiro atualizado!",
|
||||
"upload_destination": "Destino de upload de resultados",
|
||||
"upload_file": "Subir arquivo",
|
||||
"visit_external_website": "VER DOCUMENTAÇÃO",
|
||||
"when": "Agendar Execução"
|
||||
},
|
||||
"service": {
|
||||
"billing_code": "código de cobrança",
|
||||
"billing_frequency": "Freqüência de cobrança",
|
||||
@@ -866,6 +1001,8 @@
|
||||
"concurrent_devices": "Dispositivos Simultâneos",
|
||||
"controller": "Controlador",
|
||||
"current_live_devices": "Dispositivos ativos atuais",
|
||||
"currently_running_one": "Atualmente, há {{count}} simulação em execução",
|
||||
"currently_running_other": "Existem atualmente {{count}} simulações em execução",
|
||||
"delete_success": "Simulação excluída!",
|
||||
"duration": "Duração",
|
||||
"error_devices": "Dispositivos de Erro",
|
||||
@@ -889,6 +1026,7 @@
|
||||
"rx_messages": "Mensagens Rx",
|
||||
"sim_currently_running": "Simulação \"{{sim}}\" em andamento",
|
||||
"sim_history": "{{sim}} execuções anteriores",
|
||||
"simulated": "Simulado",
|
||||
"start": "Iniciar simulação",
|
||||
"start_success": "Corrida de simulação iniciada!",
|
||||
"state_interval": "Intervalo de estado",
|
||||
@@ -922,6 +1060,7 @@
|
||||
},
|
||||
"system": {
|
||||
"backend_logs": "Registros de back-end",
|
||||
"configuration": "Configuração",
|
||||
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
||||
"endpoint": "Ponto final",
|
||||
"hostname": "Nome de anfitrião",
|
||||
@@ -932,6 +1071,10 @@
|
||||
"os": "Sistema Operacional",
|
||||
"processors": "Processadores",
|
||||
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
|
||||
"secrets": "Segredos",
|
||||
"secrets_create": "Criar Segredo",
|
||||
"secrets_one": "Segredo",
|
||||
"services": "Serviços",
|
||||
"start": "Começar",
|
||||
"subsystems": "Subsistemas",
|
||||
"success_reload": "Comando de recarga enviado com sucesso!",
|
||||
@@ -943,19 +1086,31 @@
|
||||
"version": "Versão"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Colunas",
|
||||
"columns_hidden_one": "{{count}} Coluna oculta",
|
||||
"columns_hidden_other": "{{count}} Colunas ocultas",
|
||||
"display_column": "Exibição",
|
||||
"drag_always_show": "Você não pode ocultar esta coluna, mas pode alterar sua posição",
|
||||
"drag_explanation": "Arraste e solte para reordenar e alterar a visibilidade da coluna",
|
||||
"drag_locked": "Esta coluna está travada em sua posição",
|
||||
"export_current_page": "Somente página atual",
|
||||
"first_page": "Primeira Página",
|
||||
"go_to_page": "Vá para página",
|
||||
"hide_column": "Ocultar",
|
||||
"last_page": "Última Página",
|
||||
"next_page": "Próxima página",
|
||||
"page": "Página",
|
||||
"previous_page": "Página anterior"
|
||||
"preferences": "Preferências de Tabela",
|
||||
"previous_page": "Página anterior",
|
||||
"reset": "Reiniciar preferências",
|
||||
"settings": "Definições"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "e-mail não validado",
|
||||
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
||||
"password": "Senha",
|
||||
"role": "Função",
|
||||
"suspended": "Suspenso",
|
||||
"title": "Do utilizador"
|
||||
},
|
||||
"users": {
|
||||
@@ -1000,9 +1155,11 @@
|
||||
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
||||
"title": "Locais",
|
||||
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
||||
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
|
||||
"update_success": "Local atualizado!",
|
||||
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
|
||||
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
|
||||
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
|
||||
"upgrade_options_available": "Aqui estão todas as revisões disponíveis, selecione aquela para a qual você deseja que TODOS os dispositivos deste local sejam atualizados",
|
||||
"use_existing": "Usar existente",
|
||||
"use_existing_contacts": "Usar contatos existentes",
|
||||
"use_this_contact": "Use este contato"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, SpaceProps } from '@chakra-ui/react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface CloseButtonProps extends SpaceProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, SpaceProps } from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface CreateButtonProps extends SpaceProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface DeleteButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
|
||||
export interface EditButtonProps {
|
||||
onClick: () => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { ArrowsClockwise } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface RefreshButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { FloppyDisk } from 'phosphor-react';
|
||||
import { FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface SaveButtonProps
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { ArrowRight, FloppyDisk } from 'phosphor-react';
|
||||
import { ArrowRight, FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface StepButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pencil, X } from 'phosphor-react';
|
||||
import { Pencil, X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
|
||||
import { FunnelSimple } from 'phosphor-react';
|
||||
import { FunnelSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
isSorted: boolean;
|
||||
|
||||
15
src/components/DatePickers/DatePickerInput/index.tsx
Normal file
15
src/components/DatePickers/DatePickerInput/index.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
onClick?: () => void;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
const DatePickerInput = forwardRef(({ value, onClick, isDisabled }: Props, ref: React.Ref<HTMLButtonElement>) => (
|
||||
<Button colorScheme="gray" onClick={onClick} ref={ref} isDisabled={isDisabled}>
|
||||
{value}
|
||||
</Button>
|
||||
));
|
||||
|
||||
export default DatePickerInput;
|
||||
@@ -42,7 +42,7 @@ const DurationField = ({ name, label, isRequired, isDisabled, unit }: Props) =>
|
||||
{label}
|
||||
</FormLabel>
|
||||
<Flex h="40px">
|
||||
<RadioGroup onChange={onRadioChange} defaultValue={value === 0 ? '0' : '1'}>
|
||||
<RadioGroup onChange={onRadioChange} defaultValue={value === 0 ? '0' : '1'} my="auto">
|
||||
<Stack spacing={5} direction="row">
|
||||
<Radio colorScheme="blue" value="0">
|
||||
{t('simulation.infinite')}
|
||||
@@ -52,20 +52,29 @@ const DurationField = ({ name, label, isRequired, isDisabled, unit }: Props) =>
|
||||
<Text my="auto" mr={2}>
|
||||
{t('common.custom')}
|
||||
</Text>
|
||||
<InputGroup>
|
||||
<NumberInput isDisabled={value === 0} value={value} onChange={(_, v) => onChange(v)} w="120px">
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
<InputRightAddon>{unit}</InputRightAddon>
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
<InputGroup>
|
||||
<NumberInput
|
||||
isDisabled={value === 0 || isDisabled}
|
||||
min={1}
|
||||
value={value}
|
||||
onChange={(_, v) => {
|
||||
if (Number.isNaN(v) || v === 0) onChange(1);
|
||||
else onChange(v);
|
||||
}}
|
||||
w="120px"
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
<InputRightAddon>{unit}</InputRightAddon>
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
<FormErrorMessage>{error}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DataTable } from '../../../DataTables/DataTable';
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import { Question } from 'phosphor-react';
|
||||
import { Question } from '@phosphor-icons/react';
|
||||
|
||||
export type InfoPopoverProps = {
|
||||
title: string;
|
||||
|
||||
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;
|
||||
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;
|
||||
};
|
||||
@@ -1,19 +1,26 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useSimulatorStore } from './useStore';
|
||||
import { WebSocketInitialMessage, WebSocketNotification } from './utils';
|
||||
import { SimulationWebSocketRawMessage, WebSocketNotification } from './utils';
|
||||
import { axiosOwls, axiosSec } from 'constants/axiosInstances';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { SimulationStatus } from 'hooks/Network/Simulations';
|
||||
|
||||
const extractWebSocketNotification = (message?: WebSocketInitialMessage): WebSocketNotification | undefined => {
|
||||
const extractWebSocketNotification = (message?: SimulationWebSocketRawMessage): WebSocketNotification | undefined => {
|
||||
if (message && message.notification) {
|
||||
if (message.notification.type === 'owls_simulation_update') {
|
||||
if (message.notification.type_id === 1000) {
|
||||
return {
|
||||
type: 'SIMULATION_STATUS',
|
||||
content: message.notification.content,
|
||||
content: message.notification.content as SimulationStatus,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (message?.notificationTypes) {
|
||||
return {
|
||||
type: 'INITIAL_MESSAGE',
|
||||
message,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -38,7 +45,7 @@ export const SimulatorSocketProvider = ({ children }: { children: React.ReactEle
|
||||
|
||||
const onMessage = useCallback((message: MessageEvent<string>) => {
|
||||
try {
|
||||
const data = JSON.parse(message.data) as WebSocketInitialMessage | undefined;
|
||||
const data = JSON.parse(message.data) as SimulationWebSocketRawMessage | undefined;
|
||||
const extracted = extractWebSocketNotification(data);
|
||||
if (extracted) {
|
||||
addMessage(extracted);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import create from 'zustand';
|
||||
import { SocketEventCallback, WebSocketNotification } from './utils';
|
||||
import { axiosOwls } from 'constants/axiosInstances';
|
||||
import { SimulationStatus } from 'hooks/Network/Simulations';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
|
||||
export type WebSocketMessage =
|
||||
| {
|
||||
@@ -17,15 +19,14 @@ export type SimulationOperationStatus = {
|
||||
msgsTx: number;
|
||||
timestamp: Date;
|
||||
operationId: string;
|
||||
rawData: {
|
||||
rx: number;
|
||||
tx: number;
|
||||
msgsRx: number;
|
||||
msgsTx: number;
|
||||
};
|
||||
simulationId: string;
|
||||
rawData: SimulationStatus;
|
||||
};
|
||||
|
||||
export type SimulatorStoreState = {
|
||||
availableLogTypes: NotificationType[];
|
||||
hiddenLogIds: number[];
|
||||
setHiddenLogIds: (logsToHide: number[]) => void;
|
||||
lastMessage?: WebSocketMessage;
|
||||
allMessages: WebSocketMessage[];
|
||||
addMessage: (message: WebSocketNotification) => void;
|
||||
@@ -36,10 +37,18 @@ export type SimulatorStoreState = {
|
||||
startWebSocket: (token: string, tries?: number) => void;
|
||||
isWebSocketOpen: boolean;
|
||||
setWebSocketOpen: (isOpen: boolean) => void;
|
||||
currentSimulationData: SimulationOperationStatus[];
|
||||
currentSimulationsData: Record<string, SimulationOperationStatus[]>;
|
||||
};
|
||||
|
||||
export const useSimulatorStore = create<SimulatorStoreState>((set, get) => ({
|
||||
availableLogTypes: [],
|
||||
hiddenLogIds: [],
|
||||
setHiddenLogIds: (logsToHide: number[]) => {
|
||||
get().send(JSON.stringify({ 'drop-notifications': logsToHide }));
|
||||
set(() => ({
|
||||
hiddenLogIds: logsToHide,
|
||||
}));
|
||||
},
|
||||
allMessages: [] as WebSocketMessage[],
|
||||
addMessage: (msg: WebSocketNotification) => {
|
||||
const obj: WebSocketMessage = {
|
||||
@@ -47,52 +56,72 @@ export const useSimulatorStore = create<SimulatorStoreState>((set, get) => ({
|
||||
data: msg,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
const prevContent = get().currentSimulationData;
|
||||
const newSimStatusMsg: SimulationOperationStatus = {
|
||||
rx: 0,
|
||||
tx: 0,
|
||||
msgsRx: 0,
|
||||
msgsTx: 0,
|
||||
timestamp: obj.timestamp,
|
||||
operationId: msg.content.id,
|
||||
rawData: {
|
||||
rx: msg.content.rx,
|
||||
tx: msg.content.tx,
|
||||
msgsRx: msg.content.msgsRx,
|
||||
msgsTx: msg.content.msgsTx,
|
||||
},
|
||||
};
|
||||
const prevEntry = prevContent[Math.max(0, prevContent.length - 1)];
|
||||
if (prevEntry?.operationId === newSimStatusMsg.operationId) {
|
||||
newSimStatusMsg.rx = Math.max(0, newSimStatusMsg.rawData.rx - prevEntry.rawData.rx);
|
||||
newSimStatusMsg.tx = Math.max(0, newSimStatusMsg.rawData.tx - prevEntry.rawData.tx);
|
||||
newSimStatusMsg.msgsRx = Math.max(0, newSimStatusMsg.rawData.msgsRx - prevEntry.rawData.msgsRx);
|
||||
newSimStatusMsg.msgsTx = Math.max(0, newSimStatusMsg.rawData.msgsTx - prevEntry.rawData.msgsTx);
|
||||
}
|
||||
const newCurrSimStatus =
|
||||
prevEntry?.operationId === newSimStatusMsg.operationId ? [...prevContent, newSimStatusMsg] : [newSimStatusMsg];
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(({ type }) => type === msg.type);
|
||||
if (msg.type === 'SIMULATION_STATUS') {
|
||||
const allStoredStatus = get().currentSimulationsData;
|
||||
const newSimStatusMsg: SimulationOperationStatus = {
|
||||
rx: 0,
|
||||
tx: 0,
|
||||
msgsRx: 0,
|
||||
msgsTx: 0,
|
||||
timestamp: obj.timestamp,
|
||||
operationId: msg.content.id,
|
||||
simulationId: msg.content.simulationId,
|
||||
rawData: msg.content,
|
||||
};
|
||||
|
||||
if (eventsToFire.length > 0) {
|
||||
for (const event of eventsToFire) {
|
||||
event.callback();
|
||||
const key = newSimStatusMsg.operationId;
|
||||
if (!allStoredStatus[key]) allStoredStatus[key] = [];
|
||||
|
||||
const prevContent = allStoredStatus[key] as SimulationOperationStatus[];
|
||||
const prevEntry = prevContent[Math.max(0, prevContent.length - 1)];
|
||||
if (prevEntry?.operationId === newSimStatusMsg.operationId) {
|
||||
newSimStatusMsg.rx = Math.max(0, newSimStatusMsg.rawData.rx - prevEntry.rawData.rx);
|
||||
newSimStatusMsg.tx = Math.max(0, newSimStatusMsg.rawData.tx - prevEntry.rawData.tx);
|
||||
newSimStatusMsg.msgsRx = Math.max(0, newSimStatusMsg.rawData.msgsRx - prevEntry.rawData.msgsRx);
|
||||
newSimStatusMsg.msgsTx = Math.max(0, newSimStatusMsg.rawData.msgsTx - prevEntry.rawData.msgsTx);
|
||||
}
|
||||
const newCurrSimStatus =
|
||||
prevEntry?.operationId === newSimStatusMsg.operationId ? [...prevContent, newSimStatusMsg] : [newSimStatusMsg];
|
||||
const newCurrentSimulationData = allStoredStatus;
|
||||
newCurrentSimulationData[key] = newCurrSimStatus.length <= 60 * 10 ? newCurrSimStatus : newCurrSimStatus.slice(1);
|
||||
|
||||
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),
|
||||
),
|
||||
currentSimulationsData: newCurrentSimulationData,
|
||||
}));
|
||||
}
|
||||
|
||||
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)),
|
||||
currentSimulationData: newCurrSimStatus.length <= 60 * 10 ? newCurrSimStatus : newCurrSimStatus.slice(1),
|
||||
currentSimulationsData: newCurrentSimulationData,
|
||||
}));
|
||||
}
|
||||
|
||||
if (msg.type === 'INITIAL_MESSAGE') {
|
||||
if (msg.message.notificationTypes) {
|
||||
set({ availableLogTypes: msg.message.notificationTypes });
|
||||
}
|
||||
}
|
||||
|
||||
return set((state) => ({
|
||||
allMessages:
|
||||
state.allMessages.length <= 1000 ? [...state.allMessages, obj] : [...state.allMessages.slice(1), obj],
|
||||
lastMessage: obj,
|
||||
currentSimulationData: newCurrSimStatus.length <= 60 * 10 ? newCurrSimStatus : newCurrSimStatus.slice(1),
|
||||
}));
|
||||
},
|
||||
eventListeners: [] as SocketEventCallback[],
|
||||
@@ -127,5 +156,5 @@ export const useSimulatorStore = create<SimulatorStoreState>((set, get) => ({
|
||||
}
|
||||
}
|
||||
},
|
||||
currentSimulationData: [] as SimulationOperationStatus[],
|
||||
currentSimulationsData: {},
|
||||
}));
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
import { SimulationStatus } from 'hooks/Network/Simulations';
|
||||
import { InitialSocketMessage } from 'models/Socket';
|
||||
|
||||
export type LogLevel = 'information' | 'critical' | 'debug' | 'error' | 'fatal' | 'notice' | 'trace' | 'warning';
|
||||
|
||||
type SimulationUpdateMessage = {
|
||||
notification: {
|
||||
notificationId: number;
|
||||
type: 'owls_simulation_update';
|
||||
notification_id: number;
|
||||
type_id: 1000;
|
||||
content: SimulationStatus;
|
||||
};
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type WebSocketInitialMessage = SimulationUpdateMessage;
|
||||
|
||||
export type WebSocketNotification = {
|
||||
type: 'SIMULATION_STATUS';
|
||||
content: SimulationStatus;
|
||||
type LogMessage = {
|
||||
notification: {
|
||||
notificationId: number;
|
||||
type_id: number;
|
||||
content: {
|
||||
level: LogLevel;
|
||||
msg: string;
|
||||
source: string;
|
||||
thread_id: number;
|
||||
thread_name: string;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
notificationTypes?: undefined;
|
||||
};
|
||||
|
||||
export type SimulationWebSocketRawMessage = Partial<SimulationUpdateMessage> | InitialSocketMessage | LogMessage;
|
||||
|
||||
export type WebSocketNotification =
|
||||
| {
|
||||
type: 'SIMULATION_STATUS';
|
||||
content: SimulationStatus;
|
||||
}
|
||||
| {
|
||||
type: 'LOG';
|
||||
serialNumber?: undefined;
|
||||
serialNumbers?: undefined;
|
||||
notificationTypes?: undefined;
|
||||
log: LogMessage['notification']['content'];
|
||||
}
|
||||
| {
|
||||
type: 'INITIAL_MESSAGE';
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'SIMULATION_STATUS';
|
||||
|
||||
83
src/hooks/Network/Secrets.ts
Normal file
83
src/hooks/Network/Secrets.ts
Normal 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']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { axiosOwls } from 'constants/axiosInstances';
|
||||
import { AtLeast } from 'models/General';
|
||||
|
||||
@@ -25,16 +25,16 @@ export type Simulation = {
|
||||
};
|
||||
|
||||
const getSimulations = () => async () =>
|
||||
axiosOwls.get(`simulation`).then((response) => response.data as { list: Simulation[] });
|
||||
axiosOwls.get(`simulation/*`).then((response) => response.data as { list: Simulation[] });
|
||||
|
||||
export const useGetSimulations = () =>
|
||||
useQuery(['simulations', 'all'], getSimulations(), {
|
||||
useQuery(['simulations'], getSimulations(), {
|
||||
keepPreviousData: true,
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
const getSimulation = (id?: string) => async () =>
|
||||
axiosOwls.get(`simulation?id=${id}`).then((response) => response.data as { list: Simulation[] });
|
||||
axiosOwls.get(`simulation/${id}`).then((response) => response.data as { list: Simulation[] });
|
||||
export const useGetSimulation = ({ id }: { id?: string }) =>
|
||||
useQuery(['simulation', id], getSimulation(id), {
|
||||
keepPreviousData: true,
|
||||
@@ -42,7 +42,7 @@ export const useGetSimulation = ({ id }: { id?: string }) =>
|
||||
staleTime: 30000,
|
||||
});
|
||||
|
||||
const createSimulation = async (newSimulation: Partial<Simulation>) => axiosOwls.post(`simulation`, newSimulation);
|
||||
const createSimulation = async (newSimulation: Partial<Simulation>) => axiosOwls.post(`simulation/0`, newSimulation);
|
||||
export const useCreateSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -54,19 +54,19 @@ export const useCreateSimulation = () => {
|
||||
};
|
||||
|
||||
const updateSimulation = async (newSimulation: AtLeast<Simulation, 'id'>) =>
|
||||
axiosOwls.put(`simulation?id=${newSimulation.id}`, newSimulation);
|
||||
axiosOwls.put(`simulation/${newSimulation.id}`, newSimulation).then((response) => response.data as Simulation);
|
||||
export const useUpdateSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(updateSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulation']);
|
||||
onSuccess: (newSimulation) => {
|
||||
queryClient.setQueryData(['simulation'], newSimulation);
|
||||
queryClient.invalidateQueries(['simulations']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation?id=${id}`);
|
||||
const deleteSimulation = async ({ id }: { id: string }) => axiosOwls.delete(`simulation/${id}`);
|
||||
export const useDeleteSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -77,34 +77,35 @@ export const useDeleteSimulation = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const startSimulation = async ({ id }: { id: string }) =>
|
||||
axiosOwls.post(`operation?simulationId=${id}&operation=start`);
|
||||
const startSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation/${id}?operation=start`);
|
||||
export const useStartSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(startSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulationStatus']);
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
const stopSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation?id=${id}&operation=stop`);
|
||||
const stopSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
|
||||
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=stop`);
|
||||
export const useStopSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(stopSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulationStatus']);
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
const cancelSimulation = async ({ id }: { id: string }) => axiosOwls.post(`operation?id=${id}&operation=cancel`);
|
||||
const cancelSimulation = async ({ runId, simulationId }: { simulationId: string; runId: string }) =>
|
||||
axiosOwls.post(`operation/${simulationId}?runningId=${runId}&operation=cancel`);
|
||||
export const useCancelSimulation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(cancelSimulation, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulationStatus']);
|
||||
queryClient.invalidateQueries(['simulations', 'status']);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -124,28 +125,37 @@ export type SimulationStatus = {
|
||||
timeToFullDevices: number;
|
||||
tx: number;
|
||||
};
|
||||
const getSimulationStatus = async () => axiosOwls.get(`status`).then((response) => response.data as SimulationStatus);
|
||||
export const useGetSimulationStatus = () =>
|
||||
useQuery(['simulationStatus'], getSimulationStatus, {
|
||||
const getSimulationsStatus = async () =>
|
||||
axiosOwls.get(`status/*`).then((response) => response.data as SimulationStatus[]);
|
||||
export const useGetSimulationsStatus = () =>
|
||||
useQuery(['simulations', 'status'], getSimulationsStatus, {
|
||||
keepPreviousData: true,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const getSimulationHistory = async () =>
|
||||
axiosOwls.get(`results`).then((response) => response.data.list as SimulationStatus[]);
|
||||
export const useGetSimulationHistory = () =>
|
||||
useQuery(['simulationStatus', 'all'], getSimulationHistory, {
|
||||
const getSimulationStatus = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
axiosOwls.get(`status/${context.queryKey[2]}`).then((response) => response.data as SimulationStatus);
|
||||
export const useGetSimulationStatus = ({ id }: { id: string }) =>
|
||||
useQuery(['simulations', 'status', id], getSimulationStatus, {
|
||||
keepPreviousData: true,
|
||||
staleTime: 30000,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results?id=${id}`);
|
||||
const getSimulationHistory = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||
axiosOwls.get(`results/${context.queryKey[2]}`).then((response) => response.data.list as SimulationStatus[]);
|
||||
export const useGetSimulationHistory = ({ id }: { id: string }) =>
|
||||
useQuery(['simulations', 'history', id], getSimulationHistory, {
|
||||
keepPreviousData: true,
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
const deleteSimulationResult = async ({ id }: { id: string }) => axiosOwls.delete(`results/${id}`);
|
||||
export const useDeleteSimulationResult = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteSimulationResult, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['simulationStatus']);
|
||||
queryClient.invalidateQueries(['simulations', 'history']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,16 +1,81 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
import { User } from 'models/User';
|
||||
import { AtLeast } from 'models/General';
|
||||
import { Note } from 'models/Note';
|
||||
|
||||
const getAvatarPromises = (userList: User[]) => {
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
| 'subscriber'
|
||||
| 'partner'
|
||||
| 'csr'
|
||||
| 'system'
|
||||
| 'installer'
|
||||
| 'noc'
|
||||
| 'accounting';
|
||||
|
||||
export type User = {
|
||||
avatar: string;
|
||||
blackListed: boolean;
|
||||
creationDate: number;
|
||||
currentLoginURI: string;
|
||||
currentPassword: string;
|
||||
description: string;
|
||||
email: string;
|
||||
id: string;
|
||||
lastEmailCheck: number;
|
||||
lastLogin: number;
|
||||
lastPasswordChange: number;
|
||||
lastPasswords: string[];
|
||||
locale: string;
|
||||
location: string;
|
||||
modified: number;
|
||||
name: string;
|
||||
notes: Note[];
|
||||
oauthType: string;
|
||||
oauthUserInfo: string;
|
||||
owner: string;
|
||||
securityPolicy: string;
|
||||
securityPolicyChange: number;
|
||||
signingUp: string;
|
||||
suspended: boolean;
|
||||
userRole: UserRole;
|
||||
userTypeProprietaryInfo: {
|
||||
authenticatorSecret: string;
|
||||
mfa: {
|
||||
enabled: boolean;
|
||||
method?: 'authenticator' | 'sms' | 'email' | '';
|
||||
};
|
||||
mobiles: { number: string }[];
|
||||
};
|
||||
validated: boolean;
|
||||
validationDate: number;
|
||||
validationEmail: string;
|
||||
validationURI: string;
|
||||
waitingForEmailCheck: boolean;
|
||||
};
|
||||
|
||||
const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
|
||||
const promises = userList.map(async (user) => {
|
||||
if (user.avatar !== '' && user.avatar !== '0') {
|
||||
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
// If the avatar is already in the cache, return it
|
||||
const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]);
|
||||
if (cachedAvatar) return cachedAvatar;
|
||||
|
||||
return axiosSec
|
||||
.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||
responseType: 'arraybuffer',
|
||||
})
|
||||
.then((response) => {
|
||||
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
|
||||
return response;
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return Promise.resolve('');
|
||||
});
|
||||
@@ -18,28 +83,57 @@ const getAvatarPromises = (userList: User[]) => {
|
||||
return promises;
|
||||
};
|
||||
|
||||
export const useGetUsers = ({ setUsersWithAvatars }: { setUsersWithAvatars: (users: unknown) => void }) => {
|
||||
const getBatchUsers = async (offset: number, limit: number) => {
|
||||
const users = await axiosSec
|
||||
.get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`)
|
||||
.then(({ data }) => data.users as User[]);
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
const getAllUsers = async () => {
|
||||
let users: User[] = [];
|
||||
let offset = 0;
|
||||
const limit = 500;
|
||||
let lastResponseLength = 0;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const response = await getBatchUsers(offset, limit);
|
||||
users = [...users, ...response];
|
||||
offset += limit;
|
||||
lastResponseLength = response.length;
|
||||
} while (lastResponseLength === limit);
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
const getUsers = async (queryClient: QueryClient) => {
|
||||
const users = await getAllUsers();
|
||||
|
||||
const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).then((results) =>
|
||||
results.map((response) => {
|
||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||
const base64 = btoa(
|
||||
// @ts-ignore
|
||||
new Uint8Array(response.value.data).reduce((respData, byte) => respData + String.fromCharCode(byte), ''),
|
||||
);
|
||||
return `data:;base64,${base64}`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
|
||||
return users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] })) as User[];
|
||||
};
|
||||
|
||||
export const useGetUsers = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useQuery(['get-users'], () => axiosSec.get('users').then(({ data }) => data.users), {
|
||||
onSuccess: async (users) => {
|
||||
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
||||
results.map((response) => {
|
||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||
const base64 = btoa(
|
||||
// @ts-ignore
|
||||
new Uint8Array(response.value.data).reduce((respData, byte) => respData + String.fromCharCode(byte), ''),
|
||||
);
|
||||
return `data:;base64,${base64}`;
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
);
|
||||
|
||||
const newUsers = users.map((newUser: User, i: number) => ({ ...newUser, avatar: avatars[i] }));
|
||||
setUsersWithAvatars(newUsers);
|
||||
},
|
||||
return useQuery(['users'], () => getUsers(queryClient), {
|
||||
staleTime: 30 * 1000,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('users-fetching-error'))
|
||||
toast({
|
||||
@@ -62,24 +156,28 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
return useQuery(['get-user', id], () => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data), {
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('user-fetching-error'))
|
||||
toast({
|
||||
id: 'user-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('users.one'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
return useQuery(
|
||||
['users', id],
|
||||
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||
{
|
||||
enabled,
|
||||
onError: (e: AxiosError) => {
|
||||
if (!toast.isActive('user-fetching-error'))
|
||||
toast({
|
||||
id: 'user-fetching-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_fetching_obj', {
|
||||
obj: t('users.one'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refresh: () => void }) => {
|
||||
@@ -114,13 +212,80 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
|
||||
},
|
||||
});
|
||||
};
|
||||
export const useSuspendUser = ({ id }: { id: string }) =>
|
||||
useMutation((isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
);
|
||||
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
|
||||
export const useSuspendUser = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
export const useResetPassword = ({ id }: { id: string }) =>
|
||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
||||
return useMutation(
|
||||
(isSuspended: boolean) =>
|
||||
axiosSec.put(`user/${id}`, {
|
||||
suspended: isSuspended,
|
||||
}),
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const useResetMfa = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetPassword = ({ id }: { id: string }) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
||||
export const useDeleteUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(deleteUser, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createUser = async (newUser: {
|
||||
name: string;
|
||||
description?: string;
|
||||
email: string;
|
||||
currentPassword: string;
|
||||
notes?: { note: string }[];
|
||||
userRole: string;
|
||||
emailValidation: boolean;
|
||||
changePassword: boolean;
|
||||
}) => axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser);
|
||||
export const useCreateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(createUser, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const modifyUser = async (newUser: AtLeast<User, 'id'>) => axiosSec.put(`user/${newUser.id}`, newUser);
|
||||
export const useUpdateUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation(modifyUser, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['users']);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ i18next
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
returnNull: false,
|
||||
// debug: process.env.NODE_ENV === "development",
|
||||
});
|
||||
export default i18next;
|
||||
|
||||
7
src/i18next.d.ts
vendored
Normal file
7
src/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'i18next';
|
||||
|
||||
declare module 'i18next' {
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Box, useStyleConfig } from '@chakra-ui/react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const PanelContainer: React.FC<Props> = ({ children }) => {
|
||||
const styles = useStyleConfig('PanelContainer');
|
||||
// Pass the computed styles into the `__css` prop
|
||||
return <Box __css={styles}>{children}</Box>;
|
||||
};
|
||||
|
||||
export default PanelContainer;
|
||||
@@ -1,14 +0,0 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Box, useStyleConfig } from '@chakra-ui/react';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const PanelContent: React.FC<Props> = ({ children }) => {
|
||||
const styles = useStyleConfig('PanelContent');
|
||||
|
||||
return <Box __css={styles}>{children}</Box>;
|
||||
};
|
||||
|
||||
export default PanelContent;
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, LayoutProps, useStyleConfig } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const MainPanel: React.FC<Props> = ({ children, ...props }) => {
|
||||
const styles = useStyleConfig('MainPanel');
|
||||
|
||||
return (
|
||||
<Box __css={styles} mb="16px" {...props}>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPanel;
|
||||
@@ -16,69 +16,51 @@ import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowCircleLeft } from 'phosphor-react';
|
||||
import { ArrowCircleLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import routes from 'router/routes';
|
||||
|
||||
interface Props {
|
||||
secondary: boolean;
|
||||
export type NavbarProps = {
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
activeRoute?: string;
|
||||
languageSwitcher?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Navbar: React.FC<Props> = ({ secondary, toggleSidebar }) => {
|
||||
export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const breakpoint = useBreakpoint();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { logout, user, avatar } = useAuth();
|
||||
const getActiveRoute = () => {
|
||||
const route = routes.find(
|
||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
||||
);
|
||||
|
||||
if (route) return route.navName ?? route.name;
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
// Style variables
|
||||
let navbarPosition: 'absolute' | 'fixed' = 'absolute';
|
||||
let navbarFilter = 'none';
|
||||
let navbarBackdrop = 'blur(21px)';
|
||||
let navbarShadow = 'none';
|
||||
let navbarBg = 'none';
|
||||
let navbarBorder = 'transparent';
|
||||
let secondaryMargin = '0px';
|
||||
|
||||
// Values if scrolled
|
||||
const scrolledNavbarShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||
const scrolledNavbarBg = useColorModeValue(
|
||||
const boxShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||
const bg = useColorModeValue(
|
||||
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.82) 0%, rgba(255, 255, 255, 0.8) 110.84%)',
|
||||
'linear-gradient(112.83deg, rgba(255, 255, 255, 0.21) 0%, rgba(255, 255, 255, 0) 110.84%)',
|
||||
);
|
||||
const scrolledNavbarBorder = useColorModeValue('#FFFFFF', 'rgba(255, 255, 255, 0.31)');
|
||||
const scrolledNavbarFilter = useColorModeValue('none', 'drop-shadow(0px 7px 23px rgba(0, 0, 0, 0.05))');
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
|
||||
if (scrolled === true) {
|
||||
navbarPosition = 'fixed';
|
||||
navbarShadow = scrolledNavbarShadow;
|
||||
navbarBg = scrolledNavbarBg;
|
||||
navbarBorder = scrolledNavbarBorder;
|
||||
navbarFilter = scrolledNavbarFilter;
|
||||
}
|
||||
|
||||
if (secondary) {
|
||||
navbarBackdrop = 'none';
|
||||
navbarPosition = 'absolute';
|
||||
secondaryMargin = '22px';
|
||||
}
|
||||
const borderColor = useColorModeValue('#FFFFFF', 'rgba(255, 255, 255, 0.31)');
|
||||
const filter = useColorModeValue('none', 'drop-shadow(0px 7px 23px rgba(0, 0, 0, 0.05))');
|
||||
const scrollDependentStyles = scrolled
|
||||
? ({
|
||||
position: 'fixed',
|
||||
boxShadow,
|
||||
bg,
|
||||
borderColor,
|
||||
filter,
|
||||
} as const)
|
||||
: ({
|
||||
position: 'absolute',
|
||||
filter: 'none',
|
||||
boxShadow: 'none',
|
||||
bg: 'none',
|
||||
borderColor: 'transparent',
|
||||
} as const);
|
||||
|
||||
const goBack = () => navigate(-1);
|
||||
|
||||
@@ -95,84 +77,78 @@ const Navbar: React.FC<Props> = ({ secondary, toggleSidebar }) => {
|
||||
window.addEventListener('scroll', changeNavbar);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
position={navbarPosition}
|
||||
boxShadow={navbarShadow}
|
||||
bg={navbarBg}
|
||||
borderColor={navbarBorder}
|
||||
filter={navbarFilter}
|
||||
backdropFilter={navbarBackdrop}
|
||||
borderWidth="1.5px"
|
||||
borderStyle="solid"
|
||||
transitionDelay="0s, 0s, 0s, 0s"
|
||||
transitionDuration=" 0.25s, 0.25s, 0.25s, 0s"
|
||||
transition-property="box-shadow, background-color, filter, border"
|
||||
transitionTimingFunction="linear, linear, linear, linear"
|
||||
alignItems="center"
|
||||
borderRadius="16px"
|
||||
display="flex"
|
||||
minH="75px"
|
||||
justifyContent="center"
|
||||
lineHeight="25.6px"
|
||||
mx="auto"
|
||||
mt={secondaryMargin}
|
||||
pb="8px"
|
||||
right={{ base: '0px', sm: '0px' }}
|
||||
pl="30px"
|
||||
ps="12px"
|
||||
pt="8px"
|
||||
top="18px"
|
||||
w={isCompact ? '100%' : 'calc(100vw - 70px - 186px)'}
|
||||
>
|
||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||
<Heading>{t(getActiveRoute())}</Heading>
|
||||
<Tooltip label={t('common.go_back')}>
|
||||
<IconButton
|
||||
mt={2}
|
||||
ml={4}
|
||||
colorScheme="blue"
|
||||
aria-label={t('common.go_back')}
|
||||
onClick={goBack}
|
||||
size="sm"
|
||||
icon={<ArrowCircleLeft width={20} height={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Box ms="auto" w={{ base: 'unset' }}>
|
||||
<Flex alignItems="center" flexDirection="row">
|
||||
<Tooltip hasArrow label={t('common.theme')}>
|
||||
<IconButton
|
||||
aria-label={t('common.theme')}
|
||||
variant="ghost"
|
||||
icon={colorMode === 'light' ? <MoonIcon h="20px" w="20px" /> : <SunIcon h="20px" w="20px" />}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</Tooltip>
|
||||
<LanguageSwitcher />
|
||||
<HStack spacing={{ base: '0', md: '6' }} ml={1} mr={4}>
|
||||
<Menu>
|
||||
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
|
||||
<HStack>
|
||||
{!isCompact && <Text fontWeight="bold">{user?.name}</Text>}
|
||||
<Avatar h="40px" w="40px" fontSize="0.8rem" lineHeight="2rem" src={avatar} name={user?.name} />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<MenuList
|
||||
bg={useColorModeValue('white', 'gray.900')}
|
||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
>
|
||||
<MenuItem onClick={goToProfile} w="100%">
|
||||
{t('account.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
<Portal>
|
||||
<Flex
|
||||
{...scrollDependentStyles}
|
||||
backdropFilter="blur(21px)"
|
||||
borderWidth="1.5px"
|
||||
borderStyle="solid"
|
||||
transitionDelay="0s, 0s, 0s, 0s"
|
||||
transitionDuration=" 0.25s, 0.25s, 0.25s, 0s"
|
||||
transition-property="box-shadow, background-color, filter, border"
|
||||
transitionTimingFunction="linear, linear, linear, linear"
|
||||
alignItems="center"
|
||||
borderRadius="15px"
|
||||
minH="75px"
|
||||
justifyContent="center"
|
||||
lineHeight="25.6px"
|
||||
pb="8px"
|
||||
right={{ base: '0px', sm: '0px', lg: '20px' }}
|
||||
ps="12px"
|
||||
pt="8px"
|
||||
top="15px"
|
||||
w={isCompact ? '100%' : 'calc(100vw - 271px)'}
|
||||
>
|
||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||
<Heading>{activeRoute}</Heading>
|
||||
<Tooltip label={t('common.go_back')}>
|
||||
<IconButton
|
||||
mt={2}
|
||||
ml={4}
|
||||
colorScheme="blue"
|
||||
aria-label={t('common.go_back')}
|
||||
onClick={goBack}
|
||||
size="sm"
|
||||
icon={<ArrowCircleLeft width={20} height={20} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Box ms="auto" w={{ base: 'unset' }}>
|
||||
<Flex alignItems="center" flexDirection="row">
|
||||
<Tooltip hasArrow label={t('common.theme')}>
|
||||
<IconButton
|
||||
aria-label={t('common.theme')}
|
||||
variant="ghost"
|
||||
icon={colorMode === 'light' ? <MoonIcon h="20px" w="20px" /> : <SunIcon h="20px" w="20px" />}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</Tooltip>
|
||||
{languageSwitcher}
|
||||
<HStack spacing={{ base: '0', md: '6' }} ml={1} mr={4}>
|
||||
<Menu>
|
||||
<MenuButton py={2} transition="all 0.3s" _focus={{ boxShadow: 'none' }}>
|
||||
<HStack>
|
||||
{!isCompact && <Text fontWeight="bold">{user?.name}</Text>}
|
||||
<Avatar h="40px" w="40px" fontSize="0.8rem" lineHeight="2rem" src={avatar} name={user?.name} />
|
||||
</HStack>
|
||||
</MenuButton>
|
||||
<Portal>
|
||||
<MenuList
|
||||
bg={useColorModeValue('white', 'gray.900')}
|
||||
borderColor={useColorModeValue('gray.200', 'gray.700')}
|
||||
>
|
||||
<MenuItem onClick={goToProfile} w="100%">
|
||||
{t('account.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={logout}>{t('common.logout')}</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
||||
48
src/layout/PageContainer/index.tsx
Normal file
48
src/layout/PageContainer/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Center, Flex, Spinner, useBreakpoint } from '@chakra-ui/react';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
export type PageContainerProps = {
|
||||
waitForUser: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const PageContainer = ({ waitForUser, children }: PageContainerProps) => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
const breakpoint = useBreakpoint('xl');
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
|
||||
return (
|
||||
<Box
|
||||
w={isCompact ? 'calc(100%)' : 'calc(100% - 210px)'}
|
||||
float="right"
|
||||
position="relative"
|
||||
transition="all 0.33s cubic-bezier(0.685, 0.0473, 0.346, 1)"
|
||||
transitionDelay=".2s, .2s, .35s"
|
||||
transitionProperty="top, bottom, width"
|
||||
transitionTimingFunction="linear, linear, ease"
|
||||
px="15px"
|
||||
pb="15px"
|
||||
>
|
||||
<Box minH="calc(100vh - 123px)" pt="105px" pl="10px" pr="5px" pb="0px">
|
||||
<Flex flexDirection="column">
|
||||
<React.Suspense
|
||||
fallback={
|
||||
<Center mt="100px">
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
}
|
||||
>
|
||||
{waitForUser && !isUserLoaded ? (
|
||||
<Center mt="100px">
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</React.Suspense>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import NavLinkButton from './NavLinkButton';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
const createLinks = (
|
||||
routes: Route[],
|
||||
activeRoute: (path: string, otherRoute: string | undefined) => string,
|
||||
role: string,
|
||||
) =>
|
||||
routes.map((route) => (
|
||||
<NavLinkButton key={uuid()} activeRoute={activeRoute} role={role} route={route} />
|
||||
));
|
||||
|
||||
export default createLinks;
|
||||
@@ -8,45 +8,47 @@ import { Route } from 'models/Routes';
|
||||
|
||||
const variantChange = '0.2s linear';
|
||||
|
||||
interface Props {
|
||||
activeRoute: (path: string, otherRoute: string | undefined) => string;
|
||||
route: Route;
|
||||
role: string;
|
||||
}
|
||||
const commonStyle = {
|
||||
boxSize: 'initial',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
transition: variantChange,
|
||||
bg: 'transparent',
|
||||
ps: '6px',
|
||||
py: '12px',
|
||||
pe: '4px',
|
||||
w: '100%',
|
||||
borderRadius: '15px',
|
||||
_active: {
|
||||
bg: 'inherit',
|
||||
transform: 'none',
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
_focus: {
|
||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const NavLinkButton: React.FC<Props> = ({ activeRoute, route, role }) => {
|
||||
type Props = {
|
||||
isActive: boolean;
|
||||
route: Route;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
||||
|
||||
if (route.navButton) {
|
||||
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink to={route.path} key={uuid()}>
|
||||
{activeRoute(route.path, undefined) === 'active' ? (
|
||||
<Button
|
||||
hidden={route.hidden || !route.authorized.includes(role)}
|
||||
boxSize="initial"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
boxShadow="none"
|
||||
bg="transparent"
|
||||
transition={variantChange}
|
||||
mb="12px"
|
||||
mx="auto"
|
||||
ps="10px"
|
||||
py="12px"
|
||||
ml={4}
|
||||
w="90%"
|
||||
borderRadius="15px"
|
||||
_active={{
|
||||
bg: 'inherit',
|
||||
transform: 'none',
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
_focus={{
|
||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
||||
}}
|
||||
>
|
||||
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
|
||||
{isActive ? (
|
||||
<Button {...commonStyle} boxShadow="none">
|
||||
<Flex>
|
||||
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
|
||||
{route.icon(true)}
|
||||
@@ -58,22 +60,8 @@ const NavLinkButton: React.FC<Props> = ({ activeRoute, route, role }) => {
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
hidden={route.hidden || !route.authorized.includes(role)}
|
||||
boxSize="initial"
|
||||
justifyContent="flex-start"
|
||||
alignItems="center"
|
||||
bg="transparent"
|
||||
mb="12px"
|
||||
py="12px"
|
||||
{...commonStyle}
|
||||
ps="6px"
|
||||
borderRadius="15px"
|
||||
w="90%"
|
||||
ml={2}
|
||||
_active={{
|
||||
bg: 'inherit',
|
||||
transform: 'none',
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
_focus={{
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
@@ -91,5 +79,3 @@ const NavLinkButton: React.FC<Props> = ({ activeRoute, route, role }) => {
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NavLinkButton);
|
||||
|
||||
@@ -8,69 +8,85 @@ import {
|
||||
DrawerOverlay,
|
||||
Flex,
|
||||
useColorModeValue,
|
||||
useColorMode,
|
||||
Text,
|
||||
Spacer,
|
||||
useBreakpoint,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import createLinks from './CreateLinks';
|
||||
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
||||
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NavLinkButton } from './NavLinkButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
const variantChange = '0.2s linear';
|
||||
|
||||
interface Props {
|
||||
export type SidebarProps = {
|
||||
routes: Route[];
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
}
|
||||
logo: React.ReactNode;
|
||||
version: string;
|
||||
children?: React.ReactNode;
|
||||
topNav?: (isRouteActive: (str: string, str2: string) => boolean, toggleSidebar: () => void) => React.ReactNode;
|
||||
};
|
||||
|
||||
const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
||||
export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, children }: SidebarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const location = useLocation();
|
||||
const { colorMode } = useColorMode();
|
||||
const navbarShadow = useColorModeValue('0px 7px 23px rgba(0, 0, 0, 0.05)', 'none');
|
||||
const breakpoint = useBreakpoint();
|
||||
|
||||
const activeRoute = (routeName: string, otherRoute: string | undefined) => {
|
||||
const isRouteActive = (routeName: string, otherRoute?: string) => {
|
||||
if (otherRoute)
|
||||
return location.pathname.split('/')[1] === routeName.split('/')[1] ||
|
||||
return (
|
||||
location.pathname.split('/')[1] === routeName.split('/')[1] ||
|
||||
location.pathname.split('/')[1] === otherRoute.split('/')[1]
|
||||
? 'active'
|
||||
: '';
|
||||
);
|
||||
|
||||
return location.pathname === routeName ? 'active' : '';
|
||||
return location.pathname === routeName.replace(':id', '0');
|
||||
};
|
||||
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
|
||||
const brand = (
|
||||
<Box pt="25px" mb="12px">
|
||||
<img
|
||||
src={colorMode === 'light' ? lightLogo : darkLogo}
|
||||
alt="OpenWifi"
|
||||
width="180px"
|
||||
height="100px"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
<Box pt="25px" mb="15px" px="12px">
|
||||
{logo}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const sidebarContent = React.useMemo(
|
||||
() => (
|
||||
<>
|
||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||
{routes
|
||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||
.map((route) => (
|
||||
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
|
||||
))}
|
||||
</VStack>
|
||||
<Spacer />
|
||||
<Box mb={2}>{children}</Box>
|
||||
<Box>
|
||||
<Text color="gray.400">
|
||||
{t('footer.version')} {version}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
),
|
||||
[user?.userRole, location],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Drawer isOpen={isCompact && isOpen} onClose={toggle} placement="left">
|
||||
<DrawerOverlay />
|
||||
<DrawerContent
|
||||
w="200px"
|
||||
maxW="200px"
|
||||
w="250px"
|
||||
maxW="250px"
|
||||
ms={{
|
||||
base: '16px',
|
||||
}}
|
||||
@@ -80,17 +96,11 @@ const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
||||
borderRadius="16px"
|
||||
>
|
||||
<DrawerCloseButton />
|
||||
<DrawerBody w="200px" px={0}>
|
||||
<Box maxW="200px" h="90vh">
|
||||
<Box>{brand}</Box>
|
||||
<DrawerBody maxW="250px" px="1rem">
|
||||
<Box maxW="100%" h="90vh">
|
||||
{brand}
|
||||
<Flex direction="column" mb="40px" h="calc(100vh - 200px)" alignItems="center" overflowY="auto">
|
||||
<Box>{createLinks(routes, activeRoute, user?.userRole ?? '')}</Box>
|
||||
<Spacer />
|
||||
<Box>
|
||||
<Text color="gray.400">
|
||||
{t('footer.version')} {__APP_VERSION__}
|
||||
</Text>
|
||||
</Box>
|
||||
{sidebarContent}
|
||||
</Flex>
|
||||
</Box>
|
||||
</DrawerBody>
|
||||
@@ -104,21 +114,14 @@ const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
||||
transition={variantChange}
|
||||
w="200px"
|
||||
maxW="200px"
|
||||
ms="14px"
|
||||
h="calc(100vh - 32px)"
|
||||
mt="16px"
|
||||
my="16px"
|
||||
ml="16px"
|
||||
borderRadius="16px"
|
||||
>
|
||||
<Box>{brand}</Box>
|
||||
{brand}
|
||||
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">
|
||||
<Box>{createLinks(routes, activeRoute, user?.userRole ?? '')}</Box>
|
||||
<Spacer />
|
||||
<Box>
|
||||
<Text color="gray.400">
|
||||
{t('footer.version')} {__APP_VERSION__}
|
||||
</Text>
|
||||
</Box>
|
||||
{sidebarContent}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -126,5 +129,3 @@ const Sidebar = ({ routes, isOpen, toggle }: Props) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -1,52 +1,65 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { Center, Flex, Portal, Spinner, useBoolean, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { useBoolean, useColorMode } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import PanelContainer from './Containers/PanelContainer';
|
||||
import PanelContent from './Containers/PanelContent';
|
||||
import MainPanel from './MainPanel';
|
||||
import Navbar from './Navbar';
|
||||
import Sidebar from './Sidebar';
|
||||
import { Navbar } from './Navbar';
|
||||
import { PageContainer } from './PageContainer';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
||||
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||
import { Route as RouteProps } from 'models/Routes';
|
||||
import NotFoundPage from 'pages/NotFound';
|
||||
import routes from 'router/routes';
|
||||
|
||||
const Layout = () => {
|
||||
const breakpoint = useBreakpoint('xl');
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { colorMode } = useColorMode();
|
||||
const [isSidebarOpen, { toggle: toggleSidebar }] = useBoolean(false);
|
||||
document.documentElement.dir = 'ltr';
|
||||
|
||||
const activeRoute = React.useMemo(() => {
|
||||
const route = routes.find(
|
||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
||||
);
|
||||
|
||||
if (route) return route.navName ? t(route.navName) : t(route.name);
|
||||
|
||||
return '';
|
||||
}, [t, location.pathname]);
|
||||
|
||||
const getRoutes = (r: RouteProps[]) =>
|
||||
// @ts-ignore
|
||||
r.map((route: RouteProps) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
||||
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sidebar routes={routes} isOpen={isSidebarOpen} toggle={toggleSidebar} />
|
||||
<Portal>
|
||||
<Navbar secondary={false} toggleSidebar={toggleSidebar} />
|
||||
</Portal>
|
||||
<MainPanel w={isCompact ? 'calc(100%)' : 'calc(100% - 210px)'}>
|
||||
<PanelContent>
|
||||
<PanelContainer>
|
||||
<Suspense
|
||||
fallback={
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
<Center mt={10}>
|
||||
<Spinner />
|
||||
</Center>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<Routes>
|
||||
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</PanelContainer>
|
||||
</PanelContent>
|
||||
</MainPanel>
|
||||
<Sidebar
|
||||
routes={routes}
|
||||
isOpen={isSidebarOpen}
|
||||
toggle={toggleSidebar}
|
||||
version={__APP_VERSION__}
|
||||
logo={
|
||||
<img
|
||||
src={colorMode === 'light' ? lightLogo : darkLogo}
|
||||
alt="OpenWifi"
|
||||
width="180px"
|
||||
height="100px"
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
||||
<PageContainer waitForUser>
|
||||
<Routes>
|
||||
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
||||
</Routes>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface Route {
|
||||
name: string;
|
||||
navName?: string;
|
||||
icon: (active: boolean) => ReactNode;
|
||||
navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode;
|
||||
isEntity?: boolean;
|
||||
component: unknown;
|
||||
hidden?: boolean;
|
||||
|
||||
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[];
|
||||
};
|
||||
@@ -1,13 +1,42 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
| 'subscriber'
|
||||
| 'partner'
|
||||
| 'csr'
|
||||
| 'system'
|
||||
| 'installer'
|
||||
| 'noc'
|
||||
| 'accounting';
|
||||
|
||||
export type User = {
|
||||
avatar: string;
|
||||
blackListed: boolean;
|
||||
creationDate: number;
|
||||
currentLoginURI: string;
|
||||
currentPassword: string;
|
||||
description: string;
|
||||
currentPassword?: string;
|
||||
id: string;
|
||||
email: string;
|
||||
userRole: string;
|
||||
id: string;
|
||||
lastEmailCheck: number;
|
||||
lastLogin: number;
|
||||
lastPasswordChange: number;
|
||||
lastPasswords: string[];
|
||||
locale: string;
|
||||
location: string;
|
||||
modified: number;
|
||||
name: string;
|
||||
notes: Note[];
|
||||
oauthType: string;
|
||||
oauthUserInfo: string;
|
||||
owner: string;
|
||||
securityPolicy: string;
|
||||
securityPolicyChange: number;
|
||||
signingUp: string;
|
||||
suspended: boolean;
|
||||
userRole: UserRole;
|
||||
userTypeProprietaryInfo: {
|
||||
authenticatorSecret: string;
|
||||
mfa: {
|
||||
@@ -16,6 +45,9 @@ export interface User {
|
||||
};
|
||||
mobiles: { number: string }[];
|
||||
};
|
||||
suspended: boolean;
|
||||
notes: Note[];
|
||||
}
|
||||
validated: boolean;
|
||||
validationDate: number;
|
||||
validationEmail: string;
|
||||
validationURI: string;
|
||||
waitingForEmailCheck: boolean;
|
||||
};
|
||||
|
||||
@@ -48,7 +48,10 @@ const _LoginForm: React.FC<_LoginFormProps> = ({ setActiveForm }) => {
|
||||
const displayError = useMemo(() => {
|
||||
const loginError: AxiosError = error as AxiosError;
|
||||
|
||||
if (loginError?.response?.data?.ErrorCode === 4) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 5) return t('login.waiting_for_email_verification');
|
||||
if (loginError?.response?.data?.ErrorCode === 15) {
|
||||
return t('login.suspended_error');
|
||||
}
|
||||
return t('login.invalid_credentials');
|
||||
}, [t, error]);
|
||||
|
||||
|
||||
189
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
189
src/pages/Notifications/GeneralLogs/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from '@phosphor-icons/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 { useSimulatorStore } from 'contexts/SimulatorSocketProvider/useStore';
|
||||
import { LogLevel } from 'contexts/SimulatorSocketProvider/utils';
|
||||
import { dateForFilename } from 'helpers/dateFormatting';
|
||||
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||
|
||||
const GeneralLogsCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { availableLogTypes, hiddenLogIds, setHiddenLogIds, logs } = useSimulatorStore((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.length, 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[]}
|
||||
>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</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;
|
||||
189
src/pages/Notifications/SecLogs/index.tsx
Normal file
189
src/pages/Notifications/SecLogs/index.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Flex,
|
||||
HStack,
|
||||
IconButton,
|
||||
Select,
|
||||
Spacer,
|
||||
Table,
|
||||
Text,
|
||||
Th,
|
||||
Thead,
|
||||
Tooltip,
|
||||
Tr,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download } from '@phosphor-icons/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/SimulatorSocketProvider/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[]}
|
||||
>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</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;
|
||||
120
src/pages/Notifications/Simulations/index.tsx
Normal file
120
src/pages/Notifications/Simulations/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, HStack, IconButton, Spacer, Table, Text, Th, Thead, Tooltip, Tr } from '@chakra-ui/react';
|
||||
import { Download } from '@phosphor-icons/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 { useSimulatorStore } from 'contexts/SimulatorSocketProvider/useStore';
|
||||
import { dateForFilename } from 'helpers/dateFormatting';
|
||||
import { useGetSimulations } from 'hooks/Network/Simulations';
|
||||
|
||||
const SimulationsLogCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { logs } = useSimulatorStore((state) => ({
|
||||
logs: state.allMessages,
|
||||
}));
|
||||
const getSimulations = useGetSimulations();
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const arr = logs.filter((d) => d.type === 'NOTIFICATION' && d.data.type === 'SIMULATION_STATUS');
|
||||
return arr.reverse();
|
||||
}, [logs.length]);
|
||||
|
||||
const getSimulationName = (id?: string) =>
|
||||
getSimulations.data?.list.find(({ id: simId }) => simId === id)?.name ?? id;
|
||||
|
||||
type RowProps = { index: number; style: React.CSSProperties };
|
||||
const Row = React.useCallback(
|
||||
({ index, style }: RowProps) => {
|
||||
const msg = data[index];
|
||||
if (msg?.type === 'NOTIFICATION' && msg?.data.type === 'SIMULATION_STATUS') {
|
||||
return (
|
||||
<Box style={style}>
|
||||
<Flex w="100%">
|
||||
<Box flex="0 1 110px">
|
||||
<Text>{msg.timestamp.toLocaleTimeString()}</Text>
|
||||
</Box>
|
||||
<Box flex="0 1 120px" textAlign="left">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{getSimulationName(msg.data.content.simulationId) ?? '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box textAlign="left" w="calc(100% - 80px - 120px - 30px)">
|
||||
<Text textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(msg.data.content)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[t, data, getSimulations],
|
||||
);
|
||||
|
||||
const downloadableLogs = React.useMemo(
|
||||
() =>
|
||||
data.map((msg) =>
|
||||
msg.type === 'NOTIFICATION' && msg.data.type === 'SIMULATION_STATUS'
|
||||
? {
|
||||
timestamp: msg.timestamp.toLocaleString(),
|
||||
message: JSON.stringify(msg.data.content),
|
||||
}
|
||||
: {},
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<HStack spacing={2}>
|
||||
<CSVLink
|
||||
filename={`logs_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||
data={downloadableLogs as object[]}
|
||||
>
|
||||
<Tooltip label={t('logs.export')} hasArrow>
|
||||
<IconButton aria-label={t('logs.export')} icon={<Download />} colorScheme="blue" />
|
||||
</Tooltip>
|
||||
</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="120px">{t('simulation.one')}</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}
|
||||
itemKey={(index) => data[index]?.id ?? uuid()}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
)}
|
||||
</ReactVirtualizedAutoSizer>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SimulationsLogCard;
|
||||
83
src/pages/Notifications/index.tsx
Normal file
83
src/pages/Notifications/index.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import GeneralLogsCard from './GeneralLogs';
|
||||
import SecLogsCard from './SecLogs';
|
||||
import SimulationsLogCard from './Simulations';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
|
||||
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 [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||
|
||||
const handleTabChange = (index: number) => {
|
||||
setTabIndex(index);
|
||||
localStorage.setItem(INDEX_PARAM, index.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<Card p={0}>
|
||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||
<TabList>
|
||||
<CardHeader>
|
||||
<Tab>{t('simulation.other')}</Tab>
|
||||
<Tab>Simulator</Tab>
|
||||
<Tab>{t('logs.security')}</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"
|
||||
>
|
||||
<SimulationsLogCard />
|
||||
</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>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationsPage;
|
||||
89
src/pages/Profile/DeleteButton.tsx
Normal file
89
src/pages/Profile/DeleteButton.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Spinner,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DeleteButton } from '../../components/Buttons/DeleteButton';
|
||||
import { Modal } from '../../components/Modals/Modal';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useDeleteUser } from 'hooks/Network/Users';
|
||||
|
||||
type Props = {
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
const DeleteProfileButton = ({ isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { user, logout } = useAuth();
|
||||
const deleteUser = useDeleteUser();
|
||||
const modalProps = useDisclosure();
|
||||
|
||||
const onDeleteClick = () =>
|
||||
deleteUser.mutate(user?.id ?? '', {
|
||||
onSuccess: () => {
|
||||
setTimeout(() => {
|
||||
logout();
|
||||
}, 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const onOpen = () => {
|
||||
deleteUser.reset();
|
||||
modalProps.onOpen();
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DeleteButton isCompact isDisabled={isDisabled} onClick={onOpen} />
|
||||
<Modal {...modalProps} title={t('profile.delete_account')}>
|
||||
<Box>
|
||||
{deleteUser.isSuccess ? (
|
||||
<Center>
|
||||
<Alert status="success">
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
{t('Your profile is now deleted, we will now log you out...')} <Spinner />
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Center>
|
||||
) : (
|
||||
<>
|
||||
<Center>
|
||||
{deleteUser.error ? (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{axios.isAxiosError(deleteUser.error) ? deleteUser.error.response?.data?.ErrorDescription : ''}
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert status="warning">
|
||||
<AlertIcon />
|
||||
<AlertDescription>{t('profile.delete_warning')}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</Center>
|
||||
<Center my={8}>
|
||||
<Button onClick={onDeleteClick} isLoading={deleteUser.isLoading} colorScheme="red">
|
||||
{t('profile.delete_account_confirm')}
|
||||
</Button>
|
||||
</Center>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteProfileButton;
|
||||
@@ -1,20 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Center, Flex, Heading, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
|
||||
import { Box, Center, Flex, Heading, HStack, Link, Spacer, Spinner, useToast } from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import DeleteProfileButton from './DeleteButton';
|
||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testRegex } from 'helpers/formTests';
|
||||
import { useUpdateAccount } from 'hooks/Network/Account';
|
||||
import { UserRole } from 'hooks/Network/Users';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
import { useFormModal } from 'hooks/useFormModal';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
@@ -70,13 +74,16 @@ const GeneralInformationProfile = () => {
|
||||
<CardHeader mb={2}>
|
||||
<Heading size="md">{t('profile.your_profile')}</Heading>
|
||||
<Spacer />
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} ml={2} />
|
||||
<HStack>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
hidden={!isEditing}
|
||||
/>
|
||||
<ToggleEditButton toggleEdit={toggleEditing} isEditing={isEditing} />
|
||||
<DeleteProfileButton isDisabled={isEditing} />
|
||||
</HStack>
|
||||
</CardHeader>
|
||||
<CardBody display="block">
|
||||
{!user ? (
|
||||
@@ -89,6 +96,7 @@ const GeneralInformationProfile = () => {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}>
|
||||
key={formKey}
|
||||
initialValues={
|
||||
@@ -97,11 +105,13 @@ const GeneralInformationProfile = () => {
|
||||
description: user?.description ?? '',
|
||||
firstName: user?.name.split(' ')[0] ?? '',
|
||||
lastName: user?.name.split(' ')[1] ?? '',
|
||||
userRole: user?.userRole,
|
||||
} as {
|
||||
description: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}
|
||||
}
|
||||
innerRef={
|
||||
@@ -111,17 +121,19 @@ const GeneralInformationProfile = () => {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
newPassword?: string;
|
||||
userRole: UserRole;
|
||||
}>
|
||||
>
|
||||
}
|
||||
validationSchema={FormSchema(t, { passRegex: passwordPattern })}
|
||||
onSubmit={async ({ description, firstName, lastName, newPassword }, { setSubmitting }) => {
|
||||
onSubmit={async ({ description, firstName, lastName, newPassword, userRole }, { setSubmitting }) => {
|
||||
await updateUser.mutateAsync(
|
||||
{
|
||||
id: user?.id,
|
||||
description,
|
||||
name: `${firstName} ${lastName}`,
|
||||
currentPassword: newPassword,
|
||||
userRole: user?.userRole === 'root' ? userRole : undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
@@ -139,13 +151,45 @@ const GeneralInformationProfile = () => {
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) {
|
||||
toast({
|
||||
id: 'account-update-error',
|
||||
title: t('common.error'),
|
||||
description: e.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting }) => (
|
||||
<Form>
|
||||
<StringField name="email" label={t('common.email')} isDisabled />
|
||||
<Flex>
|
||||
<StringField name="email" label={t('common.email')} isDisabled />
|
||||
<Box w={8} />
|
||||
<SelectField
|
||||
name="userRole"
|
||||
label={t('user.role')}
|
||||
options={[
|
||||
{ value: 'accounting', label: 'Accounting' },
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'csr', label: 'CSR' },
|
||||
{ value: 'installer', label: 'Installer' },
|
||||
{ value: 'noc', label: 'NOC' },
|
||||
{ value: 'root', label: 'Root' },
|
||||
{ value: 'system', label: 'System' },
|
||||
]}
|
||||
isRequired
|
||||
isDisabled={isSubmitting || !isEditing || user?.userRole !== 'root'}
|
||||
w="max-content"
|
||||
/>
|
||||
</Flex>
|
||||
<Flex my={4}>
|
||||
<StringField
|
||||
name="firstName"
|
||||
|
||||
@@ -10,7 +10,7 @@ const ProfileLayout = () => (
|
||||
breakpointCols={{
|
||||
default: 3,
|
||||
1800: 2,
|
||||
1100: 1,
|
||||
1200: 1,
|
||||
}}
|
||||
className="my-masonry-grid"
|
||||
columnClassName="my-masonry-grid_column"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, IconButton, Text, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddPhoneNumberModal from './AddPhoneNumberModal';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useToast,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataTable } from '../../components/DataTables/DataTable';
|
||||
import FormattedDate from '../../components/InformationDisplays/FormattedDate';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Avatar, AvatarBadge, Box, Center, Heading, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
import AvatarModal from './AvatarModal';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Center, Flex, Spinner } from '@chakra-ui/react';
|
||||
import ProfileLayout from './Layout';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
{!isUserLoaded ? (
|
||||
<Center mt={40}>
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
) : (
|
||||
<ProfileLayout />
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
const ProfilePage = () => <ProfileLayout />;
|
||||
|
||||
export default ProfilePage;
|
||||
|
||||
@@ -22,11 +22,18 @@ import { useSimulatorStore } from 'contexts/SimulatorSocketProvider/useStore';
|
||||
|
||||
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
|
||||
const MessagesChart = () => {
|
||||
const { colorMode } = useColorMode();
|
||||
const currentSimulationData = useSimulatorStore((state) => state.currentSimulationData);
|
||||
type Props = {
|
||||
statusId: string;
|
||||
};
|
||||
|
||||
const { data } = React.useMemo(() => {
|
||||
const MessagesChart = ({ statusId }: Props) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const currentSimulationData = useSimulatorStore(
|
||||
React.useCallback((state) => state.currentSimulationsData[statusId] ?? [], [statusId]),
|
||||
(oldState, newState) => oldState?.length === newState?.length,
|
||||
);
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
const labels = [] as string[];
|
||||
const msgsTx = [] as number[];
|
||||
const msgsRx = [] as number[];
|
||||
@@ -41,21 +48,14 @@ const MessagesChart = () => {
|
||||
labels.push(curr.timestamp.toLocaleTimeString());
|
||||
msgsTx.push(curr.msgsTx);
|
||||
msgsRx.push(curr.msgsRx);
|
||||
} else if (acc.entriesCount < 3) {
|
||||
} else {
|
||||
acc.msgsTx += curr.msgsTx;
|
||||
acc.msgsRx += curr.msgsRx;
|
||||
msgsTx.push(curr.msgsTx);
|
||||
msgsRx.push(curr.msgsRx);
|
||||
acc.entriesCount += 1;
|
||||
acc.dates.push(Math.floor(curr.timestamp.getTime() / 1000));
|
||||
} else {
|
||||
labels.push(
|
||||
new Date(Math.floor(acc.dates.reduce((a, b) => a + b, 0) / acc.entriesCount) * 1000).toLocaleTimeString(),
|
||||
);
|
||||
msgsTx.push(acc.msgsTx);
|
||||
msgsRx.push(acc.msgsRx);
|
||||
acc.msgsTx = 0;
|
||||
acc.msgsRx = 0;
|
||||
acc.entriesCount = 0;
|
||||
acc.dates = [];
|
||||
acc.dates.push(curr.timestamp.getTime() / 1000);
|
||||
labels.push(curr.timestamp.toLocaleTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +79,7 @@ const MessagesChart = () => {
|
||||
],
|
||||
};
|
||||
|
||||
return {
|
||||
data: newData,
|
||||
};
|
||||
return newData;
|
||||
}, [currentSimulationData]);
|
||||
|
||||
const options: _DeepPartialObject<
|
||||
@@ -89,7 +87,7 @@ const MessagesChart = () => {
|
||||
ElementChartOptions<'line'> &
|
||||
PluginChartOptions<'line'> &
|
||||
DatasetChartOptions<'line'> &
|
||||
ScaleChartOptions<any> &
|
||||
ScaleChartOptions<'line'> &
|
||||
LineControllerChartOptions
|
||||
> = React.useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Center, Flex, Heading, SimpleGrid, Spacer, Spinner, Text, Tooltip } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MessagesChart from './MessagesChart';
|
||||
import TxRxChart from './TxRxChart';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { useSimulatorStore } from 'contexts/SimulatorSocketProvider/useStore';
|
||||
import { bytesString } from 'helpers/stringHelper';
|
||||
import { SimulationStatus } from 'hooks/Network/Simulations';
|
||||
|
||||
type Props = {
|
||||
status: SimulationStatus;
|
||||
onStop: (id: string, simulationId: string) => void;
|
||||
isStopLoading: boolean;
|
||||
onCancel: (id: string, simulationId: string) => void;
|
||||
isCancelLoading: boolean;
|
||||
};
|
||||
|
||||
const SingleSimulationCurrentlyRunning = ({ status, onStop, isStopLoading, onCancel, isCancelLoading }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const currentStatus = useSimulatorStore(
|
||||
React.useCallback((state) => state.currentSimulationsData[status.id] ?? [], [status.id]),
|
||||
(oldState, newState) => oldState?.length === newState?.length,
|
||||
);
|
||||
const latestStatus = currentStatus?.[currentStatus.length - 1]?.rawData ?? status;
|
||||
|
||||
const handleStopClick = () => onStop(status.id, status.simulationId);
|
||||
const handleCancelClick = () => onCancel(status.id, status.simulationId);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{status ? (
|
||||
<>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<Button onClick={handleStopClick} isLoading={isStopLoading} colorScheme="yellow" size="sm">
|
||||
{t('simulation.stop')}
|
||||
</Button>
|
||||
<Tooltip label={t('simulation.cancel_explanation')}>
|
||||
<Button onClick={handleCancelClick} isLoading={isCancelLoading} colorScheme="red" size="sm" ml={2}>
|
||||
{t('simulation.cancel')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="200px">
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('common.started')}
|
||||
</Heading>
|
||||
<FormattedDate key={currentStatus.length} date={latestStatus.startTime} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.owner')}
|
||||
</Heading>
|
||||
<Text>{status.owner}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.time_to_full')}
|
||||
</Heading>
|
||||
<Text>
|
||||
{latestStatus.timeToFullDevices} {t('common.seconds')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.current_live_devices')}
|
||||
</Heading>
|
||||
<Text>{latestStatus.liveDevices}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.error_devices')}
|
||||
</Heading>
|
||||
<Text>{latestStatus.errorDevices}</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
<Heading mt={4} mb={1} size="sm" textDecor="underline">
|
||||
{t('analytics.total_data')}
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="200px">
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.tx_messages')}
|
||||
</Heading>
|
||||
<Text>{latestStatus.msgsTx.toLocaleString()}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.tx')}
|
||||
</Heading>
|
||||
<Text>{bytesString(latestStatus.tx)}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.rx_messages')}
|
||||
</Heading>
|
||||
<Text>{latestStatus.msgsRx.toLocaleString()}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.rx')}
|
||||
</Heading>
|
||||
<Text>{bytesString(latestStatus.rx)}</Text>
|
||||
</Box>
|
||||
<Box />
|
||||
</SimpleGrid>
|
||||
<Heading mt={4} mb={0} size="sm" textDecor="underline">
|
||||
{t('simulation.realtime_data')}
|
||||
</Heading>
|
||||
<Box h="calc(20vh)" w="100%">
|
||||
<TxRxChart statusId={status.id} />
|
||||
</Box>
|
||||
<Heading mt={4} mb={0} size="sm" textDecor="underline">
|
||||
{t('simulation.realtime_messages')}
|
||||
</Heading>
|
||||
<Box h="calc(20vh)" w="100%">
|
||||
<MessagesChart statusId={status.id} />
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Center my="50px">
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleSimulationCurrentlyRunning;
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
ElementChartOptions,
|
||||
PluginChartOptions,
|
||||
DatasetChartOptions,
|
||||
ScaleChartOptions,
|
||||
LineControllerChartOptions,
|
||||
} from 'chart.js';
|
||||
import { _DeepPartialObject } from 'chart.js/types/utils';
|
||||
@@ -35,9 +34,16 @@ const getDivisionFactor = (maxBytes: number) => {
|
||||
return { factor: 1024 * 1024 * 1024, unit: 'GB' };
|
||||
};
|
||||
|
||||
const TxRxChart = () => {
|
||||
type Props = {
|
||||
statusId: string;
|
||||
};
|
||||
|
||||
const TxRxChart = ({ statusId }: Props) => {
|
||||
const { colorMode } = useColorMode();
|
||||
const currentSimulationData = useSimulatorStore((state) => state.currentSimulationData);
|
||||
const currentSimulationData = useSimulatorStore(
|
||||
React.useCallback((state) => state.currentSimulationsData[statusId] ?? [], [statusId]),
|
||||
(oldState, newState) => oldState?.length === newState?.length,
|
||||
);
|
||||
|
||||
const { data, unit } = React.useMemo(() => {
|
||||
const { factor, unit: newUnit } = getDivisionFactor(Math.max(...currentSimulationData.map((o) => o.tx)));
|
||||
@@ -55,21 +61,14 @@ const TxRxChart = () => {
|
||||
labels.push(curr.timestamp.toLocaleTimeString());
|
||||
tx.push(Math.floor((curr.tx / factor) * 100) / 100);
|
||||
rx.push(Math.floor((curr.rx / factor) * 100) / 100);
|
||||
} else if (acc.entriesCount < 3) {
|
||||
} else {
|
||||
acc.tx += curr.tx;
|
||||
acc.rx += curr.rx;
|
||||
tx.push(Math.floor((curr.tx / factor) * 100) / 100);
|
||||
rx.push(Math.floor((curr.rx / factor) * 100) / 100);
|
||||
acc.entriesCount += 1;
|
||||
acc.dates.push(Math.floor(curr.timestamp.getTime() / 1000));
|
||||
} else {
|
||||
labels.push(
|
||||
new Date(Math.floor(acc.dates.reduce((a, b) => a + b, 0) / acc.entriesCount) * 1000).toLocaleTimeString(),
|
||||
);
|
||||
tx.push(Math.floor((acc.tx / factor) * 100) / 100);
|
||||
rx.push(Math.floor((acc.rx / factor) * 100) / 100);
|
||||
acc.tx = 0;
|
||||
acc.rx = 0;
|
||||
acc.entriesCount = 0;
|
||||
acc.dates = [];
|
||||
acc.dates.push(curr.timestamp.getTime() / 1000);
|
||||
labels.push(curr.timestamp.toLocaleTimeString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +103,6 @@ const TxRxChart = () => {
|
||||
ElementChartOptions<'line'> &
|
||||
PluginChartOptions<'line'> &
|
||||
DatasetChartOptions<'line'> &
|
||||
ScaleChartOptions<any> &
|
||||
LineControllerChartOptions
|
||||
> = React.useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -8,45 +8,41 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Flex,
|
||||
Heading,
|
||||
SimpleGrid,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Text,
|
||||
Tooltip,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MessagesChart from './MessagesChart';
|
||||
import TxRxChart from './TxRxChart';
|
||||
import SingleSimulationCurrentlyRunning from './SingleSimulationCurrentlyRunning';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { bytesString } from 'helpers/stringHelper';
|
||||
import {
|
||||
SimulationStatus,
|
||||
useCancelSimulation,
|
||||
useGetSimulations,
|
||||
useGetSimulationStatus,
|
||||
useGetSimulationsStatus,
|
||||
useStopSimulation,
|
||||
} from 'hooks/Network/Simulations';
|
||||
|
||||
const CurrentlyRunningCard = () => {
|
||||
type Props = {
|
||||
currentlyRunningStatus: SimulationStatus[];
|
||||
};
|
||||
|
||||
const CurrentlyRunningCard = ({ currentlyRunningStatus }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const getSims = useGetSimulations();
|
||||
const getStatus = useGetSimulationStatus();
|
||||
const getStatus = useGetSimulationsStatus();
|
||||
const cancelSim = useCancelSimulation();
|
||||
const stopSim = useStopSimulation();
|
||||
|
||||
const currentSim = getSims.data?.list?.find((sim) => sim.id === getStatus.data?.simulationId);
|
||||
|
||||
const handleStopClick = () =>
|
||||
const handleStopClick = (id: string, simulationId: string) =>
|
||||
stopSim.mutate(
|
||||
{ id: getStatus.data?.id ?? '' },
|
||||
{ runId: id, simulationId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@@ -74,9 +70,9 @@ const CurrentlyRunningCard = () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
const handleCancelClick = () =>
|
||||
const handleCancelClick = (id: string, simulationId: string) =>
|
||||
cancelSim.mutate(
|
||||
{ id: getStatus.data?.id ?? '' },
|
||||
{ runId: id, simulationId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@@ -104,6 +100,10 @@ const CurrentlyRunningCard = () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const getTabName = (status: SimulationStatus) =>
|
||||
getSims.data?.list.find((sim) => sim.id === status.simulationId)?.name ?? 'Unknown';
|
||||
|
||||
return (
|
||||
<Card p={0}>
|
||||
<Accordion allowToggle>
|
||||
@@ -118,119 +118,35 @@ const CurrentlyRunningCard = () => {
|
||||
borderBottomRightRadius={isExpanded ? '0px' : undefined}
|
||||
>
|
||||
<AlertIcon />
|
||||
<AlertTitle>{t('simulation.sim_currently_running', { sim: currentSim?.name })}</AlertTitle>
|
||||
<AlertTitle>
|
||||
{t('simulation.currently_running', { count: currentlyRunningStatus.length })}
|
||||
</AlertTitle>
|
||||
<Spacer />
|
||||
<AccordionIcon />
|
||||
</Alert>
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<Box>
|
||||
{getStatus.data ? (
|
||||
<>
|
||||
<Flex>
|
||||
<Heading my="auto" size="sm" textDecor="underline">
|
||||
{currentSim?.name}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Button onClick={handleStopClick} isLoading={stopSim.isLoading} colorScheme="yellow" size="sm">
|
||||
{t('simulation.stop')}
|
||||
</Button>
|
||||
<Tooltip label={t('simulation.cancel_explanation')}>
|
||||
<Button
|
||||
onClick={handleCancelClick}
|
||||
isLoading={cancelSim.isLoading}
|
||||
colorScheme="red"
|
||||
size="sm"
|
||||
ml={2}
|
||||
>
|
||||
{t('simulation.cancel')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<SimpleGrid minChildWidth="200px">
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('common.started')}
|
||||
</Heading>
|
||||
<FormattedDate date={getStatus.data?.startTime} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.owner')}
|
||||
</Heading>
|
||||
<Text>{getStatus.data?.owner}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.time_to_full')}
|
||||
</Heading>
|
||||
<Text>
|
||||
{getStatus.data.timeToFullDevices} {t('common.seconds')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.current_live_devices')}
|
||||
</Heading>
|
||||
<Text>{getStatus.data.liveDevices}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.error_devices')}
|
||||
</Heading>
|
||||
<Text>{getStatus.data.errorDevices}</Text>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
<Heading mt={4} mb={1} size="sm" textDecor="underline">
|
||||
{t('analytics.total_data')}
|
||||
</Heading>
|
||||
<SimpleGrid minChildWidth="200px">
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.tx_messages')}
|
||||
</Heading>
|
||||
<Text>{getStatus.data.msgsTx.toLocaleString()}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.tx')}
|
||||
</Heading>
|
||||
<Text>{bytesString(getStatus.data.tx)}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.rx_messages')}
|
||||
</Heading>
|
||||
<Text>{getStatus.data.msgsRx.toLocaleString()}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Heading size="sm" my="auto">
|
||||
{t('simulation.rx')}
|
||||
</Heading>
|
||||
<Text>{bytesString(getStatus.data.rx)}</Text>
|
||||
</Box>
|
||||
<Box />
|
||||
</SimpleGrid>
|
||||
<Heading mt={4} mb={0} size="sm" textDecor="underline">
|
||||
{t('simulation.realtime_data')}
|
||||
</Heading>
|
||||
<Box h="calc(20vh)" w="100%">
|
||||
<TxRxChart />
|
||||
</Box>
|
||||
<Heading mt={4} mb={0} size="sm" textDecor="underline">
|
||||
{t('simulation.realtime_messages')}
|
||||
</Heading>
|
||||
<Box h="calc(20vh)" w="100%">
|
||||
<MessagesChart />
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Center my="50px">
|
||||
<Spinner size="xl" />
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
<Tabs key={getStatus.data?.length}>
|
||||
<TabList>
|
||||
{getStatus.data?.map((status) => (
|
||||
<Tab key={status.id}>{getTabName(status)}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{getStatus.data?.map((status) => (
|
||||
<TabPanel key={status.id}>
|
||||
<SingleSimulationCurrentlyRunning
|
||||
status={status}
|
||||
onStop={handleStopClick}
|
||||
isStopLoading={stopSim.isLoading}
|
||||
onCancel={handleCancelClick}
|
||||
isCancelLoading={cancelSim.isLoading}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</AccordionPanel>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,13 +3,13 @@ import { Alert, AlertDescription, AlertIcon, AlertTitle, Center, Spinner } from
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CurrentlyRunningCard from './CurrentlyRunning';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { useGetSimulationStatus } from 'hooks/Network/Simulations';
|
||||
import { useGetSimulationsStatus } from 'hooks/Network/Simulations';
|
||||
|
||||
const StatusCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const getStatus = useGetSimulationStatus();
|
||||
const getStatus = useGetSimulationsStatus();
|
||||
|
||||
if (getStatus.isLoading) {
|
||||
if (getStatus.isLoading || !getStatus.data) {
|
||||
return (
|
||||
<Card p={0} h="48px">
|
||||
<Center my="auto">
|
||||
@@ -19,8 +19,9 @@ const StatusCard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (getStatus.data?.state === 'running') {
|
||||
return <CurrentlyRunningCard />;
|
||||
const currentlyRunningStatus = getStatus.data.filter((status) => status.state === 'running');
|
||||
if (currentlyRunningStatus.length > 0) {
|
||||
return <CurrentlyRunningCard currentlyRunningStatus={currentlyRunningStatus} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -17,13 +17,13 @@ import {
|
||||
useToast,
|
||||
HStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { ClockCounterClockwise, MagnifyingGlass, Play, Stop, Trash } from '@phosphor-icons/react';
|
||||
import axios from 'axios';
|
||||
import { ClockCounterClockwise, MagnifyingGlass, Play, Stop, Trash } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Simulation,
|
||||
useDeleteSimulation,
|
||||
useGetSimulationStatus,
|
||||
useGetSimulationsStatus,
|
||||
useStartSimulation,
|
||||
useStopSimulation,
|
||||
} from 'hooks/Network/Simulations';
|
||||
@@ -38,11 +38,13 @@ const Actions = ({ simulation, openEdit, onOpenHistory }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const getStatus = useGetSimulationStatus();
|
||||
const getStatus = useGetSimulationsStatus();
|
||||
const startSim = useStartSimulation();
|
||||
const stopSim = useStopSimulation();
|
||||
const deleteSim = useDeleteSimulation();
|
||||
|
||||
const currentSimulationStatus = getStatus.data?.find(({ simulationId }) => simulationId === simulation.id);
|
||||
|
||||
const handleEditClick = () => {
|
||||
openEdit(simulation);
|
||||
};
|
||||
@@ -81,7 +83,7 @@ const Actions = ({ simulation, openEdit, onOpenHistory }: Props) => {
|
||||
);
|
||||
const handleStopClick = () =>
|
||||
stopSim.mutate(
|
||||
{ id: getStatus.data?.id ?? '' },
|
||||
{ runId: currentSimulationStatus?.id ?? '', simulationId: simulation.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@@ -143,7 +145,7 @@ const Actions = ({ simulation, openEdit, onOpenHistory }: Props) => {
|
||||
|
||||
return (
|
||||
<HStack mx="auto">
|
||||
{getStatus.data?.simulationId === simulation.id && getStatus.data.state === 'running' ? (
|
||||
{currentSimulationStatus && currentSimulationStatus.state === 'running' ? (
|
||||
<Tooltip hasArrow label={t('simulation.stop')} placement="top">
|
||||
<IconButton
|
||||
aria-label={t('simulation.stop')}
|
||||
|
||||
@@ -12,7 +12,6 @@ import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { DEVICE_TYPES } from 'constants/deviceTypes';
|
||||
import { useGetDeviceTypes } from 'hooks/Network/Firmware';
|
||||
import { Simulation, useCreateSimulation } from 'hooks/Network/Simulations';
|
||||
import { useFormModal } from 'hooks/useFormModal';
|
||||
@@ -23,7 +22,6 @@ import { AxiosError } from 'models/Axios';
|
||||
const CreateSimulationModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const getDeviceTypes = useGetDeviceTypes();
|
||||
const deviceTypes = getDeviceTypes.data?.deviceTypes ?? DEVICE_TYPES;
|
||||
const { form, formRef } = useFormRef();
|
||||
const [formKey, setFormKey] = React.useState(uuid());
|
||||
const { isOpen, isConfirmOpen, onOpen, closeConfirm, closeModal, closeCancelAndForm } = useFormModal({
|
||||
@@ -47,18 +45,14 @@ const CreateSimulationModal = () => {
|
||||
onClose={closeModal}
|
||||
title={t('crud.create_object', { obj: t('simulation.one') })}
|
||||
topRightButtons={
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
<SaveButton onClick={form.submitForm} isLoading={form.isSubmitting} isDisabled={!form.isValid} />
|
||||
}
|
||||
>
|
||||
<Formik
|
||||
innerRef={formRef as React.Ref<FormikProps<object>>}
|
||||
key={formKey}
|
||||
initialValues={SimulationSchema(t, deviceTypes[0]).cast(undefined)}
|
||||
validationSchema={SimulationSchema(t, deviceTypes[0])}
|
||||
initialValues={SimulationSchema(t, getDeviceTypes.data?.deviceTypes?.[0]).cast(undefined)}
|
||||
validationSchema={SimulationSchema(t, getDeviceTypes.data?.deviceTypes?.[0])}
|
||||
onSubmit={async (data, { setSubmitting, resetForm }) =>
|
||||
createSim.mutateAsync(data as Partial<Simulation>, {
|
||||
onSuccess: () => {
|
||||
@@ -90,7 +84,7 @@ const CreateSimulationModal = () => {
|
||||
name="deviceType"
|
||||
label={t('common.type')}
|
||||
options={
|
||||
deviceTypes.map((v) => ({
|
||||
getDeviceTypes.data?.deviceTypes?.map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
})) ?? []
|
||||
|
||||
@@ -40,7 +40,7 @@ type Props = {
|
||||
const HistoryModal = ({ simulation, modalProps }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const getHistory = useGetSimulationHistory();
|
||||
const getHistory = useGetSimulationHistory({ id: simulation?.id ?? '' });
|
||||
const deleteResult = useDeleteSimulationResult();
|
||||
|
||||
const handleDeleteClick = (id: string) => () =>
|
||||
@@ -85,13 +85,13 @@ const HistoryModal = ({ simulation, modalProps }: Props) => {
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
{getHistory.error && (
|
||||
{getHistory.error ? (
|
||||
<Alert status="error">
|
||||
<AlertIcon />
|
||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||
<AlertDescription>{getHistory.error?.data?.ErrorDescription}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
) : null}
|
||||
{getHistory.data && (
|
||||
<Accordion allowMultiple>
|
||||
{getHistory.data
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Flex, Heading, IconButton, SimpleGrid, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import { Box, Flex, Heading, IconButton, SimpleGrid, Tooltip, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { Play, Stop } from '@phosphor-icons/react';
|
||||
import axios from 'axios';
|
||||
import { Form, Formik, FormikProps } from 'formik';
|
||||
import { Play, Stop } from 'phosphor-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { SimulationSchema } from '../../utils';
|
||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||
import DurationField from 'components/Form/Fields/DurationField';
|
||||
import { NumberField } from 'components/Form/Fields/NumberField';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { DEVICE_TYPES } from 'constants/deviceTypes';
|
||||
import { useGetDeviceTypes } from 'hooks/Network/Firmware';
|
||||
import {
|
||||
Simulation,
|
||||
useGetSimulationStatus,
|
||||
useGetSimulationsStatus,
|
||||
useStartSimulation,
|
||||
useStopSimulation,
|
||||
useUpdateSimulation,
|
||||
@@ -36,8 +36,8 @@ type Props = {
|
||||
const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen: isEditing, onToggle: onToggleEditing, onClose: stopEditing } = useDisclosure();
|
||||
const getDeviceTypes = useGetDeviceTypes();
|
||||
const deviceTypes = getDeviceTypes.data?.deviceTypes ?? DEVICE_TYPES;
|
||||
const { form, formRef } = useFormRef();
|
||||
const [formKey, setFormKey] = React.useState(uuid());
|
||||
const updateSim = useUpdateSimulation();
|
||||
@@ -46,10 +46,12 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
operationType: 'update',
|
||||
onClose: () => {},
|
||||
});
|
||||
const getStatus = useGetSimulationStatus();
|
||||
const getStatus = useGetSimulationsStatus();
|
||||
const startSim = useStartSimulation();
|
||||
const stopSim = useStopSimulation();
|
||||
|
||||
const currentSimulationStatus = getStatus.data?.find(({ simulationId }) => simulationId === simulation.id);
|
||||
|
||||
const handleStartClick = () =>
|
||||
startSim.mutate(
|
||||
{ id: simulation.id },
|
||||
@@ -82,7 +84,7 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
);
|
||||
const handleStopClick = () =>
|
||||
stopSim.mutate(
|
||||
{ id: getStatus.data?.id ?? '' },
|
||||
{ simulationId: simulation.id, runId: currentSimulationStatus?.id ?? '' },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
@@ -113,8 +115,13 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
|
||||
React.useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
stopEditing();
|
||||
}, [modalProps.isOpen, simulation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isEditing]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalProps.isOpen}
|
||||
@@ -125,15 +132,22 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
hidden={!isEditing}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
{getStatus.data?.simulationId === simulation.id && getStatus.data.state === 'running' ? (
|
||||
<ToggleEditButton
|
||||
toggleEdit={onToggleEditing}
|
||||
isEditing={isEditing}
|
||||
isDisabled={currentSimulationStatus?.state === 'running'}
|
||||
/>
|
||||
{currentSimulationStatus && currentSimulationStatus.state === 'running' ? (
|
||||
<Tooltip hasArrow label={t('simulation.stop')} placement="top">
|
||||
<IconButton
|
||||
aria-label={t('simulation.stop')}
|
||||
colorScheme="yellow"
|
||||
icon={<Stop size={20} />}
|
||||
onClick={handleStopClick}
|
||||
isDisabled={isEditing}
|
||||
isLoading={stopSim.isLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -145,6 +159,7 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
icon={<Play size={20} />}
|
||||
onClick={handleStartClick}
|
||||
isLoading={startSim.isLoading}
|
||||
isDisabled={isEditing}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -156,7 +171,7 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
innerRef={formRef as React.Ref<FormikProps<object>>}
|
||||
key={formKey}
|
||||
initialValues={simulation}
|
||||
validationSchema={SimulationSchema(t, deviceTypes[0])}
|
||||
validationSchema={SimulationSchema(t, getDeviceTypes.data?.deviceTypes?.[0])}
|
||||
onSubmit={async (data, { setSubmitting, resetForm }) =>
|
||||
updateSim.mutateAsync(
|
||||
{ ...(data as Partial<Simulation>), id: simulation.id },
|
||||
@@ -178,11 +193,23 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
<Form>
|
||||
<Box>
|
||||
<SimpleGrid minChildWidth="200px" spacing={2} mb={2}>
|
||||
<StringField name="name" label={t('common.name')} isRequired />
|
||||
<StringField name="gateway" label={t('simulation.controller')} isRequired />
|
||||
<NumberField name="threads" label={t('simulation.threads')} w="100px" isRequired />
|
||||
<StringField name="name" label={t('common.name')} isRequired isDisabled={!isEditing} />
|
||||
<StringField name="gateway" label={t('simulation.controller')} isRequired isDisabled={!isEditing} />
|
||||
<NumberField
|
||||
name="threads"
|
||||
label={t('simulation.threads')}
|
||||
w="100px"
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<DurationField name="simulationLength" label={t('simulation.duration')} isRequired unit="s" />
|
||||
<DurationField
|
||||
name="simulationLength"
|
||||
label={t('simulation.duration')}
|
||||
isRequired
|
||||
unit="s"
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
<Heading size="sm" mt={4}>
|
||||
{t('devices.title')}
|
||||
</Heading>
|
||||
@@ -191,37 +218,75 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
name="deviceType"
|
||||
label={t('common.type')}
|
||||
options={
|
||||
deviceTypes.map((v) => ({
|
||||
getDeviceTypes.data?.deviceTypes?.map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
})) ?? []
|
||||
}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
<StringField
|
||||
name="macPrefix"
|
||||
label={t('simulation.mac_prefix')}
|
||||
w="110px"
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
<NumberField name="devices" label={t('devices.title')} w="100px" isRequired isDisabled={!isEditing} />
|
||||
<NumberField
|
||||
name="concurrentDevices"
|
||||
w="100px"
|
||||
label={t('simulation.concurrent_devices')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
<StringField name="macPrefix" label={t('simulation.mac_prefix')} w="110px" isRequired />
|
||||
<NumberField name="devices" label={t('devices.title')} w="100px" isRequired />
|
||||
<NumberField name="concurrentDevices" w="100px" label={t('simulation.concurrent_devices')} isRequired />
|
||||
</SimpleGrid>
|
||||
<Heading size="sm" mt={4}>
|
||||
{t('configurations.advanced_settings')}
|
||||
</Heading>
|
||||
<Flex my={2}>
|
||||
<Box mr={2} w="160px">
|
||||
<NumberField name="minAssociations" label={t('simulation.min_associations')} isRequired />
|
||||
<NumberField
|
||||
name="minAssociations"
|
||||
label={t('simulation.min_associations')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
<Box w="160px">
|
||||
<NumberField name="maxAssociations" label={t('simulation.max_associations')} isRequired />
|
||||
<NumberField
|
||||
name="maxAssociations"
|
||||
label={t('simulation.max_associations')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex my={2}>
|
||||
<Box mr={2} w="160px">
|
||||
<NumberField name="minClients" label={t('simulation.min_clients')} isRequired />
|
||||
<NumberField
|
||||
name="minClients"
|
||||
label={t('simulation.min_clients')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
<Box mr={2} w="160px">
|
||||
<NumberField name="maxClients" label={t('simulation.max_clients')} isRequired />
|
||||
<NumberField
|
||||
name="maxClients"
|
||||
label={t('simulation.max_clients')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
<Box w="160px">
|
||||
<NumberField name="clientInterval" label={t('simulation.client_interval')} isRequired />
|
||||
<NumberField
|
||||
name="clientInterval"
|
||||
label={t('simulation.client_interval')}
|
||||
isRequired
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex my={2}>
|
||||
@@ -231,10 +296,17 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
label={t('simulation.healthcheck_interval')}
|
||||
isRequired
|
||||
unit="s"
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
<Box mr={2} w="160px">
|
||||
<NumberField name="stateInterval" label={t('simulation.state_interval')} isRequired unit="s" />
|
||||
<NumberField
|
||||
name="stateInterval"
|
||||
label={t('simulation.state_interval')}
|
||||
isRequired
|
||||
unit="s"
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
<Box w="160px">
|
||||
<NumberField
|
||||
@@ -242,11 +314,18 @@ const UpdateSimulationModal = ({ modalProps, simulation }: Props) => {
|
||||
label={t('simulation.reconnect_interval')}
|
||||
isRequired
|
||||
unit="s"
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box w="160px" my={2}>
|
||||
<NumberField name="keepAlive" label={t('simulation.keep_alive')} isRequired unit="s" />
|
||||
<NumberField
|
||||
name="keepAlive"
|
||||
label={t('simulation.keep_alive')}
|
||||
isRequired
|
||||
unit="s"
|
||||
isDisabled={!isEditing}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Form>
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Flex, VStack } from '@chakra-ui/react';
|
||||
import { VStack } from '@chakra-ui/react';
|
||||
import StatusCard from './StatusCard';
|
||||
import SimulationsTable from './Table';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const SimulationsPage = () => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
<VStack spacing={4}>
|
||||
{isUserLoaded && (
|
||||
<>
|
||||
<StatusCard />
|
||||
<SimulationsTable />
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
const SimulationsPage = () => (
|
||||
<VStack spacing={4}>
|
||||
<>
|
||||
<StatusCard />
|
||||
<SimulationsTable />
|
||||
</>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
export default SimulationsPage;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as Yup from 'yup';
|
||||
import { testMac } from 'helpers/formTests';
|
||||
|
||||
export const SimulationSchema = (t: (str: string) => string, defaultType?: string) =>
|
||||
Yup.object().shape({
|
||||
@@ -6,6 +7,7 @@ export const SimulationSchema = (t: (str: string) => string, defaultType?: strin
|
||||
gateway: Yup.string().required(t('form.required')).default('https://your-controller-api-endpoint.your_domain.com'),
|
||||
macPrefix: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('macPrefix-valid-mac', t('simulation.mac_prefix_length'), (val) => testMac(`${val}112233`))
|
||||
.test('macPrefix-length', t('simulation.mac_prefix_length'), (val) => val?.length === 6)
|
||||
.default('AABBCC'),
|
||||
healthCheckInterval: Yup.number().required(t('form.required')).moreThan(29).lessThan(6001).integer().default(60),
|
||||
|
||||
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
139
src/pages/SystemPage/SystemSecrets/Actions.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import { CopyIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Center,
|
||||
Box,
|
||||
Button,
|
||||
useDisclosure,
|
||||
HStack,
|
||||
Text,
|
||||
useClipboard,
|
||||
} from '@chakra-ui/react';
|
||||
import { Eye, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import EditSecretButton from './EditButton';
|
||||
import { Secret, useDeleteSystemSecret } from 'hooks/Network/Secrets';
|
||||
|
||||
interface Props {
|
||||
secret: Secret;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
const SystemSecretActions = ({ secret, isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const deleteSecret = useDeleteSystemSecret();
|
||||
const { hasCopied, onCopy } = useClipboard(secret.value);
|
||||
|
||||
const handleDeleteClick = React.useCallback(() => {
|
||||
deleteSecret.mutate(secret.key, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HStack mx="auto">
|
||||
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="delete-device"
|
||||
colorScheme="red"
|
||||
icon={<Trash size={20} />}
|
||||
size="sm"
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="340px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.delete')} {secret.key}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text whiteSpace="break-spaces">{t('crud.delete_confirm', { obj: t('system.secrets_one') })}</Text>
|
||||
</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteSecret.isLoading}>
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Tooltip
|
||||
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
|
||||
hasArrow
|
||||
closeOnClick={false}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={5} w={5} />}
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
colorScheme="teal"
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<EditSecretButton secret={secret} />
|
||||
<Popover>
|
||||
<Tooltip label={`${t('common.view')} ${t('system.secrets_one')}`} hasArrow closeOnClick={false}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label={t('common.view')} icon={<Eye size={20} />} size="sm" colorScheme="purple" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="560px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('common.view')} {secret.key}
|
||||
<Tooltip
|
||||
label={hasCopied ? `${t('common.copied')}!` : `${t('common.copy')} ${t('system.secrets_one')}`}
|
||||
hasArrow
|
||||
closeOnClick={false}
|
||||
>
|
||||
<IconButton
|
||||
aria-label={t('common.copy')}
|
||||
icon={<CopyIcon h={4} w={4} />}
|
||||
onClick={onCopy}
|
||||
size="xs"
|
||||
colorScheme="teal"
|
||||
ml={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text whiteSpace="break-spaces">
|
||||
<Center>
|
||||
<pre style={{ fontFamily: 'monospace' }}>{secret.value}</pre>
|
||||
</Center>
|
||||
</Text>
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSecretActions;
|
||||
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
118
src/pages/SystemPage/SystemSecrets/CreateButton.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Input,
|
||||
Textarea,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CreateButton } from '../../../components/Buttons/CreateButton';
|
||||
import { SaveButton } from '../../../components/Buttons/SaveButton';
|
||||
import { Modal } from '../../../components/Modals/Modal';
|
||||
import { useCreateSystemSecret } from 'hooks/Network/Secrets';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FORM_VALUES: FormValues = {
|
||||
key: '',
|
||||
value: '',
|
||||
};
|
||||
|
||||
const SystemSecretCreateButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [form, setForm] = React.useState<FormValues>(DEFAULT_FORM_VALUES);
|
||||
const [isNameChanged, setIsNameChanged] = React.useState(false);
|
||||
const [isValueChanged, setIsValueChanged] = React.useState(false);
|
||||
const create = useCreateSystemSecret();
|
||||
|
||||
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, key: e.target.value });
|
||||
if (!isNameChanged) setIsNameChanged(true);
|
||||
};
|
||||
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm({ ...form, value: e.target.value });
|
||||
if (!isValueChanged) setIsValueChanged(true);
|
||||
};
|
||||
|
||||
const isNameError = form.key.length === 0;
|
||||
const isValueError = form.value.length === 0;
|
||||
|
||||
const onSubmit = () => {
|
||||
create.mutate(form, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: 'create-system-secret-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_update_obj', {
|
||||
obj: t('system.secrets_one'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: 'create-system-secret-error',
|
||||
title: t('common.error'),
|
||||
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenClick = () => {
|
||||
setIsNameChanged(false);
|
||||
setIsValueChanged(false);
|
||||
setForm(DEFAULT_FORM_VALUES);
|
||||
onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateButton onClick={handleOpenClick} isCompact />
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t('system.secrets_create')}
|
||||
topRightButtons={
|
||||
<SaveButton onClick={onSubmit} isDisabled={isNameError || isValueError} isLoading={create.isLoading} />
|
||||
}
|
||||
options={{
|
||||
modalSize: 'sm',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<Input value={form.key} onChange={onKeyChange} />
|
||||
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
|
||||
<FormLabel>{t('common.value')}</FormLabel>
|
||||
<Textarea value={form.value} onChange={onValueChange} rows={2} />
|
||||
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSecretCreateButton;
|
||||
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
146
src/pages/SystemPage/SystemSecrets/EditButton.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
IconButton,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverBody,
|
||||
PopoverCloseButton,
|
||||
PopoverContent,
|
||||
PopoverFooter,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
Text,
|
||||
Textarea,
|
||||
Tooltip,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { Pencil } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Secret, useUpdateSystemSecret } from 'hooks/Network/Secrets';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
type FormValues = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
secret: Secret;
|
||||
};
|
||||
|
||||
const EditSecretButton = ({ secret }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const [form, setForm] = React.useState<FormValues>({
|
||||
key: secret.key,
|
||||
value: secret.value,
|
||||
});
|
||||
const [isNameChanged, setIsNameChanged] = React.useState(false);
|
||||
const [isValueChanged, setIsValueChanged] = React.useState(false);
|
||||
const update = useUpdateSystemSecret();
|
||||
|
||||
const onKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setForm({ ...form, key: e.target.value });
|
||||
if (!isNameChanged) setIsNameChanged(true);
|
||||
};
|
||||
const onValueChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setForm({ ...form, value: e.target.value });
|
||||
if (!isValueChanged) setIsValueChanged(true);
|
||||
};
|
||||
|
||||
const isNameError = form.key.length === 0;
|
||||
const isValueError = form.value.length === 0;
|
||||
|
||||
const onSubmit = () => {
|
||||
update.mutate(form, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
id: 'create-system-secret-success',
|
||||
title: t('common.success'),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: 'create-system-secret-error',
|
||||
title: t('common.error'),
|
||||
description: (e as AxiosError)?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenClick = () => {
|
||||
setIsNameChanged(false);
|
||||
setIsValueChanged(false);
|
||||
setForm({
|
||||
key: secret.key,
|
||||
value: secret.value,
|
||||
});
|
||||
onOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover isOpen={isOpen} onOpen={handleOpenClick} onClose={onClose}>
|
||||
<Tooltip hasArrow label={t('crud.edit')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton aria-label="delete-device" colorScheme="blue" icon={<Pencil size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<PopoverContent w="340px">
|
||||
<PopoverArrow />
|
||||
<PopoverCloseButton />
|
||||
<PopoverHeader>
|
||||
{t('crud.edit')} {secret.key}
|
||||
</PopoverHeader>
|
||||
<PopoverBody>
|
||||
<Text whiteSpace="break-spaces">
|
||||
<Box>
|
||||
<FormControl mb={2} isInvalid={isNameError && isNameChanged}>
|
||||
<FormLabel>{t('common.name')}</FormLabel>
|
||||
<Input value={form.key} onChange={onKeyChange} />
|
||||
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl mb={2} isInvalid={isValueError && isValueChanged}>
|
||||
<FormLabel>{t('common.value')}</FormLabel>
|
||||
<Textarea value={form.value} onChange={onValueChange} rows={2} />
|
||||
<FormErrorMessage>{t('form.required')}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Text>
|
||||
</PopoverBody>
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="blue" ml="1" onClick={onSubmit} isLoading={update.isLoading}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSecretButton;
|
||||
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
70
src/pages/SystemPage/SystemSecrets/Table.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||
import SystemSecretActions from './Actions';
|
||||
import { Secret, useGetAllSystemSecrets, useGetSystemSecretsDictionary } from 'hooks/Network/Secrets';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
const SystemSecretsTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const getSecrets = useGetAllSystemSecrets();
|
||||
const getDictionary = useGetSystemSecretsDictionary();
|
||||
|
||||
const descriptionCell = React.useCallback(
|
||||
(secret: Secret) => {
|
||||
if (!getDictionary.data) return '-';
|
||||
|
||||
return getDictionary.data.find((d) => d.key === secret.key)?.description ?? '-';
|
||||
},
|
||||
[getDictionary.data],
|
||||
);
|
||||
|
||||
const actionCell = React.useCallback((secret: Secret) => <SystemSecretActions secret={secret} />, []);
|
||||
|
||||
const columns = React.useMemo(
|
||||
(): Column<Secret>[] => [
|
||||
{
|
||||
id: 'key',
|
||||
Header: t('common.name'),
|
||||
Footer: '',
|
||||
accessor: 'key',
|
||||
alwaysShow: true,
|
||||
customWidth: '120px',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
Header: t('common.description'),
|
||||
Footer: '',
|
||||
Cell: ({ cell }) => descriptionCell(cell.row.original),
|
||||
accessor: 'description',
|
||||
hasPopover: true,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
Cell: (v) => actionCell(v.cell.row.original),
|
||||
disableSortBy: true,
|
||||
customWidth: '120px',
|
||||
alwaysShow: true,
|
||||
},
|
||||
],
|
||||
[t, descriptionCell],
|
||||
);
|
||||
return (
|
||||
<Box w="100%">
|
||||
<DataTable
|
||||
columns={columns as Column<object>[]}
|
||||
saveSettingsId="system.secrets.table"
|
||||
data={getSecrets.data ?? []}
|
||||
obj={t('keys.other')}
|
||||
sortBy={[{ id: 'key', desc: false }]}
|
||||
showAllRows
|
||||
hideControls
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemSecretsTable;
|
||||
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
53
src/pages/SystemPage/SystemSecrets/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
Box,
|
||||
EffectProps,
|
||||
Heading,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemSecretCreateButton from './CreateButton';
|
||||
import SystemSecretsTable from './Table';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
export interface SystemSecretsCardProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps {}
|
||||
|
||||
export const SystemSecretsCard = ({ ...props }: SystemSecretsCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user || user.userRole !== 'root') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box px={4} py={4}>
|
||||
<Card variant="widget" {...props}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('system.secrets')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<SystemSecretCreateButton />
|
||||
</CardHeader>
|
||||
<CardBody p={4}>
|
||||
<SystemSecretsTable />
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { FloppyDisk } from 'phosphor-react';
|
||||
import { FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||
import { Modal } from 'components/Modals/Modal';
|
||||
import { useGetSystemLogLevelNames, useGetSystemLogLevels, useUpdateSystemLogLevels } from 'hooks/Network/System';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, useDisclosure } from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||
import { Article } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SystemLoggingModal from './Modal';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
@@ -15,9 +16,17 @@ const SystemLoggingButton = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button colorScheme="teal" onClick={modalProps.onOpen} mr={2} my="auto">
|
||||
{t('system.logging')}
|
||||
</Button>
|
||||
<Tooltip label={t('system.logging')} hasArrow>
|
||||
<IconButton
|
||||
aria-label={t('system.logging')}
|
||||
colorScheme="teal"
|
||||
type="button"
|
||||
my="auto"
|
||||
onClick={modalProps.onOpen}
|
||||
icon={<Article size={20} />}
|
||||
mr={2}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SystemLoggingModal modalProps={modalProps} endpoint={endpoint.uri} token={token} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DataTable } from 'components/DataTables/DataTable';
|
||||
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||
import { compactDate } from 'helpers/dateFormatting';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
|
||||
@@ -17,13 +17,14 @@ import {
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { MultiValue, Select } from 'chakra-react-select';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import { ArrowsClockwise } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
|
||||
import SystemLoggingButton from './LoggingButton';
|
||||
import SystemCertificatesTable from './SystemCertificatesTable';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { compactSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
import { EndpointApiResponse } from 'hooks/Network/Endpoints';
|
||||
import { useGetSubsystems, useGetSystemInfo, useReloadSubsystems } from 'hooks/Network/System';
|
||||
@@ -65,21 +66,12 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<Card variant="widget">
|
||||
<Box display="flex" mb={2}>
|
||||
<Heading pt={0}>{endpoint.type}</Heading>
|
||||
<Spacer />
|
||||
<SystemLoggingButton endpoint={endpoint} token={token} />
|
||||
<Button
|
||||
mt={1}
|
||||
minWidth="112px"
|
||||
colorScheme="gray"
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
onClick={refresh}
|
||||
isLoading={isFetchingSystem || isFetchingSubsystems}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<RefreshButton onClick={refresh} isFetching={isFetchingSystem || isFetchingSubsystems} />
|
||||
</Box>
|
||||
<CardBody>
|
||||
<VStack w="100%">
|
||||
@@ -179,7 +171,7 @@ const SystemTile = ({ endpoint, token }: Props) => {
|
||||
ml={2}
|
||||
onClick={handleReloadClick}
|
||||
icon={<ArrowsClockwise size={20} />}
|
||||
colorScheme="gray"
|
||||
colorScheme="blue"
|
||||
isLoading={isReloading}
|
||||
isDisabled={subs.length === 0}
|
||||
/>
|
||||
|
||||
@@ -1,27 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Flex, Heading, SimpleGrid, Spacer } from '@chakra-ui/react';
|
||||
import { Box, SimpleGrid, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { RefreshButton } from '../../components/Buttons/RefreshButton';
|
||||
import { SystemSecretsCard } from './SystemSecrets';
|
||||
import SystemTile from './SystemTile';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useGetEndpoints } from 'hooks/Network/Endpoints';
|
||||
|
||||
const SystemPage = () => {
|
||||
const getDefaultTabIndex = () => {
|
||||
const index = localStorage.getItem('system-tab-index') || '0';
|
||||
try {
|
||||
return parseInt(index, 10);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
type Props = {
|
||||
isOnlySec?: boolean;
|
||||
};
|
||||
|
||||
const SystemPage = ({ isOnlySec }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { token, isUserLoaded } = useAuth();
|
||||
const { token, user } = useAuth();
|
||||
const { data: endpoints, refetch, isFetching } = useGetEndpoints({ onSuccess: () => {} });
|
||||
const [tabIndex, setTabIndex] = React.useState(getDefaultTabIndex());
|
||||
const handleTabChange = (index: number) => {
|
||||
setTabIndex(index);
|
||||
localStorage.setItem('system-tab-index', index.toString());
|
||||
};
|
||||
|
||||
const isRoot = user && user.userRole === 'root';
|
||||
|
||||
const endpointsList = React.useMemo(() => {
|
||||
if (!endpoints || !token || !isUserLoaded) return null;
|
||||
if (!token || (!isOnlySec && !endpoints)) return null;
|
||||
|
||||
const endpointList = [...endpoints];
|
||||
const endpointList = endpoints ? [...endpoints] : [];
|
||||
endpointList.push({
|
||||
uri: axiosSec.defaults.baseURL?.split('/api/v1')[0] ?? '',
|
||||
type: 'owsec',
|
||||
type: isOnlySec ? '' : 'owsec',
|
||||
id: 0,
|
||||
vendor: 'owsec',
|
||||
authenticationType: '',
|
||||
@@ -34,25 +54,54 @@ const SystemPage = () => {
|
||||
return 0;
|
||||
})
|
||||
.map((endpoint) => <SystemTile key={uuid()} endpoint={endpoint} token={token} />);
|
||||
}, [endpoints, token, isUserLoaded]);
|
||||
|
||||
if (!isUserLoaded) return null;
|
||||
}, [endpoints, token]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
<Card mb={4} py={2} px={4}>
|
||||
<CardHeader>
|
||||
<Heading size="md" my="auto">
|
||||
{t('controller.firmware.endpoints')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<SimpleGrid minChildWidth="500px" spacing="20px" mb={3}>
|
||||
{endpointsList}
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
<Card p={0}>
|
||||
<Tabs index={tabIndex} onChange={handleTabChange} variant="enclosed" isLazy>
|
||||
<TabList>
|
||||
<CardHeader>
|
||||
<Tab>{t('system.services')}</Tab>
|
||||
<Tab hidden={!isRoot}>{t('system.configuration')}</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"
|
||||
>
|
||||
{!isOnlySec && (
|
||||
<CardHeader px={4} pt={4}>
|
||||
<Spacer />
|
||||
<RefreshButton onClick={refetch} isFetching={isFetching} />
|
||||
</CardHeader>
|
||||
)}
|
||||
<SimpleGrid minChildWidth="500px" spacing="20px" p={4}>
|
||||
{endpointsList}
|
||||
</SimpleGrid>
|
||||
</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"
|
||||
>
|
||||
<SystemSecretsCard />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemPage;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip, useToast } from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Wrench } from 'phosphor-react';
|
||||
import { Wrench } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useSendUserEmailValidation, useSuspendUser, useResetMfa, useResetPassword } from 'hooks/Network/Users';
|
||||
import { useResetMfa, useResetPassword, useSendUserEmailValidation, useSuspendUser } from 'hooks/Network/Users';
|
||||
import { useMutationResult } from 'hooks/useMutationResult';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
isSuspended: boolean;
|
||||
isWaitingForCheck: boolean;
|
||||
refresh: () => void;
|
||||
isDisabled?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refresh }) => {
|
||||
const UserActions = ({ id, isSuspended, isWaitingForCheck, refresh, size = 'sm', isDisabled }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { mutateAsync: sendValidation } = useSendUserEmailValidation({ id, refresh });
|
||||
@@ -32,7 +34,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
onSuccess();
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
const handleResetMfaClick = () =>
|
||||
@@ -49,7 +51,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -67,7 +69,7 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e)) onError(e);
|
||||
onError(e as AxiosError);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -75,10 +77,17 @@ const UserActions: React.FC<Props> = ({ id, isSuspended, isWaitingForCheck, refr
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('commands.other')}>
|
||||
<MenuButton as={IconButton} aria-label="Commands" icon={<Wrench size={20} />} size="sm" ml={2} />
|
||||
<Tooltip label={t('common.actions')}>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
icon={<Wrench size={20} />}
|
||||
size={size}
|
||||
ml={2}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</Tooltip>
|
||||
<MenuList>
|
||||
<MenuList fontSize="md">
|
||||
<MenuItem onClick={handleSuspendClick}>
|
||||
{isSuspended ? t('users.reactivate_user') : t('users.suspend')}
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ToggleField } from 'components/Form/Fields/ToggleField';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testRegex } from 'helpers/formTests';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
|
||||
const propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
createUser: PropTypes.instanceOf(Object).isRequired,
|
||||
refreshUsers: PropTypes.func.isRequired,
|
||||
formRef: PropTypes.instanceOf(Object).isRequired,
|
||||
};
|
||||
|
||||
const CreateUserSchema = (t, { passRegex }) =>
|
||||
Yup.object().shape({
|
||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||
name: Yup.string().required('Required'),
|
||||
description: Yup.string(),
|
||||
currentPassword: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
|
||||
.default(''),
|
||||
note: Yup.string(),
|
||||
userRole: Yup.string(),
|
||||
});
|
||||
const CreateUserNonRootSchema = (t, { passRegex }) =>
|
||||
Yup.object().shape({
|
||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||
name: Yup.string().required('Required'),
|
||||
description: Yup.string(),
|
||||
currentPassword: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex))
|
||||
.default(''),
|
||||
note: Yup.string(),
|
||||
userRole: Yup.string(),
|
||||
});
|
||||
|
||||
const CreateUserForm = ({ isOpen, onClose, createUser, refreshUsers, formRef }) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
||||
|
||||
const createParameters = ({
|
||||
name,
|
||||
description,
|
||||
email,
|
||||
currentPassword,
|
||||
note,
|
||||
userRole,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
}) => {
|
||||
if (userRole === 'root') {
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
currentPassword,
|
||||
userRole,
|
||||
description: description.length > 0 ? description : undefined,
|
||||
notes: note.length > 0 ? [{ note }] : undefined,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
currentPassword,
|
||||
userRole,
|
||||
description: description.length > 0 ? description : undefined,
|
||||
notes: note.length > 0 ? [{ note }] : undefined,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
key={formKey}
|
||||
initialValues={{
|
||||
name: '',
|
||||
description: '',
|
||||
email: '',
|
||||
currentPassword: '',
|
||||
note: '',
|
||||
userRole: user.userRole === 'admin' ? 'csr' : user.userRole,
|
||||
changePassword: true,
|
||||
emailValidation: true,
|
||||
}}
|
||||
validationSchema={
|
||||
user?.userRole === 'root'
|
||||
? CreateUserSchema(t, { passRegex: passwordPattern })
|
||||
: CreateUserNonRootSchema(t, { passRegex: passwordPattern })
|
||||
}
|
||||
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
||||
createUser.mutateAsync(createParameters(formData), {
|
||||
onSuccess: () => {
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
toast({
|
||||
id: 'user-creation-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_create_obj', {
|
||||
obj: t('user.title'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
refreshUsers();
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_create_obj', {
|
||||
obj: t('user.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{({ errors, touched }) => (
|
||||
<Form>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<StringField name="email" label={t('common.email')} errors={errors} touched={touched} isRequired />
|
||||
<StringField name="name" label={t('common.name')} errors={errors} touched={touched} isRequired />
|
||||
<SelectField
|
||||
name="userRole"
|
||||
label={t('user.role')}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
options={[
|
||||
{ value: 'accounting', label: 'Accounting' },
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'csr', label: 'CSR' },
|
||||
{ value: 'installer', label: 'Installer' },
|
||||
{ value: 'noc', label: 'NOC' },
|
||||
{ value: 'root', label: 'Root' },
|
||||
{ value: 'system', label: 'System' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
<StringField
|
||||
name="currentPassword"
|
||||
label={t('user.password')}
|
||||
errors={errors}
|
||||
touched={touched}
|
||||
isRequired
|
||||
hideButton
|
||||
/>
|
||||
<ToggleField name="changePassword" label={t('users.change_password')} errors={errors} touched={touched} />
|
||||
<ToggleField name="emailValidation" label={t('users.email_validation')} errors={errors} touched={touched} />
|
||||
<StringField name="description" label={t('common.description')} errors={errors} touched={touched} />
|
||||
<StringField name="note" label={t('common.note')} errors={errors} touched={touched} />
|
||||
</SimpleGrid>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
|
||||
<Box w="100%">
|
||||
<Link href={passwordPolicyLink} isExternal>
|
||||
{t('login.password_policy')}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
CreateUserForm.propTypes = propTypes;
|
||||
|
||||
export default CreateUserForm;
|
||||
197
src/pages/UsersPage/Table/CreateUserModal/Form.tsx
Normal file
197
src/pages/UsersPage/Table/CreateUserModal/Form.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, Link, useToast, SimpleGrid } from '@chakra-ui/react';
|
||||
import axios from 'axios';
|
||||
import { Formik, Form, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import { SelectField } from '../../../../components/Form/Fields/SelectField';
|
||||
import { StringField } from '../../../../components/Form/Fields/StringField';
|
||||
import { ToggleField } from '../../../../components/Form/Fields/ToggleField';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testRegex } from 'helpers/formTests';
|
||||
import { useCreateUser } from 'hooks/Network/Users';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
|
||||
export type CreateUserFormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
email: string;
|
||||
currentPassword: string;
|
||||
note: string;
|
||||
userRole: string;
|
||||
emailValidation: boolean;
|
||||
changePassword: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
formRef: React.Ref<FormikProps<CreateUserFormValues>>;
|
||||
};
|
||||
|
||||
const CreateUserForm = ({ isOpen, onClose, formRef }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
const createUser = useCreateUser();
|
||||
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
||||
|
||||
const CreateUserSchema = Yup.object().shape({
|
||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||
name: Yup.string().required('Required'),
|
||||
description: Yup.string(),
|
||||
currentPassword: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
|
||||
.default(''),
|
||||
note: Yup.string(),
|
||||
userRole: Yup.string(),
|
||||
});
|
||||
const CreateUserNonRootSchema = Yup.object().shape({
|
||||
email: Yup.string().email(t('form.invalid_email')).required('Required'),
|
||||
name: Yup.string().required('Required'),
|
||||
description: Yup.string(),
|
||||
currentPassword: Yup.string()
|
||||
.required(t('form.required'))
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern))
|
||||
.default(''),
|
||||
note: Yup.string(),
|
||||
userRole: Yup.string(),
|
||||
});
|
||||
|
||||
const createParameters = ({
|
||||
name,
|
||||
description,
|
||||
email,
|
||||
currentPassword,
|
||||
note,
|
||||
userRole,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
}: CreateUserFormValues) => {
|
||||
if (userRole === 'root') {
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
currentPassword,
|
||||
userRole,
|
||||
description: description.length > 0 ? description : undefined,
|
||||
notes: note.length > 0 ? [{ note }] : undefined,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
currentPassword,
|
||||
userRole,
|
||||
description: description.length > 0 ? description : undefined,
|
||||
notes: note.length > 0 ? [{ note }] : undefined,
|
||||
emailValidation,
|
||||
changePassword,
|
||||
};
|
||||
};
|
||||
|
||||
const defaultRole = () => {
|
||||
if (user?.userRole === 'admin') return 'csr';
|
||||
if (user) return user.userRole;
|
||||
return 'csr';
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
key={formKey}
|
||||
initialValues={
|
||||
{
|
||||
name: '',
|
||||
description: '',
|
||||
email: '',
|
||||
currentPassword: '',
|
||||
note: '',
|
||||
userRole: defaultRole(),
|
||||
changePassword: true,
|
||||
emailValidation: true,
|
||||
} as CreateUserFormValues
|
||||
}
|
||||
validationSchema={user?.userRole === 'root' ? CreateUserSchema : CreateUserNonRootSchema}
|
||||
onSubmit={(formData, { setSubmitting, resetForm }) =>
|
||||
createUser.mutate(createParameters(formData), {
|
||||
onSuccess: () => {
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
toast({
|
||||
id: 'user-creation-success',
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_create_obj', {
|
||||
obj: t('user.title'),
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
setSubmitting(false);
|
||||
if (axios.isAxiosError(e))
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<Form>
|
||||
<SimpleGrid minChildWidth="300px" spacing="20px">
|
||||
<StringField name="email" label={t('common.email')} isRequired />
|
||||
<StringField name="name" label={t('common.name')} isRequired />
|
||||
<SelectField
|
||||
name="userRole"
|
||||
label={t('user.role')}
|
||||
options={[
|
||||
{ value: 'accounting', label: 'Accounting' },
|
||||
{ value: 'admin', label: 'Admin' },
|
||||
{ value: 'csr', label: 'CSR' },
|
||||
{ value: 'installer', label: 'Installer' },
|
||||
{ value: 'noc', label: 'NOC' },
|
||||
{ value: 'root', label: 'Root' },
|
||||
{ value: 'system', label: 'System' },
|
||||
]}
|
||||
isRequired
|
||||
/>
|
||||
<StringField name="currentPassword" label={t('user.password')} isRequired hideButton />
|
||||
<ToggleField name="changePassword" label={t('users.change_password')} />
|
||||
<ToggleField name="emailValidation" label={t('users.email_validation')} />
|
||||
<StringField name="description" label={t('common.description')} />
|
||||
<StringField name="note" label={t('common.note')} />
|
||||
</SimpleGrid>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" mt={4} mb={6}>
|
||||
<Box w="100%">
|
||||
<Link href={passwordPolicyLink} isExternal>
|
||||
{t('login.password_policy')}
|
||||
<ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUserForm;
|
||||
@@ -1,96 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { Button, useDisclosure, Modal, ModalOverlay, ModalContent, ModalBody } from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CreateUserForm from './Form';
|
||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
const propTypes = {
|
||||
requirements: PropTypes.shape({
|
||||
accessPolicy: PropTypes.string,
|
||||
passwordPolicy: PropTypes.string,
|
||||
}),
|
||||
refreshUsers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
requirements: {
|
||||
accessPolicy: '',
|
||||
passwordPolicy: '',
|
||||
},
|
||||
};
|
||||
|
||||
const CreateUserModal = ({ requirements, refreshUsers }) => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef();
|
||||
const createUser = useMutation((newUser) =>
|
||||
axiosSec.post(`user/0${newUser.emailValidation ? '?email_verification=true' : ''}`, newUser),
|
||||
);
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
hidden={user?.userRole === 'CSR'}
|
||||
alignItems="center"
|
||||
colorScheme="blue"
|
||||
rightIcon={<AddIcon />}
|
||||
onClick={onOpen}
|
||||
ml={2}
|
||||
>
|
||||
{t('crud.create')}
|
||||
</Button>
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
<ModalHeader
|
||||
title={t('crud.create_object', { obj: t('user.title') })}
|
||||
right={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
<CloseButton ml={2} onClick={closeModal} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
<CreateUserForm
|
||||
requirements={requirements}
|
||||
createUser={createUser}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refreshUsers={refreshUsers}
|
||||
formRef={formRef}
|
||||
/>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CreateUserModal.propTypes = propTypes;
|
||||
CreateUserModal.defaultProps = defaultProps;
|
||||
|
||||
export default CreateUserModal;
|
||||
48
src/pages/UsersPage/Table/CreateUserModal/index.tsx
Normal file
48
src/pages/UsersPage/Table/CreateUserModal/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as React from 'react';
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import CreateUserForm, { CreateUserFormValues } from './Form';
|
||||
import { CreateButton } from 'components/Buttons/CreateButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
const CreateUserModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef<CreateUserFormValues>();
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{user?.userRole === 'CSR' ? null : <CreateButton onClick={onOpen} ml={2} />}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
title={t('crud.create_object', { obj: t('user.title') })}
|
||||
topRightButtons={
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!form.isValid || !form.dirty}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<CreateUserForm isOpen={isOpen} onClose={onClose} formRef={formRef} />
|
||||
</Modal>
|
||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUserModal;
|
||||
@@ -1,85 +1,83 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, Link, useToast, Tabs, TabList, TabPanels, TabPanel, Tab, SimpleGrid } from '@chakra-ui/react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import PropTypes from 'prop-types';
|
||||
import axios from 'axios';
|
||||
import { Formik, Form, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as Yup from 'yup';
|
||||
import { NotesField } from 'components/Form/Fields/NotesField';
|
||||
import { SelectField } from 'components/Form/Fields/SelectField';
|
||||
import { StringField } from 'components/Form/Fields/StringField';
|
||||
import { ToggleField } from 'components/Form/Fields/ToggleField';
|
||||
import { NotesField } from '../../../../components/Form/Fields/NotesField';
|
||||
import { SelectField } from '../../../../components/Form/Fields/SelectField';
|
||||
import { StringField } from '../../../../components/Form/Fields/StringField';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { testObjectName, testRegex } from 'helpers/formTests';
|
||||
import { User, useUpdateUser } from 'hooks/Network/Users';
|
||||
import { useApiRequirements } from 'hooks/useApiRequirements';
|
||||
|
||||
const UpdateUserSchema = (t, { passRegex }) =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
|
||||
currentPassword: Yup.string()
|
||||
.notRequired()
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passRegex)),
|
||||
description: Yup.string(),
|
||||
mfa: Yup.string(),
|
||||
phoneNumber: Yup.string(),
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
editing: PropTypes.bool.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
updateUser: PropTypes.instanceOf(Object).isRequired,
|
||||
refreshUsers: PropTypes.func.isRequired,
|
||||
userToUpdate: PropTypes.shape({
|
||||
email: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
currentPassword: PropTypes.string.isRequired,
|
||||
userRole: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
formRef: PropTypes.instanceOf(Object).isRequired,
|
||||
type Props = {
|
||||
editing: boolean;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedUser: User;
|
||||
formRef: React.Ref<FormikProps<User>>;
|
||||
};
|
||||
|
||||
const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, userToUpdate, formRef }) => {
|
||||
const UpdateUserForm = ({ editing, isOpen, onClose, selectedUser, formRef }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [formKey, setFormKey] = useState(uuid());
|
||||
const { passwordPolicyLink, passwordPattern } = useApiRequirements();
|
||||
const updateUser = useUpdateUser();
|
||||
|
||||
const UpdateUserSchema = () =>
|
||||
Yup.object().shape({
|
||||
name: Yup.string().required(t('form.required')).test('len', t('common.name_error'), testObjectName),
|
||||
currentPassword: Yup.string()
|
||||
.notRequired()
|
||||
.test('test-password', t('form.invalid_password'), (v) => testRegex(v, passwordPattern)),
|
||||
description: Yup.string(),
|
||||
mfa: Yup.string(),
|
||||
phoneNumber: Yup.string(),
|
||||
});
|
||||
|
||||
const formIsDisabled = () => {
|
||||
if (!editing) return true;
|
||||
if (user?.userRole === 'root') return false;
|
||||
if (user?.userRole === 'partner') return false;
|
||||
if (user?.userRole === 'admin') {
|
||||
if (userToUpdate.userRole === 'partner' || userToUpdate.userRole === 'admin') return true;
|
||||
if (selectedUser.userRole === 'root' || selectedUser.userRole === 'partner' || selectedUser.userRole === 'admin')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const canEditRole = () => {
|
||||
if (selectedUser.userRole === 'root') return false;
|
||||
if (user?.userRole === 'root') return true;
|
||||
if (user?.userRole === 'admin' && selectedUser.userRole !== 'admin') return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFormKey(uuid());
|
||||
}, [isOpen]);
|
||||
}, [isOpen, editing]);
|
||||
|
||||
return (
|
||||
<Formik
|
||||
innerRef={formRef}
|
||||
enableReinitialize
|
||||
key={formKey}
|
||||
initialValues={userToUpdate}
|
||||
validationSchema={UpdateUserSchema(t, { passRegex: passwordPattern })}
|
||||
onSubmit={(
|
||||
{ name, description, currentPassword, userRole, notes, changePassword },
|
||||
{ setSubmitting, resetForm },
|
||||
) =>
|
||||
initialValues={selectedUser}
|
||||
validationSchema={UpdateUserSchema}
|
||||
onSubmit={({ name, description, currentPassword, userRole, notes }, { setSubmitting, resetForm }) =>
|
||||
updateUser.mutateAsync(
|
||||
{
|
||||
id: selectedUser.id,
|
||||
name,
|
||||
currentPassword: currentPassword.length > 0 ? currentPassword : undefined,
|
||||
userRole,
|
||||
changePassword,
|
||||
description,
|
||||
notes: notes.filter((note) => note.isNew),
|
||||
},
|
||||
@@ -98,23 +96,20 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
refreshUsers();
|
||||
onClose();
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_update_obj', {
|
||||
obj: t('user.title'),
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
setSubmitting(false);
|
||||
if (axios.isAxiosError(e))
|
||||
toast({
|
||||
id: uuid(),
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -144,10 +139,9 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
{ value: 'system', label: 'System' },
|
||||
]}
|
||||
isRequired
|
||||
isDisabled
|
||||
isDisabled={!canEditRole() || formIsDisabled()}
|
||||
/>
|
||||
<StringField name="name" label={t('common.name')} isDisabled={formIsDisabled()} isRequired />
|
||||
<ToggleField name="changePassword" label={t('users.change_password')} isDisabled={formIsDisabled()} />
|
||||
<StringField
|
||||
name="currentPassword"
|
||||
label={t('user.password')}
|
||||
@@ -176,6 +170,4 @@ const UpdateUserForm = ({ editing, isOpen, onClose, updateUser, refreshUsers, us
|
||||
);
|
||||
};
|
||||
|
||||
UpdateUserForm.propTypes = propTypes;
|
||||
|
||||
export default UpdateUserForm;
|
||||
@@ -1,111 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
useToast,
|
||||
Spinner,
|
||||
Center,
|
||||
useDisclosure,
|
||||
useBoolean,
|
||||
} from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import UpdateUserForm from './Form';
|
||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
||||
import { EditButton } from 'components/Buttons/EditButton';
|
||||
import { SaveButton } from 'components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from 'components/Modals/ConfirmCloseAlert';
|
||||
import { ModalHeader } from 'components/Modals/GenericModal/ModalHeader';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { useGetUser } from 'hooks/Network/Users';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
const propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
userId: PropTypes.string,
|
||||
requirements: PropTypes.shape({
|
||||
accessPolicy: PropTypes.string,
|
||||
passwordPolicy: PropTypes.string,
|
||||
}),
|
||||
refreshUsers: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
userId: '',
|
||||
requirements: {
|
||||
accessPolicy: '',
|
||||
passwordPolicy: '',
|
||||
},
|
||||
};
|
||||
|
||||
const EditUserModal = ({ isOpen, onClose, userId, requirements, refreshUsers }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useBoolean();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const toast = useToast();
|
||||
const { form, formRef } = useFormRef();
|
||||
const canFetchUser = userId !== '' && isOpen;
|
||||
const { data: user, isLoading } = useGetUser({ t, toast, id: userId, enabled: canFetchUser });
|
||||
const createUser = useMutation((userInfo) => axiosSec.put(`user/${userId}`, userInfo));
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditing.off();
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||
<ModalOverlay />
|
||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||
<ModalHeader
|
||||
title={t('crud.edit_obj', { obj: t('user.title') })}
|
||||
right={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
/>
|
||||
<EditButton ml={2} isDisabled={editing} onClick={setEditing.toggle} isCompact />
|
||||
<CloseButton ml={2} onClick={closeModal} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ModalBody>
|
||||
{!isLoading && user ? (
|
||||
<UpdateUserForm
|
||||
editing={editing}
|
||||
userToUpdate={user}
|
||||
requirements={requirements}
|
||||
updateUser={createUser}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
refreshUsers={refreshUsers}
|
||||
formRef={formRef}
|
||||
/>
|
||||
) : (
|
||||
<Center>
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
EditUserModal.propTypes = propTypes;
|
||||
EditUserModal.defaultProps = defaultProps;
|
||||
|
||||
export default EditUserModal;
|
||||
101
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
101
src/pages/UsersPage/Table/EditUserModal/index.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Spinner, Center, useDisclosure, useBoolean, Tag } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../../../components/Buttons/SaveButton';
|
||||
import { ConfirmCloseAlertModal } from '../../../../components/Modals/ConfirmCloseAlert';
|
||||
import { Modal } from '../../../../components/Modals/Modal';
|
||||
import ActionsDropdown from '../ActionsDropdown';
|
||||
import UpdateUserForm from './Form';
|
||||
import { ToggleEditButton } from 'components/Buttons/ToggleEditButton';
|
||||
import { useGetUser, User } from 'hooks/Network/Users';
|
||||
import { useFormRef } from 'hooks/useFormRef';
|
||||
|
||||
type Props = {
|
||||
userId?: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const EditUserModal = ({ isOpen, onClose, userId }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [editing, setEditing] = useBoolean();
|
||||
const queryClient = useQueryClient();
|
||||
const { isOpen: showConfirm, onOpen: openConfirm, onClose: closeConfirm } = useDisclosure();
|
||||
const { form, formRef } = useFormRef<User>();
|
||||
const canFetchUser = userId !== '' && isOpen;
|
||||
const { data: user, isFetching, refetch } = useGetUser({ id: userId ?? '', enabled: canFetchUser });
|
||||
|
||||
const closeModal = () => (form.dirty ? openConfirm() : onClose());
|
||||
|
||||
const closeCancelAndForm = () => {
|
||||
closeConfirm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
refetch();
|
||||
queryClient.invalidateQueries(['users']);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setEditing.off();
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={closeModal}
|
||||
title={user?.name ?? t('crud.edit_obj', { obj: t('user.title') })}
|
||||
tags={
|
||||
<>
|
||||
{user?.suspended ? (
|
||||
<Tag colorScheme="yellow" size="lg">
|
||||
{t('user.suspended')}
|
||||
</Tag>
|
||||
) : null}
|
||||
{user?.waitingForEmailCheck ? (
|
||||
<Tag colorScheme="blue" size="lg">
|
||||
{t('user.email_not_validated')}
|
||||
</Tag>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
topRightButtons={
|
||||
<>
|
||||
<SaveButton
|
||||
onClick={form.submitForm}
|
||||
isLoading={form.isSubmitting}
|
||||
isDisabled={!editing || !form.isValid || !form.dirty}
|
||||
hidden={!editing}
|
||||
/>
|
||||
<ToggleEditButton ml={2} isEditing={editing} toggleEdit={setEditing.toggle} isDirty={form.dirty} />
|
||||
{user ? (
|
||||
<ActionsDropdown
|
||||
id={user?.id}
|
||||
isSuspended={user?.suspended}
|
||||
isWaitingForCheck={user?.waitingForEmailCheck}
|
||||
refresh={refresh}
|
||||
size="md"
|
||||
isDisabled={editing}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{!isFetching && user ? (
|
||||
<UpdateUserForm editing={editing} selectedUser={user} isOpen={isOpen} onClose={onClose} formRef={formRef} />
|
||||
) : (
|
||||
<Center>
|
||||
<Spinner />
|
||||
</Center>
|
||||
)}
|
||||
</Modal>
|
||||
<ConfirmCloseAlertModal isOpen={showConfirm} confirm={closeCancelAndForm} cancel={closeConfirm} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditUserModal;
|
||||
@@ -17,67 +17,56 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import axios from 'axios';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ActionsDropdown from './ActionsDropdown';
|
||||
import { axiosSec } from 'constants/axiosInstances';
|
||||
import { useDeleteUser, User } from 'hooks/Network/Users';
|
||||
|
||||
const deleteUserApi = async (userId) => axiosSec.delete(`/user/${userId}`).then(() => true);
|
||||
|
||||
const propTypes = {
|
||||
cell: PropTypes.shape({
|
||||
original: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
suspended: PropTypes.bool.isRequired,
|
||||
waitingForEmailCheck: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
}).isRequired,
|
||||
refreshTable: PropTypes.func.isRequired,
|
||||
openEdit: PropTypes.func.isRequired,
|
||||
type Props = {
|
||||
user: User;
|
||||
openEdit: (user: User) => void;
|
||||
refreshTable: () => void;
|
||||
};
|
||||
|
||||
const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
const UserActions = ({ user, openEdit, refreshTable }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const deleteUser = useMutation(() => deleteUserApi(user.id), {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
refreshTable();
|
||||
toast({
|
||||
id: `user-delete-success${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_delete_obj', {
|
||||
obj: user.name,
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
toast({
|
||||
id: 'user-delete-error',
|
||||
title: t('common.error'),
|
||||
description: t('crud.error_delete_obj', {
|
||||
obj: user.name,
|
||||
e: e?.response?.data?.ErrorDescription,
|
||||
}),
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
const deleteUser = useDeleteUser();
|
||||
|
||||
const handleDeleteClick = () => deleteUser.mutateAsync();
|
||||
const handleEditClick = () => openEdit(user.id);
|
||||
const handleDeleteClick = () =>
|
||||
deleteUser.mutate(user.id, {
|
||||
onSuccess: () => {
|
||||
onClose();
|
||||
toast({
|
||||
id: `user-delete-success${uuid()}`,
|
||||
title: t('common.success'),
|
||||
description: t('crud.success_delete_obj', {
|
||||
obj: user.name,
|
||||
}),
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
onError: (e) => {
|
||||
if (axios.isAxiosError(e))
|
||||
toast({
|
||||
id: 'user-delete-error',
|
||||
title: t('common.error'),
|
||||
description: e?.response?.data?.ErrorDescription,
|
||||
status: 'error',
|
||||
duration: 5000,
|
||||
isClosable: true,
|
||||
position: 'top-right',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleEditClick = () => openEdit(user);
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
@@ -85,7 +74,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
<Tooltip hasArrow label={t('crud.delete')} placement="top" isDisabled={isOpen}>
|
||||
<Box>
|
||||
<PopoverTrigger>
|
||||
<IconButton colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
<IconButton aria-label={t('crud.delete')} colorScheme="red" icon={<Trash size={20} />} size="sm" />
|
||||
</PopoverTrigger>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -97,10 +86,10 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
<PopoverFooter>
|
||||
<Center>
|
||||
<Button colorScheme="gray" mr="1" onClick={onClose}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button colorScheme="red" ml="1" onClick={handleDeleteClick} isLoading={deleteUser.isLoading}>
|
||||
Yes
|
||||
{t('common.yes')}
|
||||
</Button>
|
||||
</Center>
|
||||
</PopoverFooter>
|
||||
@@ -114,6 +103,7 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
/>
|
||||
<Tooltip hasArrow label={t('common.view_details')} placement="top">
|
||||
<IconButton
|
||||
aria-label={t('common.view_details')}
|
||||
ml={2}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={20} />}
|
||||
@@ -125,6 +115,4 @@ const UserActions = ({ cell: { original: user }, refreshTable, openEdit }) => {
|
||||
);
|
||||
};
|
||||
|
||||
UserActions.propTypes = propTypes;
|
||||
|
||||
export default UserActions;
|
||||
@@ -1,67 +1,55 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Avatar, Box, Button, Flex, Heading, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { ArrowsClockwise } from 'phosphor-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Avatar, Box, Flex, useDisclosure } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ColumnPicker } from '../../../components/DataTables/ColumnPicker';
|
||||
import { DataTable } from '../../../components/DataTables/DataTable';
|
||||
import FormattedDate from '../../../components/InformationDisplays/FormattedDate';
|
||||
import CreateUserModal from './CreateUserModal';
|
||||
import EditUserModal from './EditUserModal';
|
||||
import UserActions from './UserActions';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
||||
import { DataTable } from 'components/DataTables/DataTable';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { useGetRequirements } from 'hooks/Network/Requirements';
|
||||
import { useGetUsers } from 'hooks/Network/Users';
|
||||
import { Column } from 'models/Table';
|
||||
import { User } from 'models/User';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
title: null,
|
||||
};
|
||||
|
||||
const UserTable = ({ title }) => {
|
||||
const UserTable = () => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
const [usersWithAvatars, setUsersWithAvatars] = useState([]);
|
||||
const { data: requirements } = useGetRequirements();
|
||||
const [editId, setEditId] = useState('');
|
||||
const [hiddenColumns, setHiddenColumns] = useState([]);
|
||||
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||
const { isOpen: editOpen, onOpen: openEdit, onClose: closeEdit } = useDisclosure();
|
||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers({ t, toast, setUsersWithAvatars });
|
||||
const { data: users, refetch: refreshUsers, isFetching } = useGetUsers();
|
||||
|
||||
const openEditModal = (userId) => {
|
||||
setEditId(userId);
|
||||
const openEditModal = React.useCallback((editUser: User) => {
|
||||
setEditId(editUser.id);
|
||||
openEdit();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const memoizedActions = useCallback(
|
||||
(cell) => <UserActions cell={cell.row} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />,
|
||||
(userActions: User) => (
|
||||
<UserActions user={userActions} refreshTable={refreshUsers} key={uuid()} openEdit={openEditModal} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
const memoizedDate = useCallback((cell) => <FormattedDate date={cell.row.values.lastLogin} key={uuid()} />, []);
|
||||
const memoizedDate = useCallback((date: number) => <FormattedDate date={date} key={uuid()} />, []);
|
||||
|
||||
const memoizedAvatar = useCallback(
|
||||
(cell) => <Avatar name={cell.row.values.name} src={cell.row.original.avatar} />,
|
||||
[],
|
||||
);
|
||||
const memoizedAvatar = useCallback((name: string, avatar: string) => <Avatar name={name} src={avatar} />, []);
|
||||
|
||||
// Columns array. This array contains your table headings and accessors which maps keys from data array
|
||||
const columns = React.useMemo(() => {
|
||||
const baseColumns = [
|
||||
const baseColumns: Column<User>[] = [
|
||||
{
|
||||
id: 'avatar',
|
||||
Header: t('account.avatar'),
|
||||
Footer: '',
|
||||
accessor: 'avatar',
|
||||
customWidth: '32px',
|
||||
Cell: ({ cell }) => memoizedAvatar(cell),
|
||||
Cell: ({ cell }) => memoizedAvatar(cell.row.original.name, cell.row.original.avatar),
|
||||
disableSortBy: true,
|
||||
alwaysShow: true,
|
||||
},
|
||||
@@ -97,7 +85,7 @@ const UserTable = ({ title }) => {
|
||||
Header: t('users.last_login'),
|
||||
Footer: '',
|
||||
accessor: 'lastLogin',
|
||||
Cell: ({ cell }) => memoizedDate(cell, 'lastLogin'),
|
||||
Cell: ({ cell }) => memoizedDate(cell.row.original.lastLogin),
|
||||
customMinWidth: '150px',
|
||||
customWidth: '150px',
|
||||
},
|
||||
@@ -111,12 +99,12 @@ const UserTable = ({ title }) => {
|
||||
];
|
||||
if (user?.userRole !== 'csr')
|
||||
baseColumns.push({
|
||||
id: 'user',
|
||||
id: 'actions',
|
||||
Header: t('common.actions'),
|
||||
Footer: '',
|
||||
accessor: 'Id',
|
||||
customWidth: '80px',
|
||||
Cell: ({ cell }) => memoizedActions(cell),
|
||||
Cell: ({ cell }) => memoizedActions(cell.row.original),
|
||||
disableSortBy: true,
|
||||
alwaysShow: true,
|
||||
});
|
||||
@@ -124,63 +112,41 @@ const UserTable = ({ title }) => {
|
||||
return baseColumns;
|
||||
}, [t, user]);
|
||||
|
||||
const showUsers = () => {
|
||||
if (usersWithAvatars.length > 0) return usersWithAvatars;
|
||||
return users ?? [];
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader mb="10px">
|
||||
<Box>
|
||||
<Heading size="md">{title}</Heading>
|
||||
</Box>
|
||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
||||
<Box ms="auto">
|
||||
<ColumnPicker
|
||||
columns={columns}
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="provisioning.userTable.hiddenColumns"
|
||||
/>
|
||||
<CreateUserModal requirements={requirements} refreshUsers={refreshUsers} />
|
||||
<Button
|
||||
colorScheme="gray"
|
||||
onClick={refreshUsers}
|
||||
rightIcon={<ArrowsClockwise />}
|
||||
ml={2}
|
||||
isLoading={isFetching}
|
||||
>
|
||||
{t('common.refresh')}
|
||||
</Button>
|
||||
<CreateUserModal />
|
||||
<RefreshButton onClick={refreshUsers} isFetching={isFetching} ml={2} />
|
||||
</Box>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<Box overflowX="auto" w="100%">
|
||||
<DataTable
|
||||
<DataTable<User>
|
||||
columns={columns}
|
||||
data={showUsers()}
|
||||
data={users ?? []}
|
||||
isLoading={isFetching}
|
||||
obj={t('users.title')}
|
||||
sortBy={[{ id: 'email', desc: false }]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
fullScreen
|
||||
onRowClick={openEditModal}
|
||||
/>
|
||||
</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<EditUserModal
|
||||
isOpen={editOpen}
|
||||
onClose={closeEdit}
|
||||
userId={editId}
|
||||
requirements={requirements}
|
||||
refreshUsers={refreshUsers}
|
||||
/>
|
||||
<EditUserModal isOpen={editOpen} onClose={closeEdit} userId={editId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
UserTable.propTypes = propTypes;
|
||||
UserTable.defaultProps = defaultProps;
|
||||
export default UserTable;
|
||||
@@ -1,16 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import UserTable from './Table';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
const UsersPage = () => {
|
||||
const { isUserLoaded } = useAuth();
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" pt="75px">
|
||||
{isUserLoaded && <UserTable />}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
const UsersPage = () => <UserTable />;
|
||||
|
||||
export default UsersPage;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { Cloud, Info, UsersThree } from 'phosphor-react';
|
||||
import { Cloud, Info, ListBullets, UsersThree } from '@phosphor-icons/react';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
const NotificationsPage = React.lazy(() => import('pages/Notifications'));
|
||||
const SimulationsPage = React.lazy(() => import('pages/Simulations'));
|
||||
const ProfilePage = React.lazy(() => import('pages/Profile'));
|
||||
const SystemPage = React.lazy(() => import('pages/SystemPage'));
|
||||
@@ -28,6 +29,15 @@ const routes: Route[] = [
|
||||
),
|
||||
component: ProfilePage,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
authorized: ['root', 'partner', 'admin', 'csr', 'system'],
|
||||
path: '/users',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
||||
import { extendTheme, Tooltip, type ThemeConfig } from '@chakra-ui/react';
|
||||
import CardComponent from './additions/card/Card';
|
||||
import CardBodyComponent from './additions/card/CardBody';
|
||||
import CardHeaderComponent from './additions/card/CardHeader';
|
||||
@@ -37,4 +37,6 @@ const theme = extendTheme({
|
||||
},
|
||||
});
|
||||
|
||||
Tooltip.defaultProps = { ...Tooltip.defaultProps, hasArrow: true };
|
||||
|
||||
export default theme;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
"target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
@@ -28,7 +28,7 @@
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
"module": "ES2022" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
||||
"baseUrl": "src" /* Specify the base directory to resolve non-relative module names. */,
|
||||
|
||||
@@ -1,49 +1,9 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
/* other options */
|
||||
},
|
||||
manifest: {
|
||||
name: 'OpenWiFi Simulator App',
|
||||
short_name: 'OpenWiFiSimulator',
|
||||
description: 'OpenWiFi Simulator App',
|
||||
theme_color: '#000000',
|
||||
icons: [
|
||||
{
|
||||
src: 'android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-384x384.png',
|
||||
sizes: '384x384',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: 'android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [tsconfigPaths(), react()],
|
||||
build: {
|
||||
outDir: './build',
|
||||
chunkSizeWarningLimit: 1000,
|
||||
|
||||
Reference in New Issue
Block a user