From 21452d091f83b3f6c89e06fb4fd6abb4fc5a7a80 Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 1 Dec 2022 16:12:30 +0000 Subject: [PATCH] [WIFI-11542] AP Scripts Signed-off-by: Charles --- .eslintrc | 3 +- package-lock.json | 4 +- package.json | 2 +- public/locales/de/translation.json | 28 ++- public/locales/en/translation.json | 28 ++- public/locales/es/translation.json | 28 ++- public/locales/fr/translation.json | 28 ++- public/locales/pt/translation.json | 28 ++- .../Buttons/DeviceActionDropdown/index.tsx | 53 ++-- .../Buttons/RefreshButton/index.tsx | 3 +- src/components/Containers/IconBox/index.tsx | 6 +- .../DataTables/ColumnPicker/index.tsx | 12 +- .../Form/Fields/SignatureField/index.tsx | 71 ++++++ .../Modals/FirmwareUpgradeModal/index.tsx | 37 ++- src/components/Modals/ScriptModal/Form.tsx | 229 ++++++++++++++++++ .../Modals/ScriptModal/LockedView.tsx | 41 ++++ .../Modals/ScriptModal/ResultDisplay.tsx | 41 ++++ .../Modals/ScriptModal/ScripFile.tsx | 107 ++++++++ .../Modals/ScriptModal/UploadField.tsx | 72 ++++++ .../Modals/ScriptModal/WhenField.tsx | 73 ++++++ src/components/Modals/ScriptModal/index.tsx | 227 +++++++++++++++++ .../Modals/ScriptModal/useScriptModal.tsx | 30 +++ src/constants/colors.ts | 1 - src/helpers/stringHelper.ts | 1 - src/hooks/Network/Commands.ts | 112 ++++++++- src/hooks/Network/Firmware.ts | 4 +- src/hooks/Network/Inventory.ts | 1 - src/hooks/Network/Requirements.ts | 1 - src/hooks/Network/Scripts.ts | 101 ++++++++ src/hooks/Network/Statistics.ts | 1 - src/hooks/useApiRequirements.ts | 1 - src/hooks/useEndpointStatus.ts | 1 - src/hooks/useFastField.ts | 2 +- src/hooks/useFormRef.ts | 1 - src/layout/Containers/PanelContainer.tsx | 14 -- src/layout/Containers/PanelContent.tsx | 14 -- src/layout/{Sidebar => }/Devices.tsx | 0 src/layout/MainPanel.tsx | 18 -- src/layout/Navbar/index.tsx | 220 ++++++++--------- src/layout/PageContainer/index.tsx | 48 ++++ src/layout/Sidebar/CreateLinks.tsx | 15 -- src/layout/Sidebar/NavLinkButton.tsx | 84 +++---- src/layout/Sidebar/index.tsx | 104 ++++---- src/layout/index.tsx | 84 ++++--- src/models/Device.ts | 1 + src/models/Routes.ts | 1 + src/models/User.ts | 13 +- src/pages/DefaultConfigurations/index.tsx | 11 +- src/pages/DefaultConfigurations/utils.ts | 1 - .../Device/LogsCard/CommandHistory/Modal.tsx | 176 -------------- .../ResultModal/DownloadScriptButton.tsx | 33 +++ .../ResultModal/DownloadTraceButton.tsx | 30 +++ .../ResultModal/DownloadWifiScanButton.tsx | 67 +++++ .../CommandHistory/ResultModal/index.tsx | 179 ++++++++++++++ .../Device/LogsCard/CommandHistory/index.tsx | 4 +- .../Device/StatisticsCard/InterfaceChart.tsx | 28 ++- .../StatisticsCard/useStatisticsCard.ts | 1 - src/pages/Device/index.tsx | 9 +- src/pages/Devices/ListCard/Actions.tsx | 4 + src/pages/Devices/ListCard/index.tsx | 5 + src/pages/Devices/Logs/index.tsx | 112 --------- src/pages/Devices/index.tsx | 120 ++++----- src/pages/Firmware/index.tsx | 80 +++--- src/pages/NotFound/index.tsx | 8 +- src/pages/Notifications/index.tsx | 132 +++++----- src/pages/Profile/index.tsx | 18 +- src/pages/Scripts/TableCard/Actions.tsx | 92 +++++++ src/pages/Scripts/TableCard/CreateButton.tsx | 207 ++++++++++++++++ src/pages/Scripts/TableCard/RolesInput.tsx | 76 ++++++ src/pages/Scripts/TableCard/ScripFile.tsx | 125 ++++++++++ src/pages/Scripts/TableCard/UploadField.tsx | 92 +++++++ src/pages/Scripts/TableCard/index.tsx | 128 ++++++++++ .../Scripts/TableCard/useScriptsTable.ts | 14 ++ src/pages/Scripts/ViewScriptCard/Form.tsx | 156 ++++++++++++ src/pages/Scripts/ViewScriptCard/index.tsx | 162 +++++++++++++ src/pages/Scripts/index.tsx | 33 +++ src/pages/SystemPage/index.tsx | 14 +- src/pages/UsersPage/index.tsx | 12 +- src/router/routes.tsx | 12 +- tsconfig.json | 4 +- 80 files changed, 3202 insertions(+), 937 deletions(-) create mode 100644 src/components/Form/Fields/SignatureField/index.tsx create mode 100644 src/components/Modals/ScriptModal/Form.tsx create mode 100644 src/components/Modals/ScriptModal/LockedView.tsx create mode 100644 src/components/Modals/ScriptModal/ResultDisplay.tsx create mode 100644 src/components/Modals/ScriptModal/ScripFile.tsx create mode 100644 src/components/Modals/ScriptModal/UploadField.tsx create mode 100644 src/components/Modals/ScriptModal/WhenField.tsx create mode 100644 src/components/Modals/ScriptModal/index.tsx create mode 100644 src/components/Modals/ScriptModal/useScriptModal.tsx create mode 100644 src/hooks/Network/Scripts.ts delete mode 100644 src/layout/Containers/PanelContainer.tsx delete mode 100644 src/layout/Containers/PanelContent.tsx rename src/layout/{Sidebar => }/Devices.tsx (100%) delete mode 100644 src/layout/MainPanel.tsx create mode 100644 src/layout/PageContainer/index.tsx delete mode 100644 src/layout/Sidebar/CreateLinks.tsx delete mode 100644 src/pages/Device/LogsCard/CommandHistory/Modal.tsx create mode 100644 src/pages/Device/LogsCard/CommandHistory/ResultModal/DownloadScriptButton.tsx create mode 100644 src/pages/Device/LogsCard/CommandHistory/ResultModal/DownloadTraceButton.tsx create mode 100644 src/pages/Device/LogsCard/CommandHistory/ResultModal/DownloadWifiScanButton.tsx create mode 100644 src/pages/Device/LogsCard/CommandHistory/ResultModal/index.tsx delete mode 100644 src/pages/Devices/Logs/index.tsx create mode 100644 src/pages/Scripts/TableCard/Actions.tsx create mode 100644 src/pages/Scripts/TableCard/CreateButton.tsx create mode 100644 src/pages/Scripts/TableCard/RolesInput.tsx create mode 100644 src/pages/Scripts/TableCard/ScripFile.tsx create mode 100644 src/pages/Scripts/TableCard/UploadField.tsx create mode 100644 src/pages/Scripts/TableCard/index.tsx create mode 100644 src/pages/Scripts/TableCard/useScriptsTable.ts create mode 100644 src/pages/Scripts/ViewScriptCard/Form.tsx create mode 100644 src/pages/Scripts/ViewScriptCard/index.tsx create mode 100644 src/pages/Scripts/index.tsx diff --git a/.eslintrc b/.eslintrc index 85105b6..5273cb8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,6 @@ "project": "./tsconfig.json" }, "ignorePatterns": ["build/", "dist/"], - "plugins": ["import", "react", "@typescript-eslint", "prettier"], "extends": [ "plugin:react/recommended", "plugin:@typescript-eslint/eslint-recommended", @@ -27,6 +26,7 @@ "plugin:import/warnings", "plugin:import/typescript" ], + "plugins": ["import", "react", "@typescript-eslint", "prettier"], "rules": { "import/extensions": [ "error", @@ -69,6 +69,7 @@ ], "max-len": ["error", { "code": 150 }], "@typescript-eslint/ban-ts-comment": ["off"], + "import/prefer-default-export": ["off"], "react/prop-types": ["warn"], "react/require-default-props": "off", "react/jsx-props-no-spreading": ["off"], diff --git a/package-lock.json b/package-lock.json index 279588c..267de03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ucentral-client", - "version": "2.8.0(28)", + "version": "2.8.0(31)", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ucentral-client", - "version": "2.8.0(28)", + "version": "2.8.0(31)", "license": "ISC", "dependencies": { "@chakra-ui/icons": "^2.0.11", diff --git a/package.json b/package.json index 708f361..77b8ac1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ucentral-client", - "version": "2.8.0(30)", + "version": "2.8.0(31)", "description": "", "private": true, "main": "index.tsx", diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 34a32e5..4454fa6 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -79,6 +79,7 @@ "live_view_help": "Hilfe zur Live-Ansicht", "memory": "Erinnerung", "memory_used": "Verwendeter Speicher", + "missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu", "mode": "Modus", "noise": "Lärm", "packets": "Pakete", @@ -370,7 +371,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", @@ -613,6 +614,7 @@ "import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note", "invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)", "new_devices": "Neue Geräte", + "no_model_image": "Kein Modellbild gefunden", "not_connected": "Nicht verbunden", "not_found_gateway": "Fehler: Gerät hat sich noch nicht mit dem Gateway verbunden", "notifications": "Gerätebenachrichtigungen", @@ -700,6 +702,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.", @@ -811,7 +815,7 @@ "level": "Niveau", "message": "Botschaft", "one": "Log", - "receiving_types": "Typen empfangen", + "receiving_types": "Benachrichtigungsfilter", "security": "Sicherheit", "source": "Quelle", "thread": "Faden", @@ -874,6 +878,10 @@ "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", @@ -901,18 +909,30 @@ "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": "Führen Sie das Geräteskript aus", + "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", - "upload_destination": "Ziel hochladen", + "update_success": "Skript aktualisiert!", + "upload_destination": "Ergebnis-Upload-Ziel", + "upload_file": "Datei hochladen", + "visit_external_website": "Dokumentation ansehen", "when": "Ausführung planen" }, "service": { diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 1b71a80..1e8bec5 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -79,6 +79,7 @@ "live_view_help": "Live View Help", "memory": "Memory", "memory_used": "Memory Used", + "missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu", "mode": "Mode", "noise": "Noise", "packets": "Packets", @@ -370,7 +371,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", @@ -613,6 +614,7 @@ "import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note", "invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)", "new_devices": "new devices", + "no_model_image": "No Model Image Found", "not_connected": "Not Connected", "not_found_gateway": "Error: device has not yet connected to the controller", "notifications": "Device Notifications", @@ -700,6 +702,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)", @@ -811,7 +815,7 @@ "level": "Level", "message": "Message", "one": "Log", - "receiving_types": "Receiving Types", + "receiving_types": "Notifications Filter", "security": "Security", "source": "Source", "thread": "Thread", @@ -874,6 +878,10 @@ "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", @@ -901,18 +909,30 @@ "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 Device Script", + "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", - "upload_destination": "Upload Destination", + "update_success": "Script updated!", + "upload_destination": "Results Upload Destination", + "upload_file": "Upload File", + "visit_external_website": "View Documentation", "when": "Schedule Execution" }, "service": { diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 7740c57..abccead 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -79,6 +79,7 @@ "live_view_help": "Ayuda de visualización en vivo", "memory": "Memoria", "memory_used": "Memoria usada", + "missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior", "mode": "Modo", "noise": "Ruido", "packets": "Paquetes", @@ -370,7 +371,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", @@ -613,6 +614,7 @@ "import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota", "invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)", "new_devices": "Nuevos dispositivos", + "no_model_image": "No se encontró ninguna imagen de modelo", "not_connected": "No conectado", "not_found_gateway": "Error: el dispositivo aún no se ha conectado a la puerta de enlace", "notifications": "notificaciones de dispositivos", @@ -700,6 +702,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)", @@ -811,7 +815,7 @@ "level": "Nivel", "message": "Mensaje", "one": "Iniciar sesión", - "receiving_types": "Tipos de recepción", + "receiving_types": "Filtro de notificaciones", "security": "SEGURIDAD", "source": "Fuente", "thread": "Hilo", @@ -874,6 +878,10 @@ "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", @@ -901,18 +909,30 @@ "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 secuencia de comandos del dispositivo", + "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", - "upload_destination": "Cargar destino", + "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": { diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index d07aba4..aeb9009 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -79,6 +79,7 @@ "live_view_help": "Aide sur l'affichage en direct", "memory": "mémoire", "memory_used": "Mémoire utilisée", + "missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut", "mode": "Mode", "noise": "Bruit", "packets": "Paquets", @@ -370,7 +371,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", @@ -613,6 +614,7 @@ "import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note", "invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)", "new_devices": "nouveaux appareils", + "no_model_image": "Aucune image de modèle trouvée", "not_connected": "Pas connecté", "not_found_gateway": "Erreur : l'appareil n'est pas encore connecté à la passerelle", "notifications": "notifications de l'appareil", @@ -700,6 +702,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)", @@ -811,7 +815,7 @@ "level": "Niveau", "message": "Message", "one": "Bûche", - "receiving_types": "Types de réception", + "receiving_types": "Filtre de notification", "security": "SÉCURITÉ", "source": "La source", "thread": "Fil de discussion", @@ -874,6 +878,10 @@ "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", @@ -901,18 +909,30 @@ "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": "Exécuter le script de périphérique", + "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", - "upload_destination": "Destination de téléchargement", + "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": { diff --git a/public/locales/pt/translation.json b/public/locales/pt/translation.json index 16c8740..82e8450 100644 --- a/public/locales/pt/translation.json +++ b/public/locales/pt/translation.json @@ -79,6 +79,7 @@ "live_view_help": "Ajuda da visualização ao vivo", "memory": "Memória", "memory_used": "Memória Usada", + "missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior", "mode": "Modo", "noise": "Barulho", "packets": "Pacotes", @@ -370,7 +371,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", @@ -613,6 +614,7 @@ "import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note", "invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)", "new_devices": "novos dispositivos", + "no_model_image": "Nenhuma imagem de modelo encontrada", "not_connected": "Não conectado", "not_found_gateway": "Erro: o dispositivo ainda não se conectou ao gateway", "notifications": "Notificações do dispositivo", @@ -700,6 +702,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)", @@ -811,7 +815,7 @@ "level": "Nível", "message": "mensagem", "one": "Registro", - "receiving_types": "Tipos de recebimento", + "receiving_types": "Filtro de notificações", "security": "SEGURANÇA", "source": "Fonte", "thread": "FIO", @@ -874,6 +878,10 @@ "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", @@ -901,18 +909,30 @@ "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 de dispositivo", + "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", - "upload_destination": "Carregar destino", + "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": { diff --git a/src/components/Buttons/DeviceActionDropdown/index.tsx b/src/components/Buttons/DeviceActionDropdown/index.tsx index 4fe793c..5564e84 100644 --- a/src/components/Buttons/DeviceActionDropdown/index.tsx +++ b/src/components/Buttons/DeviceActionDropdown/index.tsx @@ -1,5 +1,16 @@ import React from 'react'; -import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react'; +import { + Button, + IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, + Portal, + Spinner, + Tooltip, + useToast, +} from '@chakra-ui/react'; import axios from 'axios'; import { Wrench } from 'phosphor-react'; import { useTranslation } from 'react-i18next'; @@ -21,6 +32,7 @@ interface Props { onOpenEventQueue: (serialNumber: string) => void; onOpenConfigureModal: (serialNumber: string) => void; onOpenTelemetryModal: (serialNumber: string) => void; + onOpenScriptModal: (device: GatewayDevice) => void; size?: 'sm' | 'md' | 'lg'; } @@ -35,6 +47,7 @@ const DeviceActionDropdown = ({ onOpenEventQueue, onOpenTelemetryModal, onOpenConfigureModal, + onOpenScriptModal, size, }: Props) => { const { t } = useTranslation(); @@ -67,6 +80,7 @@ const DeviceActionDropdown = ({ const handleOpenQueue = () => onOpenEventQueue(device.serialNumber); const handleOpenConfigure = () => onOpenConfigureModal(device.serialNumber); const handleOpenTelemetry = () => onOpenTelemetryModal(device.serialNumber); + const handleOpenScript = () => onOpenScriptModal(device); const handleUpdateToLatest = () => { updateToLatest.mutate( { keepRedirector: true }, @@ -124,7 +138,7 @@ const DeviceActionDropdown = ({ }, ]); }, - onError: (e: unknown) => { + onError: (e) => { if (axios.isAxiosError(e)) { toast({ id: `upgrade-to-latest-error-${device.serialNumber}`, @@ -143,7 +157,7 @@ const DeviceActionDropdown = ({ const handleConnectClick = () => getRtty(); return ( - + {size === undefined ? ( )} - - {t('commands.blink')} - {t('controller.configure.title')} - {t('commands.connect')} - {t('controller.queue.title')} - {t('commands.factory_reset')} - {t('commands.firmware_upgrade')} - - {t('controller.telemetry.title')} - {t('controller.devices.trace')} - - {t('commands.wifiscan')} - + + + {t('commands.blink')} + {t('controller.configure.title')} + {t('commands.connect')} + {t('controller.queue.title')} + {t('commands.factory_reset')} + {t('commands.firmware_upgrade')} + + {t('controller.telemetry.title')} + {t('script.one')} + {t('controller.devices.trace')} + + {t('commands.wifiscan')} + + ); }; diff --git a/src/components/Buttons/RefreshButton/index.tsx b/src/components/Buttons/RefreshButton/index.tsx index 1f2bf3e..90dcf7d 100644 --- a/src/components/Buttons/RefreshButton/index.tsx +++ b/src/components/Buttons/RefreshButton/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react'; +import { Button, IconButton, ThemeTypings, Tooltip, useBreakpoint } from '@chakra-ui/react'; import { ArrowsClockwise } from 'phosphor-react'; import { useTranslation } from 'react-i18next'; @@ -10,6 +10,7 @@ export interface RefreshButtonProps { isCompact?: boolean; ml?: string | number; size?: 'sm' | 'md' | 'lg'; + colorScheme?: ThemeTypings['colorSchemes']; } const _RefreshButton: React.FC = ({ diff --git a/src/components/Containers/IconBox/index.tsx b/src/components/Containers/IconBox/index.tsx index 1d4c2b7..7a33385 100644 --- a/src/components/Containers/IconBox/index.tsx +++ b/src/components/Containers/IconBox/index.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { Flex } from '@chakra-ui/react'; +import { Flex, FlexProps } from '@chakra-ui/react'; -type Props = { +interface Props extends FlexProps { children: React.ReactNode; -}; +} const IconBox = ({ children, ...rest }: Props) => ( diff --git a/src/components/DataTables/ColumnPicker/index.tsx b/src/components/DataTables/ColumnPicker/index.tsx index a9797ed..6af99f6 100644 --- a/src/components/DataTables/ColumnPicker/index.tsx +++ b/src/components/DataTables/ColumnPicker/index.tsx @@ -12,9 +12,17 @@ export type ColumnPickerProps = { hiddenColumns: string[]; setHiddenColumns: (str: string[]) => void; size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; + isCompact?: boolean; }; -export const ColumnPicker = ({ preference, columns, hiddenColumns, setHiddenColumns, size }: ColumnPickerProps) => { +export const ColumnPicker = ({ + preference, + columns, + hiddenColumns, + setHiddenColumns, + size, + isCompact, +}: ColumnPickerProps) => { const { t } = useTranslation(); const { getPref, setPref } = useAuth(); const breakpoint = useBreakpoint(); @@ -32,7 +40,7 @@ export const ColumnPicker = ({ preference, columns, hiddenColumns, setHiddenColu setHiddenColumns(savedPrefs ? savedPrefs.split(',') : []); }, []); - if (breakpoint === 'base' || breakpoint === 'sm') { + if (isCompact || breakpoint === 'base' || breakpoint === 'sm') { return ( } /> diff --git a/src/components/Form/Fields/SignatureField/index.tsx b/src/components/Form/Fields/SignatureField/index.tsx new file mode 100644 index 0000000..1b3778f --- /dev/null +++ b/src/components/Form/Fields/SignatureField/index.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + Flex, + FormControl, + FormLabel, + InputGroup, + FormErrorMessage, + RadioGroup, + Stack, + Text, + Radio, + Input, +} from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { useFastField } from 'hooks/useFastField'; + +export type SignatureFieldProps = { + name: string; + isDisabled?: boolean; + controlProps?: React.ComponentProps<'div'>; +}; + +const _SignatureField = ({ name, isDisabled, controlProps }: SignatureFieldProps) => { + const { t } = useTranslation(); + const { value, error, isError, onChange } = useFastField({ name }); + + const onRadioChange = (v: string) => { + if (v === '0') onChange(undefined); + else onChange(''); + }; + + return ( + + + {t('script.signature')} + + + + + + {t('script.automatic')} + + + + + {t('common.custom')} + + + onChange(e.target.value)} + isDisabled={isDisabled || value === undefined} + onClick={(e) => { + (e.target as HTMLInputElement).select(); + e.stopPropagation(); + }} + w="100%" + _disabled={{ opacity: 0.8 }} + /> + + + + + + + {error} + + ); +}; + +export const SignatureField = React.memo(_SignatureField); diff --git a/src/components/Modals/FirmwareUpgradeModal/index.tsx b/src/components/Modals/FirmwareUpgradeModal/index.tsx index 5eff337..1aef64e 100644 --- a/src/components/Modals/FirmwareUpgradeModal/index.tsx +++ b/src/components/Modals/FirmwareUpgradeModal/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Ref, useRef } from 'react'; import { Modal, ModalOverlay, @@ -12,11 +12,14 @@ import { Switch, Heading, } from '@chakra-ui/react'; +import { Formik, FormikProps } from 'formik'; import { useTranslation } from 'react-i18next'; +import { v4 as uuid } from 'uuid'; import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand'; import FirmwareList from './FirmwareList'; import { CloseButton } from 'components/Buttons/CloseButton'; import { ModalHeader } from 'components/Containers/Modal/ModalHeader'; +import { SignatureField } from 'components/Form/Fields/SignatureField'; import { useGetDevice } from 'hooks/Network/Devices'; import { useGetAvailableFirmware, useUpdateDeviceFirmware } from 'hooks/Network/Firmware'; import useCommandModal from 'hooks/useCommandModal'; @@ -29,6 +32,13 @@ export type FirmwareUpgradeModalProps = { export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNumber }: FirmwareUpgradeModalProps) => { const { t } = useTranslation(); + const [formKey, setFormKey] = React.useState(uuid()); + const ref = useRef< + | FormikProps<{ + signature?: string | undefined; + }> + | undefined + >(); const [isRedirector, { toggle }] = useBoolean(false); const { data: device, isFetching: isFetchingDevice } = useGetDevice({ serialNumber, onClose }); const { data: firmware, isFetching: isFetchingFirmware } = useGetAvailableFirmware({ @@ -44,11 +54,19 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu }); const submit = (uri: string) => { - upgrade({ keepRedirector: isRedirector, uri }); + upgrade({ + keepRedirector: isRedirector, + uri, + signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined, + }); }; + React.useEffect(() => { + setFormKey(uuid()); + }, [isOpen]); + return ( - + + {device?.restrictedDevice && ( + + innerRef={ref as Ref> | undefined} + key={formKey} + enableReinitialize + initialValues={{ + signature: undefined, + }} + onSubmit={() => {}} + > + + + )} {firmware?.firmwares && ( )} diff --git a/src/components/Modals/ScriptModal/Form.tsx b/src/components/Modals/ScriptModal/Form.tsx new file mode 100644 index 0000000..11f88a2 --- /dev/null +++ b/src/components/Modals/ScriptModal/Form.tsx @@ -0,0 +1,229 @@ +import * as React from 'react'; +import { ExternalLinkIcon } from '@chakra-ui/icons'; +import { + Box, + Button, + Code, + Divider, + Flex, + FormControl, + FormLabel, + Heading, + Spacer, + Switch, + Tag, + Text, + useClipboard, +} from '@chakra-ui/react'; +import { Form, Formik, FormikProps } from 'formik'; +import { useTranslation } from 'react-i18next'; +import * as Yup from 'yup'; +import { NumberField } from '../../Form/Fields/NumberField'; +import { SelectField } from '../../Form/Fields/SelectField'; +import { ToggleField } from '../../Form/Fields/ToggleField'; +import ScriptFileInput from './ScripFile'; +import ScriptUploadField from './UploadField'; +import { SignatureField } from 'components/Form/Fields/SignatureField'; +import { DeviceScriptCommand } from 'hooks/Network/Commands'; +import { Script } from 'hooks/Network/Scripts'; +import { GatewayDevice } from 'models/Device'; + +const FormSchema = (t: (str: string) => string) => + Yup.object().shape({ + serialNumber: Yup.string().required(t('form.required')), + deferred: Yup.boolean().required(t('form.required')), + type: Yup.string().required(t('form.required')), + timeout: Yup.number().when('deferred', { + is: false, + then: Yup.number() + .required(t('form.required')) + .moreThan(10) + .lessThan(5 * 60), // 5 mins + }), + script: Yup.string().required(t('form.required')), + when: Yup.number().required(t('form.required')), + uri: Yup.string().min(1, t('form.required')), + signature: Yup.string(), + }); + +const DEFAULT_VALUES = (serialNumber: string) => + ({ + serialNumber, + deferred: true, + type: 'shell', + script: '', + when: 0, + } as DeviceScriptCommand); + +type Props = { + onStart: (data: DeviceScriptCommand) => Promise; + formRef: React.Ref>; + formKey: string; + areFieldsDisabled: boolean; + waitForResponse: boolean; + onToggleWaitForResponse: () => void; + script?: Script; + device?: GatewayDevice; + isDiagnostics?: boolean; +}; + +const CustomScriptForm = ({ + onStart, + formRef, + formKey, + areFieldsDisabled, + waitForResponse, + onToggleWaitForResponse, + device, + script, + isDiagnostics, +}: Props) => { + const { t } = useTranslation(); + const { hasCopied, onCopy, setValue } = useClipboard(script?.content ?? ''); + + React.useEffect(() => { + setValue(script?.content ?? ''); + }, [script?.content]); + + return ( + onStart(data)} + > + {(props: { setFieldValue: (k: string, v: unknown) => void; values: DeviceScriptCommand }) => ( +
+ + + {t('controller.trace.wait')} + + + + { + if (v) { + setTimeout(() => props.setFieldValue('timeout', undefined), 100); + } else { + setTimeout(() => props.setFieldValue('timeout', 120), 100); + } + }} + isDisabled={areFieldsDisabled || isDiagnostics} + /> + + + {!props.values.deferred && ( + + )} + + + + {!isDiagnostics && script && ( + <> + + + {script.name} + + + {script.type} + + + {script.author} + + {script.uri.length > 0 && ( + + )} + + + {script.description} + + + {t('script.upload_destination')}:{' '} + {script.defaultUploadURI === '' ? t('script.automatic') : script.defaultUploadURI} + + + )} + {!script && ( + + + + )} + {!isDiagnostics && ( + <> + + + {device?.restrictedDevice && } + + + + {script && ( + + + {t('script.one')} + + + + + )} + + {script ? ( + + {script.content.replace(/^\n/, '')} + + ) : ( + + )} + + + )} + + )} +
+ ); +}; + +export default CustomScriptForm; diff --git a/src/components/Modals/ScriptModal/LockedView.tsx b/src/components/Modals/ScriptModal/LockedView.tsx new file mode 100644 index 0000000..23edfe1 --- /dev/null +++ b/src/components/Modals/ScriptModal/LockedView.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Box, Code, Flex, Heading, Tag, Text } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { useGetAllDeviceScripts } from 'hooks/Network/Scripts'; + +type Props = { + id: string; +}; + +const ScriptLockedView = ({ id }: Props) => { + const { t } = useTranslation(); + const getScripts = useGetAllDeviceScripts(); + + const script = getScripts?.data?.find((curr) => curr.id === id); + + if (!script) return null; + + return ( + + + + {script.name} + + + {script.type} + + + + {script.description} + + + {t('common.by')}: {script.author} + + + {script.content.replace(/^\n/, '')} + + + ); +}; + +export default ScriptLockedView; diff --git a/src/components/Modals/ScriptModal/ResultDisplay.tsx b/src/components/Modals/ScriptModal/ResultDisplay.tsx new file mode 100644 index 0000000..92e5e12 --- /dev/null +++ b/src/components/Modals/ScriptModal/ResultDisplay.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Box, Button, Center, Code } from '@chakra-ui/react'; +import { useTranslation } from 'react-i18next'; +import { DeviceCommandHistory, useDownloadScriptResult } from 'hooks/Network/Commands'; + +type Props = { + result: DeviceCommandHistory; +}; + +const ScriptResultDisplay = ({ result }: Props) => { + const { t } = useTranslation(); + const download = useDownloadScriptResult({ serialNumber: result.serialNumber, commandId: result.UUID }); + + const onDownload = () => { + download.refetch(); + }; + + if (result.details?.uri !== undefined) { + return ( +
+ +
+ ); + } + return ( + + + {result.results?.status?.result ?? JSON.stringify(result.results, null, 2)} + + + ); +}; + +export default ScriptResultDisplay; diff --git a/src/components/Modals/ScriptModal/ScripFile.tsx b/src/components/Modals/ScriptModal/ScripFile.tsx new file mode 100644 index 0000000..b98b2d4 --- /dev/null +++ b/src/components/Modals/ScriptModal/ScripFile.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { + Button, + Flex, + FormControl, + FormErrorMessage, + FormLabel, + Input, + InputGroup, + Text, + Textarea, + useBoolean, +} from '@chakra-ui/react'; +import { UploadSimple } from 'phosphor-react'; +import { useTranslation } from 'react-i18next'; +import { v4 as uuid } from 'uuid'; +import { useFastField } from 'hooks/useFastField'; + +type Props = { + isDisabled: boolean; +}; +const ScriptFileInput = ({ isDisabled }: Props) => { + const { t } = useTranslation(); + const [fileKey, setFileKey] = React.useState(uuid()); + const fileInputRef = React.useRef(); + const { onChange, error, isError, value, onBlur } = useFastField({ name: 'script' }); + const [isTooLarge, { on, off }] = useBoolean(); + + let fileReader: FileReader | undefined; + + const handleStringFileRead = () => { + if (fileReader) { + const content = fileReader.result; + if (content) { + setFileKey(uuid()); + onChange(content as string); + } + } + }; + + const handleUploadClick = () => { + if (fileInputRef.current) fileInputRef?.current?.click(); + }; + + const changeFile = (e: React.ChangeEvent) => { + const file = e.target.files ? e.target.files[0] : undefined; + off(); + + // File has to be under 2MB + if (file && file.size < 500 * 1024) { + fileReader = new FileReader(); + fileReader.onloadend = handleStringFileRead; + fileReader.readAsText(file); + } else { + on(); + } + }; + + const onInputChange = (e: React.ChangeEvent) => { + onChange(e.target.value); + }; + + return ( + + + {t('script.one')} + + | undefined} + hidden + /> + + {isTooLarge && ( + + {t('script.file_too_large')} + + )} + + + +