Compare commits

..

1 Commits

Author SHA1 Message Date
Dmitry Dunaev
85c7a34af5 Chg: release candidate version fix for helm 2021-07-28 19:48:27 +03:00
101 changed files with 3939 additions and 8049 deletions

View File

@@ -1,25 +1,7 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
"@babel/preset-react"
],
"env": {
"production": {
"plugins": [
"@babel/plugin-transform-react-inline-elements",
"@babel/plugin-transform-react-constant-elements",
[
"transform-react-remove-prop-types",
{
"removeImport": true
}
]
]
}
}
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View File

@@ -19,6 +19,8 @@ module.exports = {
preferRelative: true,
},
plugins: [
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
@@ -51,7 +53,6 @@ module.exports = {
template: paths.public + '/index.html',
filename: 'index.html',
}),
new CleanWebpackPlugin(),
],
module: {

View File

@@ -47,7 +47,6 @@ module.exports = merge(common, {
react: path.resolve(__dirname, '../', 'node_modules', 'react'),
'react-router-dom': path.resolve('./node_modules/react-router-dom'),
'ucentral-libs': path.resolve(__dirname, '../', 'node_modules', 'ucentral-libs', 'src'),
graphlib: path.resolve(__dirname, '../', 'node_modules', 'graphlib'),
},
},
plugins: [new ReactRefreshWebpackPlugin()],

View File

@@ -4,8 +4,6 @@ const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const path = require('path');
const paths = require('./paths');
const common = require('./webpack.common');
@@ -18,65 +16,17 @@ module.exports = merge(common, {
filename: 'js/[name].[contenthash].bundle.js',
},
plugins: [
// new BundleAnalyzerPlugin(),
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash].css',
chunkFilename: '[contenthash].css',
}),
new CompressionPlugin({
filename: '[path]/[name].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$|\.eot?.+$|\.ttf?.+$|\.woff?.+$|\.svg?.+$/,
threshold: 10240,
minRatio: 0.8,
}),
],
module: {
rules: [],
},
optimization: {
minimize: true,
minimizer: [
'...',
new TerserPlugin({
terserOptions: {
warnings: false,
compress: {
comparisons: false,
},
parse: {},
mangle: true,
output: {
ascii_only: true,
},
},
parallel: true,
}),
new CssMinimizerPlugin(),
],
nodeEnv: 'production',
sideEffects: true,
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: 10,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
resolve: {
modules: [],
alias: {
graphlib: path.resolve(__dirname, '../', 'node_modules', 'graphlib'),
},
minimizer: [`...`, new TerserPlugin(), new CssMinimizerPlugin()],
},
performance: {
hints: false,

View File

@@ -7,7 +7,7 @@ fullnameOverride: ""
images:
ucentralgwui:
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/ucentralgw-ui
tag: v2.1.0-RC1
tag: v2.0.0-RC1
pullPolicy: Always
services:

4995
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,14 @@
{
"name": "ucentral-client",
"version": "2.1.0",
"version": "0.9.14",
"dependencies": {
"@coreui/coreui": "^3.4.0",
"@coreui/icons": "^2.0.1",
"@coreui/icons-react": "^1.1.0",
"@coreui/react": "^3.4.6",
"@coreui/react-chartjs": "^1.1.0",
"apexcharts": "^3.27.1",
"axios": "^0.21.1",
"axios-retry": "^3.1.9",
"dagre": "^0.8.5",
"i18next": "^20.3.1",
"i18next-browser-languagedetector": "^6.1.2",
"i18next-http-backend": "^1.2.6",
@@ -18,15 +16,12 @@
"react": "^17.0.2",
"react-apexcharts": "^1.3.9",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.6.6",
"react-i18next": "^11.11.0",
"react-paginate": "^7.1.3",
"react-router-dom": "^5.2.0",
"react-select": "^4.3.1",
"react-tooltip": "^4.2.21",
"react-widgets": "^5.1.1",
"sass": "^1.35.1",
"ucentral-libs": "^0.8.82",
"ucentral-libs": "^0.8.7",
"uuid": "^8.3.2"
},
"main": "index.js",
@@ -63,7 +58,6 @@
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^3.0.0",
"compression-webpack-plugin": "^8.0.1",
"copy-webpack-plugin": "^7.0.0",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^2.0.0",
@@ -90,7 +84,6 @@
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.4",
"webpack": "^5.40.0",
"webpack-bundle-analyzer": "^4.4.2",
"webpack-cli": "^4.7.2",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.8.0"

View File

@@ -1,6 +1,5 @@
{
"actions": {
"actions": "Aktionen",
"blink": "LEDs Blinken",
"configure": "Konfigurieren",
"connect": "Konsole öffnen",
@@ -10,7 +9,7 @@
"reboot": "Gerät neustarten",
"title": "Geräte Administrations",
"trace": "Tcpdump starten",
"wifi_scan": "Wi-Fi Scan"
"wifi_scan": "WiFi Scan"
},
"blink": {
"blink": "LEDs Blinken",
@@ -22,25 +21,18 @@
},
"commands": {
"error": "Fehler beim Senden des Befehls!",
"event_queue": "Ereigniswarteschlange",
"success": "Befehl wurde erfolgreich übermittelt",
"title": "Gerätebefehle",
"unable_queue": "Anfrage für Ereigniswarteschlange kann nicht abgeschlossen werden"
"title": "Gerätebefehle"
},
"common": {
"access_policy": "Zugangsrichtlinien",
"add": "Hinzufügen",
"adding_ellipsis": "Hinzufügen ...",
"are_you_sure": "Bist du sicher?",
"back_to_login": "Zurück zur Anmeldung",
"cancel": "Abbrechen",
"certificate": "Zertifikat",
"certificates": "Zertifikate",
"clear": "Löschen",
"close": "Schließen",
"command": "Befehl",
"commands": "Befehle",
"commands_executed": "Ausgeführte Befehle",
"compatible": "kompatibel",
"completed": "Abgeschlossen",
"config_id": "Konfigurations ID",
@@ -48,109 +40,63 @@
"connected": "Verbindung wurde hergestellt",
"copied": "kopiert!",
"copy_to_clipboard": "In die Zwischenablage kopieren",
"create": "Erstellen",
"created": "Erstellt",
"created_by": "Erstellt von",
"current": "Aktuell",
"custom_date": "Benutzerdefiniertes Datum",
"dashboard": "Instrumententafel",
"date": "Datum",
"day": "tag",
"days": "tage",
"delete": "Löschen",
"delete_device": "Gerät löschen",
"details": "Einzelheiten",
"device": "Gerät #{{serialNumber}}",
"device_dashboard": "Geräte-Dashboard",
"device_delete": "Gerät Nr.{{serialNumber}}löschen",
"device_deleted": "Gerät erfolgreich gelöscht",
"device_health": "Gerätezustand",
"device_list": "Liste der Geräte",
"device_page": "Aussicht",
"device_status": "Gerätestatus",
"device_page": "Geräte",
"devices": "Geräte",
"devices_using_latest": "Geräte mit der neuesten Firmware",
"devices_using_unknown": "Geräte mit unbekannter Firmware",
"dismiss": "entlassen",
"do_now": "Sofort",
"download": "Herunterladen",
"duration": "Dauer",
"edit": "Bearbeiten",
"edit_user": "Bearbeiten",
"email_address": "E-Mail-Addresse",
"endpoint": "Endpunkt",
"endpoints": "Endpunkte",
"error": "Fehler",
"execute_now": "Möchten Sie diesen Befehl jetzt ausführen?",
"executed": "Ausgeführt",
"exit": "Ausgang",
"firmware": "Firmware",
"firmware_dashboard": "Firmware-Dashboard",
"firmware_installed": "Firmware installiert",
"forgot_password": "Haben Sie Ihr Passwort vergessen?",
"forgot_password_title": "Passwort vergessen",
"from": "Von",
"general_error": "API-Fehler, wenden Sie sich bitte an Ihren Administrator",
"hide": "verbergen",
"hour": "stunde",
"hours": "std",
"id": "ID",
"ip_address": "IP Adresse",
"last_dashboard_refresh": "Letzte Dashboard-Aktualisierung",
"later_tonight": "Später am Abend",
"latest": "Neueste",
"list": "Liste",
"loading_ellipsis": "Wird geladen...",
"loading_more_ellipsis": "Mehr laden ...",
"logout": "Ausloggen",
"mac": "MAC-Adresse",
"manufacturer": "Hersteller",
"memory_used": "Verwendeter Speicher",
"minute": "Minute",
"minutes": "protokoll",
"na": "(unbekannt)",
"need_date": "Du brauchst ein Datum...",
"no": "Nein",
"no_devices_found": "Keine Geräte gefunden",
"no_items": "Keine Gegenstände",
"not_connected": "Nicht verbunden",
"of_connected": "% der Geräte",
"off": "Aus",
"on": "An",
"optional": "Wahlweise",
"overall_health": "Allgemeine Gesundheit",
"password_policy": "Kennwortrichtlinie",
"recorded": "Verzeichnet",
"refresh": "Aktualisierung",
"refresh_device": "Gerät aktualisieren",
"required": "Erforderlich",
"result": "Ergebnis",
"save": "Sparen",
"saved": "Gerettet!",
"saving": "Speichern ...",
"schedule": "Zeitplan",
"search": "Geräte suchen",
"second": "zweite",
"seconds": "sekunden",
"seconds_elapsed": "Sekunden verstrichen",
"serial_number": "Seriennummer",
"show_all": "Zeige alles",
"start": "Start",
"submit": "Absenden",
"submitted": "Eingereicht",
"success": "Erfolg",
"system": "System",
"table": "Tabelle",
"timestamp": "Zeit",
"to": "zu",
"type": "Art",
"unable_to_connect": "Keine Verbindung zum Gerät möglich",
"unable_to_delete": "Löschen nicht möglich",
"unknown": "unbekannte",
"up_to_date": "Aktuelle Geräte",
"uptimes": "Betriebszeiten",
"uuid": "UUID",
"vendors": "Anbieter",
"view_more": "Mehr anzeigen",
"yes": "Ja"
},
@@ -166,7 +112,6 @@
"owner": "Inhaber",
"title": "Gerätekonfiguration",
"type": "Gerätetyp",
"uuid": "Konfigurations-ID",
"view_json": "Rohe Konfiguration anzeigen"
},
"configure": {
@@ -191,20 +136,6 @@
"severity": "Wichtigkeit",
"title": "Geräteprotokolle"
},
"entity": {
"add_child": "Untergeordnete Entität zu {{entityName}}hinzufügen",
"add_failure": "Fehler, der Server hat zurückgegeben: {{error}}",
"add_root": "Root-Entität hinzufügen",
"add_success": "Entität erfolgreich erstellt!",
"cannot_delete": "Entitäten mit untergeordneten Elementen können nicht gelöscht werden. Löschen Sie die untergeordneten Elemente dieser Entität, um sie löschen zu können.",
"delete_success": "Entität erfolgreich gelöscht",
"delete_warning": "Achtung: Dieser Vorgang kann nicht rückgängig gemacht werden",
"edit_failure": "Aktualisierung fehlgeschlagen : {{error}}",
"entities": "Entitäten",
"entity": "Entität",
"only_unassigned": "Nur nicht zugewiesen",
"valid_serial": "Muss eine gültige Seriennummer sein (12 HEX-Zeichen)"
},
"factory_reset": {
"redirector": "Gatewaykonfiguration beibehalten:",
"reset": "Zurücksetzen",
@@ -212,35 +143,6 @@
"title": "Gerät auf Werkseinstellungen zurücksetzen",
"warning": "Achtung: Nach dem Absenden kann dies nicht rückgängig gemacht werden"
},
"firmware": {
"average_age": "Durchschnittliches Firmware-Alter",
"choose_custom": "Wählen",
"details_title": "Bild #{{image}} Details",
"device_type": "Gerätetyp",
"device_types": "Gerätetypen",
"downloads": "Downloads",
"error_fetching_latest": "Fehler beim Abrufen der neuesten Firmware",
"from_release": "Von",
"history_title": "Geschichte",
"image": "Bild",
"image_date": "Bilddatum",
"installed_firmware": "Installierte Firmware",
"latest_version_installed": "Neueste Version installiert Version",
"newer_firmware_available": "Neuere Versionen verfügbar",
"reinstall_latest": "Neu installieren",
"revision": "Revision",
"show_dev": "Dev-Releases anzeigen",
"size": "Größe",
"status": "Firmware-Status",
"title": "Firmware",
"to_release": "Zu",
"unknown_firmware_status": "Unbekannter Firmware-Status",
"upgrade": "Aktualisierung",
"upgrade_command_submitted": "Upgrade-Befehl erfolgreich gesendet",
"upgrade_to_latest": "Neueste",
"upgrade_to_version": "Upgrade auf diese Revision",
"upgrading": "Upgrade durchführen..."
},
"footer": {
"coreui_for_react": "CoreUI für React",
"powered_by": "Unterstützt von",
@@ -250,48 +152,13 @@
"sanity": "Gesundheitzustand",
"title": "Gesundheitzustand"
},
"inventory": {
"add_child_venue": "Untergeordneten Veranstaltungsort zu {{entityName}}hinzufügen",
"add_tag": "Tag hinzufügen",
"add_tag_to": "Inventar-Tag zu {{name}}hinzufügen",
"assign_error": "Fehler beim Versuch, Tag zuzuweisen",
"assign_to_entity": "Zu Entität zuweisen",
"error_retrieving": "Beim Abrufen von Inventar-Tags ist ein Fehler aufgetreten",
"error_unassign": "Fehler beim Aufheben der Zuweisung",
"subscriber": "Teilnehmer",
"successful_assign": "Tag erfolgreich zugewiesen",
"successful_tag_update": "Tag erfolgreich aktualisiert",
"successful_unassign": "Vorgang zum Aufheben der Zuweisung war erfolgreich",
"tag_created": "Inventar-Tag erfolgreich erstellt",
"tag_creation_error": "Fehler beim Versuch, Inventar-Tag zu erstellen",
"tag_update_error": "Fehler beim Aktualisieren des Tags",
"tags_assigned_to": "Inventar-Tags {{name}}zugewiesen",
"unassign": "Zuordnung aufheben",
"unassign_tag": "Tag von Entität zuweisen",
"unassigned_tags": "Nicht zugewiesene Tags",
"venue": "Tagungsort"
},
"login": {
"change_password": "Ändere das Passwort",
"change_password_error": "Fehler beim Ändern des Passworts. Stellen Sie sicher, dass das neue Passwort gültig ist, indem Sie die Seite \"Passwortrichtlinie\" besuchen",
"change_password_instructions": "Geben Sie Ihr neues Passwort ein und bestätigen Sie es",
"changing_password": "Passwort ändern...",
"confirm_new_password": "Bestätige neues Passwort",
"different_passwords": "Sie müssen das gleiche Passwort zweimal eingeben",
"forgot_password_error": "Fehler beim Versuch, eine E-Mail mit vergessenem Passwort zu senden. Stellen Sie sicher, dass diese Benutzer-ID mit einem Konto verknüpft ist.",
"forgot_password_explanation": "Geben Sie Ihren Benutzernamen ein, um eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts zu erhalten",
"forgot_password_success": "Sie sollten in Kürze eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts erhalten. Bitte überprüfen Sie Ihren Spam, wenn Sie die E-Mail nicht finden können",
"logging_in": "Einloggen...",
"login": "Anmeldung",
"login_error": "Anmeldefehler, stellen Sie sicher, dass die von Ihnen angegebenen Informationen gültig sind",
"new_password": "Neues Kennwort",
"login_error": "Anmeldefehler, bestätigen Sie, dass Ihr Benutzername, Ihr Passwort und Ihre Gateway-URL gültig sind",
"password": "Passwort",
"please_enter_gateway": "Bitte geben Sie eine uCentralSec-URL ein",
"please_enter_password": "Bitte geben Sie Ihr Passwort ein",
"please_enter_username": "Bitte geben Sie Ihren Benutzernamen ein",
"previously_used": "Passwort wurde zuvor verwendet",
"send_forgot": "E-Mail senden",
"sending_ellipsis": "Senden…",
"sign_in_to_account": "Melden Sie sich bei Ihrem Konto an",
"url": "uCentralSec-URL",
"username": "Benutzername"
@@ -312,15 +179,10 @@
"scanning": "Scannen... ",
"waiting_directions": "Bitte warten Sie auf das Scanergebnis. Dies kann bis zu 25 Sekunden dauern. Sie können den Vorgang später beenden und die Ergebnisse aus der Befehlstabelle anzeigen."
},
"settings": {
"title": "die Einstellungen"
},
"statistics": {
"data": "Daten (KB)",
"latest_statistics": "Neueste Statistiken",
"lifetime_stats": "Lifetime-Statistik",
"no_interfaces": "Keine Statistiken zur Schnittstellenlebensdauer verfügbar",
"show_latest": "Letzte Statistik",
"show_latest": "Neueste Statistiken anzeigen JSON",
"title": "Statistiken"
},
"status": {
@@ -336,9 +198,6 @@
"uptime": "Betriebszeit",
"used_total_memory": "{{used}} verwendet / {{total}} insgesamt"
},
"system": {
"error_fetching": "Fehler beim Abrufen von Systeminformationen"
},
"trace": {
"choose_network": "Netzwerk auswählen",
"directions": "Starten Sie eine Tcpdump auf diesem Geräts für eine bestimmte Dauer oder eine Anzahl von Paketen",
@@ -346,7 +205,6 @@
"packets": "Pakete",
"title": "Tcpdump",
"trace": "Spur",
"trace_not_successful": "Trace nicht erfolgreich: Gateway hat folgenden Fehler gemeldet: {{error}}",
"wait_for_file": "Möchten Sie warten, bis die Trace-Datei fertig ist?",
"waiting_directions": "Bitte warten Sie auf die Trace-Datendatei. Dies könnte eine Weile dauern. Sie können das Warten beenden und die Ablaufverfolgungsdatei später aus der Befehlstabelle abrufen.",
"waiting_seconds": "Verstrichene Zeit: {{seconds}} Sekunden"
@@ -366,52 +224,5 @@
"upgrade": "Aktualisierung",
"wait_for_upgrade": "Möchten Sie warten, bis das Upgrade abgeschlossen ist?",
"waiting_for_device": "Warten, bis das Gerät wieder verbunden ist"
},
"user": {
"avatar": "Dein Avatar",
"avatar_file": "Dein Avatar (max. 2 MB)",
"create": "Benutzer erstellen",
"create_failure": "Fehler beim Erstellen des Benutzers. Bitte stellen Sie sicher, dass diese E-Mail-Adresse nicht bereits mit einem Konto verknüpft ist.",
"create_success": "Benutzer erfolgreich erstellt",
"creating": "Benutzer erstellen ...",
"delete_avatar": "Avatar löschen",
"delete_failure": "Fehler beim Versuch, den Benutzer zu löschen",
"delete_success": "Benutzer erfolgreich gelöscht!",
"delete_title": "Benutzer löschen",
"delete_warning": "Warnung: Sobald Sie einen Benutzer gelöscht haben, können Sie ihn nicht wiederherstellen",
"deleting": "Löschen ...",
"description": "Beschreibung",
"edit": "Benutzer bearbeiten",
"email_address": "E-Mail-Addresse",
"force_password_change": "Passwortänderung bei der Anmeldung erzwingen",
"id": "Benutzeridentifikation.",
"last_login": "Letzte Anmeldung",
"login_id": "Anmelde-ID.",
"my_profile": "Mein Profil",
"name": "Name",
"nickname": "Spitzname",
"nickname_explanation": "Spitzname (optional)",
"not_validated": "Nicht validiert",
"note": "Hinweis",
"password": "Passwort",
"provide_email": "Bitte geben Sie eine gültige E-Mail Adresse an",
"provide_password": "Bitte geben Sie ein gültiges Passwort ein",
"save_avatar": "Avatar speichern",
"show_hide_password": "Passwort anzeigen/verbergen",
"update_failure": "Stellen Sie sicher, dass alle Ihre Daten gültig sind. Wenn Sie das Kennwort ändern, stellen Sie sicher, dass es sich nicht um ein altes handelt.",
"update_failure_title": "Update fehlgeschlagen",
"update_success": "Benutzer erfolgreich aktualisiert",
"update_success_title": "Erfolg",
"user_role": "Rolle",
"users": "Benutzer",
"validated": "Bestätigt"
},
"wifi_analysis": {
"association": "Verband",
"associations": "Verbände",
"mode": "Modus",
"network_diagram": "Netzwerkdiagramm",
"radios": "Radios",
"title": "WLAN-Analyse"
}
}

View File

@@ -1,6 +1,5 @@
{
"actions": {
"actions": "Actions",
"blink": "Blink",
"configure": "Configure",
"connect": "Connect",
@@ -10,7 +9,7 @@
"reboot": "Reboot",
"title": "Commands",
"trace": "Trace",
"wifi_scan": "Wi-Fi Scan"
"wifi_scan": "Wifi Scan"
},
"blink": {
"blink": "Blink",
@@ -22,25 +21,18 @@
},
"commands": {
"error": "Error while submitting command!",
"event_queue": "Event Queue",
"success": "Command submitted successfully, you can look at the Commands log for the result",
"title": "Command History",
"unable_queue": "Unable to complete event queue request"
"title": "Command History"
},
"common": {
"access_policy": "Access Policy",
"add": "Add",
"adding_ellipsis": "Adding...",
"are_you_sure": "Are you sure?",
"back_to_login": "Back to Login",
"cancel": "Cancel",
"certificate": "Certificate",
"certificates": "Certificates",
"clear": "Clear",
"close": "Close",
"command": "Command",
"commands": "Commands",
"commands_executed": "Commands Executed",
"compatible": "Compatible",
"completed": "Completed",
"config_id": "Config. Id",
@@ -48,109 +40,63 @@
"connected": "Connected",
"copied": "Copied!",
"copy_to_clipboard": "Copy to clipboard",
"create": "Create",
"created": "Created",
"created_by": "Created By",
"current": "Current ",
"custom_date": "Custom Date",
"dashboard": "Dashboard",
"date": "Date",
"day": "day",
"days": "days",
"delete": "Delete",
"delete_device": "Delete Device",
"details": "Details",
"device": "Device #{{serialNumber}}",
"device_dashboard": "Device Dashboard",
"device_delete": "Delete Device #{{serialNumber}}",
"device_deleted": "Device Successfully Deleted",
"device_health": "Device Health",
"device_list": "List of Devices",
"device_page": "View",
"device_status": "Device Status",
"device_page": "Device Page",
"devices": "Devices",
"devices_using_latest": "Devices Using Latest Firmware",
"devices_using_unknown": "Devices Using Unknown Firmware",
"dismiss": "Dismiss",
"do_now": "Do Now!",
"download": "Download",
"duration": "Duration",
"edit": "Edit",
"edit_user": "Edit",
"email_address": "Email Address",
"endpoint": "Endpoint",
"endpoints": "Endpoints",
"error": "Error",
"execute_now": "Would you like to execute this command now?",
"executed": "Executed",
"exit": "Exit",
"firmware": "Firmware",
"firmware_dashboard": "Firmware Dashboard",
"firmware_installed": "Firmware Installed",
"forgot_password": "Forgot your Password?",
"forgot_password_title": "Forgot Password",
"from": "From",
"general_error": "API Error, please consult your administrator",
"hide": "Hide",
"hour": "hour",
"hours": "hours",
"id": "Id",
"ip_address": "IP Address",
"last_dashboard_refresh": "Last Dashboard Refresh",
"ip_address": "Ip Address",
"later_tonight": "Later tonight",
"latest": "Latest",
"list": "List",
"loading_ellipsis": "Loading...",
"loading_more_ellipsis": "Loading more...",
"logout": "Logout",
"mac": "MAC Address",
"manufacturer": "Manufacturer",
"memory_used": "Memory Used",
"minute": "minute",
"minutes": "minutes",
"na": "N/A",
"need_date": "You need a date...",
"no": "No",
"no_devices_found": "No Devices Found",
"no_items": "No Items",
"not_connected": "Not Connected",
"of_connected": "% of devices",
"off": "Off",
"on": "On",
"optional": "Optional",
"overall_health": "Overall Health",
"password_policy": "Password Policy",
"recorded": "Recorded",
"refresh": "Refresh",
"refresh_device": "Refresh Device",
"required": "Required",
"result": "Result",
"save": "Save",
"saved": "Saved!",
"saving": "Saving... ",
"schedule": "Schedule",
"search": "Search Devices",
"second": "second",
"seconds": "seconds",
"seconds_elapsed": "Seconds elapsed",
"serial_number": "Serial Number",
"show_all": "Show All",
"start": "Start",
"submit": "Submit",
"submitted": "Submitted",
"success": "Success",
"system": "System",
"table": "Table",
"timestamp": "Time",
"to": "To",
"type": "Type",
"unable_to_connect": "Unable to Connect to Device",
"unable_to_delete": "Unable to Delete",
"unknown": "Unknown",
"up_to_date": "Up to Date Devices",
"uptimes": "Uptimes",
"uuid": "UUID",
"vendors": "Vendors",
"view_more": "View more",
"yes": "Yes"
},
@@ -166,7 +112,6 @@
"owner": "Owner",
"title": "Configuration",
"type": "Device Type",
"uuid": "Config ID",
"view_json": "View raw JSON"
},
"configure": {
@@ -191,20 +136,6 @@
"severity": "Severity",
"title": "Logs"
},
"entity": {
"add_child": "Add Child Entity to {{entityName}}",
"add_failure": "Error, the server returned : {{error}}",
"add_root": "Add Root Entity",
"add_success": "Entity Successfully Created!",
"cannot_delete": "You cannot delete entities which have children. Delete this entity's children to be able to delete it.",
"delete_success": "Entity Successfully Deleted",
"delete_warning": "Warning: this operation cannot be reverted",
"edit_failure": "Update unsuccessful : {{error}}",
"entities": "Entities",
"entity": "Entity",
"only_unassigned": "Only Unassigned",
"valid_serial": "Needs to be a valid serial number (12 HEX characters)"
},
"factory_reset": {
"redirector": "Keep redirector: ",
"reset": "Reset",
@@ -212,35 +143,6 @@
"title": "Factory Reset",
"warning": "Warning: Once you submit this cannot be reverted"
},
"firmware": {
"average_age": "Average Firmware Age",
"choose_custom": "Choose",
"details_title": "Image #{{image}} Details",
"device_type": "Device Type",
"device_types": "Device Types",
"downloads": "Downloads",
"error_fetching_latest": "Error while fetching latest firmware",
"from_release": "From",
"history_title": "History",
"image": "Image",
"image_date": "Image Date",
"installed_firmware": "Installed Firmware",
"latest_version_installed": "Latest Version Installed",
"newer_firmware_available": "Newer Revisions Available",
"reinstall_latest": "Reinstall ",
"revision": "Revision",
"show_dev": "Show Dev Releases",
"size": "Size",
"status": "Firmware Status",
"title": "Firmware",
"to_release": "To",
"unknown_firmware_status": "Unknown Firmware Status",
"upgrade": "Upgrade",
"upgrade_command_submitted": "Upgrade Command Submitted Successfully",
"upgrade_to_latest": "Latest",
"upgrade_to_version": "Upgrade to this Revision",
"upgrading": "Upgrading..."
},
"footer": {
"coreui_for_react": "CoreUI for React",
"powered_by": "Powered by",
@@ -250,48 +152,13 @@
"sanity": "Sanity",
"title": "Health"
},
"inventory": {
"add_child_venue": "Add Child Venue to {{entityName}}",
"add_tag": "Add Tag",
"add_tag_to": "Add Inventory Tag to {{name}}",
"assign_error": "Error while trying to assign tag",
"assign_to_entity": "Assign to Entity",
"error_retrieving": "Error occurred while retrieving inventory tags",
"error_unassign": "Error during unassign operation",
"subscriber": "Subscriber",
"successful_assign": "Tag successfully assigned",
"successful_tag_update": "Successfully updated tag",
"successful_unassign": "Unassign operation was successful",
"tag_created": "Inventory tag successfully created",
"tag_creation_error": "Error while trying to create inventory tag",
"tag_update_error": "Error while updating tag",
"tags_assigned_to": "Inventory tags assigned to {{name}}",
"unassign": "Unassign",
"unassign_tag": "Unassign Tag from Entity",
"unassigned_tags": "Unassigned tags",
"venue": "Venue"
},
"login": {
"change_password": "Change Password",
"change_password_error": "Error while changing password. Make sure the new password is valid by visiting the 'Password Policy' page",
"change_password_instructions": "Enter and confirm your new password",
"changing_password": "Changing Password... ",
"confirm_new_password": "Confirm New Password",
"different_passwords": "You need to enter the same password twice",
"forgot_password_error": "Error while trying to send Forgot Password email. Please make sure this userId is associated to an account.",
"forgot_password_explanation": "Enter your username to receive an email containing the instructions to reset your password",
"forgot_password_success": "You should soon receive an email containing the instructions to reset your password. Please make sure to check your spam if you can't find the email",
"logging_in": "Logging In... ",
"login": "Login",
"login_error": "Login error, make sure the information you are providing is valid",
"new_password": "New Password",
"login_error": "Login error, confirm that your username, password and gateway url are valid",
"password": "Password",
"please_enter_gateway": "Please enter a uCentralSec URL",
"please_enter_password": "Please enter your password",
"please_enter_username": "Please enter your username",
"previously_used": "Password was previously used",
"send_forgot": "Send Email",
"sending_ellipsis": "Sending... ",
"sign_in_to_account": "Sign in to your account",
"url": "uCentralSec URL",
"username": "Username"
@@ -307,20 +174,15 @@
"directions": "Launch a wifi scan of this device, which should take approximately 25 seconds.",
"re_scan": "Re-Scan",
"result_directions": "Please click the '$t(scan.re_scan)' button if you would like to do a scan with the same configuration as the last.",
"results": "Wi-Fi Scan Results",
"results": "Wifi Scan Results",
"scan": "Scan",
"scanning": "Scanning... ",
"waiting_directions": "Please wait for the scan result. This may take up to 25 seconds. You can exit and look at the results from the commands table later."
},
"settings": {
"title": "Settings"
},
"statistics": {
"data": "Data (KB)",
"latest_statistics": "Latest Statistics",
"lifetime_stats": "Lifetime Statistics",
"no_interfaces": "No interface lifetime statistics available",
"show_latest": "Last Statistics",
"show_latest": "Show latest statistics JSON",
"title": "Statistics"
},
"status": {
@@ -336,9 +198,6 @@
"uptime": "Uptime",
"used_total_memory": "{{used}} used / {{total}} total "
},
"system": {
"error_fetching": "Error while fetching system information"
},
"trace": {
"choose_network": "Choose network",
"directions": "Launch a remote trace of this device for either a specific duration or a number of packets",
@@ -346,7 +205,6 @@
"packets": "Packets",
"title": "Trace",
"trace": "Trace",
"trace_not_successful": "Trace not successful: gateway reported the following error : {{error}}",
"wait_for_file": "Would you like to wait until the trace file is ready?",
"waiting_directions": "Please wait for the trace data file. This may take some time. You can exit the wait and retrieve the trace file from the commands table later.",
"waiting_seconds": "Time Elapsed: {{seconds}} seconds"
@@ -366,52 +224,5 @@
"upgrade": "Upgrade",
"wait_for_upgrade": "Would you like to wait for the upgrade to finish?",
"waiting_for_device": "Waiting for device to reconnect"
},
"user": {
"avatar": "Your Avatar",
"avatar_file": "Your Avatar (max. of 2 MB)",
"create": "Create User",
"create_failure": "Error while creating user. Please make sure this email address is not already linked to an account.",
"create_success": "User Created Successfully",
"creating": "Creating User...",
"delete_avatar": "Delete Avatar",
"delete_failure": "Error while trying to delete user",
"delete_success": "User successfully deleted!",
"delete_title": "Delete User",
"delete_warning": "Warning: Once you delete a user you cannot revert",
"deleting": "Deleting... ",
"description": "Description",
"edit": "Edit User",
"email_address": "Email Address",
"force_password_change": "Force Password Change on Login",
"id": "User Id.",
"last_login": "Last Login",
"login_id": "Login Id.",
"my_profile": "My Profile",
"name": "Name",
"nickname": "Nickname",
"nickname_explanation": "Nickname (optional)",
"not_validated": "Not Validated",
"note": "Note",
"password": "Password",
"provide_email": "Please provide a valid email address",
"provide_password": "Please provide a valid password",
"save_avatar": "Save Avatar",
"show_hide_password": "Show/Hide Password",
"update_failure": "Make sure all of your data is valid. If you are modifying the password, make sure it is not an old one.",
"update_failure_title": "Update Failed",
"update_success": "User Updated Successfully",
"update_success_title": "Success",
"user_role": "Role",
"users": "Users",
"validated": "Validated"
},
"wifi_analysis": {
"association": "Association",
"associations": "Associations",
"mode": "Mode",
"network_diagram": "Network Diagram",
"radios": "Radios",
"title": "Wi-Fi Analysis"
}
}

View File

@@ -1,6 +1,5 @@
{
"actions": {
"actions": "Comportamiento",
"blink": "Parpadeo",
"configure": "Configurar",
"connect": "Conectar",
@@ -10,7 +9,7 @@
"reboot": "Reiniciar",
"title": "Comandos",
"trace": "Rastro",
"wifi_scan": "Escaneo Wi-Fi "
"wifi_scan": "Escaneo Wifi"
},
"blink": {
"blink": "Parpadeo",
@@ -22,25 +21,18 @@
},
"commands": {
"error": "¡Error al enviar el comando!",
"event_queue": "Cola de eventos",
"success": "Comando enviado con éxito, puede consultar el registro de Comandos para ver el resultado",
"title": "Historial de Comandos",
"unable_queue": "No se pudo completar la solicitud de cola de eventos"
"title": "Historial de Comandos"
},
"common": {
"access_policy": "Política de acceso",
"add": "Añadir",
"adding_ellipsis": "Añadiendo ...",
"are_you_sure": "¿Estás seguro?",
"back_to_login": "Atrás para iniciar sesión",
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"clear": "Claro",
"close": "Cerrar",
"command": "Mando",
"commands": "comandos",
"commands_executed": "Comandos ejecutados",
"compatible": "Compatible",
"completed": "terminado",
"config_id": "Config. Identificación",
@@ -48,109 +40,63 @@
"connected": "Conectado",
"copied": "Copiado!",
"copy_to_clipboard": "Copiar al portapapeles",
"create": "Crear",
"created": "creado",
"created_by": "Creado por",
"current": "Corriente",
"custom_date": "Fecha personalizada",
"dashboard": "Tablero",
"date": "Fecha",
"day": "día",
"days": "días",
"delete": "Borrar",
"delete_device": "Eliminar dispositivo",
"details": "Detalles",
"device": "Dispositivo n.º{{serialNumber}}",
"device_dashboard": "Panel de control del dispositivo",
"device_delete": "Eliminar dispositivo n.º{{serialNumber}}",
"device_deleted": "Dispositivo eliminado correctamente",
"device_health": "Salud del dispositivo",
"device_list": "Listado de dispositivos",
"device_page": "Ver",
"device_status": "Estado del dispositivo",
"device_page": "Página del dispositivo",
"devices": "Dispositivos",
"devices_using_latest": "Dispositivos que utilizan el firmware más reciente",
"devices_using_unknown": "Dispositivos que utilizan firmware desconocido",
"dismiss": "Despedir",
"do_now": "¡Hagan ahora!",
"download": "Descargar",
"duration": "Duración",
"edit": "Editar",
"edit_user": "Editar",
"email_address": "Dirección de correo electrónico",
"endpoint": "punto final",
"endpoints": "Puntos finales",
"error": "Error",
"execute_now": "¿Le gustaría ejecutar este comando ahora?",
"executed": "ejecutado",
"exit": "salida",
"firmware": "Firmware",
"firmware_dashboard": "Panel de firmware",
"firmware_installed": "Firmware instalado",
"forgot_password": "¿Olvidaste tu contraseña?",
"forgot_password_title": "Se te olvidó tu contraseña",
"from": "Desde",
"general_error": "Error de API, consulte a su administrador",
"hide": "Esconder",
"hour": "hora",
"hours": "horas",
"id": "Carné de identidad",
"ip_address": "Dirección IP",
"last_dashboard_refresh": "Última actualización del panel",
"later_tonight": "Más tarde esta noche",
"latest": "último",
"list": "Lista",
"loading_ellipsis": "Cargando...",
"loading_more_ellipsis": "Cargando más ...",
"logout": "Cerrar sesión",
"mac": "Dirección MAC",
"manufacturer": "Fabricante",
"memory_used": "Memoria usada",
"minute": "minuto",
"minutes": "minutos",
"na": "N / A",
"need_date": "Necesitas una cita ...",
"no": "No",
"no_devices_found": "No se encontraron dispositivos",
"no_items": "No hay articulos",
"not_connected": "No conectado",
"of_connected": "% de dispositivos",
"off": "Apagado",
"on": "en",
"optional": "Opcional",
"overall_health": "Salud en general",
"password_policy": "Política de contraseñas",
"recorded": "Grabado",
"refresh": "Refrescar",
"refresh_device": "Actualizar dispositivo",
"required": "Necesario",
"result": "Resultado",
"save": "Salvar",
"saved": "¡Salvado!",
"saving": "Ahorro...",
"schedule": "Programar",
"search": "Dispositivos de búsqueda",
"second": "segundo",
"seconds": "segundos",
"seconds_elapsed": "Segundos transcurridos",
"serial_number": "Número de serie",
"show_all": "Mostrar todo",
"start": "comienzo",
"submit": "Enviar",
"submitted": "Presentado",
"success": "Éxito",
"system": "Sistema",
"table": "Mesa",
"timestamp": "hora",
"to": "a",
"type": "Tipo",
"unable_to_connect": "No se puede conectar al dispositivo",
"unable_to_delete": "No se puede eliminar",
"unknown": "Desconocido",
"up_to_date": "Dispositivos actualizados",
"uptimes": "Tiempos de actividad",
"uuid": "UUID",
"vendors": "Vendedores",
"view_more": "Ver más",
"yes": "Sí"
},
@@ -166,7 +112,6 @@
"owner": "Propietario",
"title": "Configuración",
"type": "Tipo de dispositivo",
"uuid": "ID de configuración",
"view_json": "Ver JSON sin procesar"
},
"configure": {
@@ -191,20 +136,6 @@
"severity": "Gravedad",
"title": "Registros"
},
"entity": {
"add_child": "Agregar entidad secundaria a {{entityName}}",
"add_failure": "Error, el servidor devolvió: {{error}}",
"add_root": "Agregar entidad raíz",
"add_success": "¡Entidad creada con éxito!",
"cannot_delete": "No puede eliminar entidades que tienen hijos. Elimina los hijos de esta entidad para poder eliminarla.",
"delete_success": "Entidad eliminada correctamente",
"delete_warning": "Advertencia: esta operación no se puede revertir",
"edit_failure": "Actualización fallida: {{error}}",
"entities": "entidades",
"entity": "Entidad",
"only_unassigned": "Solo sin asignar",
"valid_serial": "Debe ser un número de serie válido (12 caracteres HEX)"
},
"factory_reset": {
"redirector": "Mantener el redirector:",
"reset": "Reiniciar",
@@ -212,35 +143,6 @@
"title": "Restablecimiento De Fábrica",
"warning": "Advertencia: una vez que envíe, esto no se podrá revertir"
},
"firmware": {
"average_age": "Edad promedio del firmware",
"choose_custom": "Escoger",
"details_title": "Detalles de la imagen n. °{{image}} ",
"device_type": "Tipo de dispositivo",
"device_types": "Tipos de dispositivos",
"downloads": "Descargas",
"error_fetching_latest": "Error al obtener el firmware más reciente",
"from_release": "Desde",
"history_title": "Historia",
"image": "Imagen",
"image_date": "Fecha de la imagen",
"installed_firmware": "Firmware instalado",
"latest_version_installed": "Última versión instalada",
"newer_firmware_available": "Nuevas revisiones disponibles",
"reinstall_latest": "Reinstalar",
"revision": "Revisión",
"show_dev": "Mostrar lanzamientos para desarrolladores",
"size": "Tamaño",
"status": "Estado del firmware",
"title": "Firmware",
"to_release": "A",
"unknown_firmware_status": "Estado de firmware desconocido",
"upgrade": "Mejorar",
"upgrade_command_submitted": "El comando de actualización se envió correctamente",
"upgrade_to_latest": "último",
"upgrade_to_version": "Actualizar a esta revisión",
"upgrading": "Actualizando ..."
},
"footer": {
"coreui_for_react": "CoreUI para React",
"powered_by": "energizado por",
@@ -250,48 +152,13 @@
"sanity": "Cordura",
"title": "Salud"
},
"inventory": {
"add_child_venue": "Agregar lugar infantil a {{entityName}}",
"add_tag": "Añadir etiqueta",
"add_tag_to": "Agregar etiqueta de inventario a {{name}}",
"assign_error": "Error al intentar asignar la etiqueta",
"assign_to_entity": "Asignar a entidad",
"error_retrieving": "Se produjo un error al recuperar las etiquetas de inventario",
"error_unassign": "Error durante la operación de anulación de asignación",
"subscriber": "Abonado",
"successful_assign": "Etiqueta asignada correctamente",
"successful_tag_update": "Etiqueta actualizada correctamente",
"successful_unassign": "La operación de anulación de asignación se realizó correctamente",
"tag_created": "Etiqueta de inventario creada correctamente",
"tag_creation_error": "Error al intentar crear una etiqueta de inventario",
"tag_update_error": "Error al actualizar la etiqueta",
"tags_assigned_to": "Etiquetas de inventario asignadas a {{name}}",
"unassign": "Anular asignación",
"unassign_tag": "Anular asignación de etiqueta de entidad",
"unassigned_tags": "Etiquetas sin asignar",
"venue": "Lugar de encuentro"
},
"login": {
"change_password": "Cambia la contraseña",
"change_password_error": "Error al cambiar la contraseña. Asegúrese de que la nueva contraseña sea válida visitando la página 'Política de contraseñas'",
"change_password_instructions": "Ingrese y confirme su nueva contraseña",
"changing_password": "Cambio de contraseña ...",
"confirm_new_password": "confirmar nueva contraseña",
"different_passwords": "Debes ingresar la misma contraseña dos veces",
"forgot_password_error": "Error al intentar enviar el correo electrónico de Olvidé mi contraseña. Asegúrese de que este ID de usuario esté asociado a una cuenta.",
"forgot_password_explanation": "Ingrese su nombre de usuario para recibir un correo electrónico con las instrucciones para restablecer su contraseña",
"forgot_password_success": "Pronto debería recibir un correo electrónico con las instrucciones para restablecer su contraseña. Asegúrese de verificar su correo no deseado si no puede encontrar el correo electrónico",
"logging_in": "Iniciar sesión...",
"login": "Iniciar sesión",
"login_error": "Error de inicio de sesión, asegúrese de que la información que proporciona sea válida",
"new_password": "Nueva contraseña",
"login_error": "Error de inicio de sesión, confirme que su nombre de usuario, contraseña y URL de puerta de enlace son válidos",
"password": "Contraseña",
"please_enter_gateway": "Ingrese una URL de uCentralSec",
"please_enter_password": "Por favor, introduzca su contraseña",
"please_enter_username": "Por favor, ingrese su nombre de usuario",
"previously_used": "La contraseña se usó anteriormente",
"send_forgot": "Enviar correo electrónico",
"sending_ellipsis": "Enviando...",
"sign_in_to_account": "Iniciar sesión en su cuenta",
"url": "URL de uCentralSec",
"username": "Nombre de usuario"
@@ -307,20 +174,15 @@
"directions": "Ejecute un escaneo wifi de este dispositivo, que debería tomar aproximadamente 25 segundos.",
"re_scan": "Vuelva a escanear",
"result_directions": "Haga clic en el botón '$ t (scan.re_scan)' si desea realizar un escaneo con la misma configuración que el anterior.",
"results": "Resultados de escaneo Wi-Fi",
"results": "Resultados de escaneo Wifi",
"scan": "Escanear",
"scanning": "Exploración... ",
"waiting_directions": "Espere el resultado del escaneo. Esto puede tardar hasta 25 segundos. Puede salir y ver los resultados de la tabla de comandos más adelante."
},
"settings": {
"title": "Ajustes"
},
"statistics": {
"data": "Datos (KB)",
"latest_statistics": "Últimas estadísticas",
"lifetime_stats": "Estadísticas de por vida",
"no_interfaces": "No hay estadísticas de vida útil de la interfaz disponibles",
"show_latest": "Últimas estadísticas",
"show_latest": "Mostrar las últimas estadísticas JSON",
"title": "estadística"
},
"status": {
@@ -336,9 +198,6 @@
"uptime": "Tiempo de actividad",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"system": {
"error_fetching": "Error al obtener información del sistema"
},
"trace": {
"choose_network": "Elija la red",
"directions": "Lanzar un rastreo remoto de este dispositivo por una duración específica o por una cantidad de paquetes",
@@ -346,7 +205,6 @@
"packets": "Paquetes",
"title": "Rastro",
"trace": "Rastro",
"trace_not_successful": "Seguimiento fallido: la puerta de enlace informó el siguiente error: {{error}}",
"wait_for_file": "¿Le gustaría esperar hasta que el archivo de seguimiento esté listo?",
"waiting_directions": "Espere el archivo de datos de seguimiento. Esto puede tomar algo de tiempo. Puede salir de la espera y recuperar el archivo de seguimiento de la tabla de comandos más tarde.",
"waiting_seconds": "Tiempo transcurrido: {{seconds}} segundos"
@@ -366,52 +224,5 @@
"upgrade": "Mejorar",
"wait_for_upgrade": "¿Le gustaría esperar a que finalice la actualización?",
"waiting_for_device": "Esperando que el dispositivo se vuelva a conectar"
},
"user": {
"avatar": "Tu avatar",
"avatar_file": "Tu avatar (máx. De 2 MB)",
"create": "Crear usuario",
"create_failure": "Error al crear usuario. Asegúrese de que esta dirección de correo electrónico no esté vinculada a una cuenta.",
"create_success": "Usuario creado con éxito",
"creating": "Creando usuario ...",
"delete_avatar": "Eliminar avatar",
"delete_failure": "Error al intentar eliminar al usuario",
"delete_success": "¡Usuario eliminado correctamente!",
"delete_title": "Borrar usuario",
"delete_warning": "Advertencia: una vez que elimina un usuario, no puede revertir",
"deleting": "Eliminando ...",
"description": "Descripción",
"edit": "editar usuario",
"email_address": "Dirección de correo electrónico",
"force_password_change": "Forzar cambio de contraseña al iniciar sesión",
"id": "Id. De usuario",
"last_login": "Último acceso",
"login_id": "Ingresar identificación.",
"my_profile": "Mi perfil",
"name": "Nombre",
"nickname": "Apodo",
"nickname_explanation": "Apodo (opcional)",
"not_validated": "No validado",
"note": "Nota",
"password": "Contraseña",
"provide_email": "Por favor ingrese su dirección de correo electrónico válida",
"provide_password": "Proporcione una contraseña válida",
"save_avatar": "Guardar avatar",
"show_hide_password": "Mostrar / Ocultar contraseña",
"update_failure": "Asegúrese de que todos sus datos sean válidos. Si está modificando la contraseña, asegúrese de que no sea antigua.",
"update_failure_title": "Actualización fallida",
"update_success": "Usuario actualizado con éxito",
"update_success_title": "Éxito",
"user_role": "papel",
"users": "Usuarios",
"validated": "Validado"
},
"wifi_analysis": {
"association": "Asociación",
"associations": "Asociaciones",
"mode": "Modo",
"network_diagram": "Diagrama de Red",
"radios": "Radios",
"title": "Análisis de Wi-Fi"
}
}

View File

@@ -1,6 +1,5 @@
{
"actions": {
"actions": "actes",
"blink": "Cligner",
"configure": "Configurer",
"connect": "Relier",
@@ -22,25 +21,18 @@
},
"commands": {
"error": "Erreur lors de la soumission de la commande !",
"event_queue": "File d'attente d'événements",
"success": "Commande soumise avec succès, vous pouvez consulter le journal des commandes pour le résultat",
"title": "Historique des commandes",
"unable_queue": "Impossible de terminer la demande de file d'attente d'événements"
"title": "Historique des commandes"
},
"common": {
"access_policy": "Politique d'accès",
"add": "Ajouter",
"adding_ellipsis": "Ajouter...",
"are_you_sure": "Êtes-vous sûr?",
"back_to_login": "Retour connexion",
"cancel": "annuler",
"certificate": "Certificat",
"certificates": "Certificats",
"clear": "Clair",
"close": "Fermer",
"command": "Commander",
"commands": "Les commandes",
"commands_executed": "commandes exécutées",
"compatible": "Compatible",
"completed": "Terminé",
"config_id": "Config. Identifiant",
@@ -48,109 +40,63 @@
"connected": "Connecté",
"copied": "Copié!",
"copy_to_clipboard": "Copier dans le presse-papier",
"create": "Créer",
"created": "Créé",
"created_by": "Créé par",
"current": "Actuel",
"custom_date": "Date personnalisée",
"dashboard": "Tableau de bord",
"date": "Rendez-vous amoureux",
"day": "journée",
"days": "journées",
"delete": "Effacer",
"delete_device": "Supprimer le périphérique",
"details": "Détails",
"device": "N° d'appareil{{serialNumber}}",
"device_dashboard": "Tableau de bord de l'appareil",
"device_delete": "Supprimer l'appareil n°{{serialNumber}}",
"device_deleted": "Appareil supprimé avec succès",
"device_health": "Santé de l'appareil",
"device_list": "Liste des appareils",
"device_page": "Vue",
"device_status": "Statut du périphérique",
"device_page": "Page de l'appareil",
"devices": "Dispositifs",
"devices_using_latest": "Appareils utilisant le dernier micrologiciel",
"devices_using_unknown": "Périphériques utilisant un micrologiciel inconnu",
"dismiss": "Rejeter",
"do_now": "Faire maintenant!",
"download": "Télécharger",
"duration": "Durée",
"edit": "modifier",
"edit_user": "Modifier",
"email_address": "Adresse électronique",
"endpoint": "Point final",
"endpoints": "Points de terminaison",
"error": "Erreur",
"error": "erreur",
"execute_now": "Souhaitez-vous exécuter cette commande maintenant ?",
"executed": "réalisé",
"exit": "Sortie",
"firmware": "Micrologiciel",
"firmware_dashboard": "Tableau de bord du micrologiciel",
"firmware_installed": "Micrologiciel installé",
"forgot_password": "Mot de passe oublié?",
"forgot_password_title": "Mot de passe oublié",
"from": "De",
"general_error": "Erreur API, veuillez consulter votre administrateur",
"hide": "Cacher",
"hour": "heure",
"hours": "heures",
"id": "Id",
"ip_address": "Adresse IP",
"last_dashboard_refresh": "Dernière actualisation du tableau de bord",
"later_tonight": "Plus tard ce soir",
"latest": "Dernier",
"list": "liste",
"loading_ellipsis": "Chargement...",
"loading_more_ellipsis": "Chargement plus ...",
"logout": "Connectez - Out",
"mac": "ADRESSE MAC",
"manufacturer": "fabricant",
"memory_used": "Mémoire utilisée",
"minute": "minute",
"minutes": "minutes",
"na": "N / A",
"need_date": "Vous avez besoin d'un rendez-vous...",
"no": "Non",
"no_devices_found": "Aucun périphérique trouvé",
"no_items": "Pas d'objet",
"not_connected": "Pas connecté",
"of_connected": "% d'appareils",
"off": "De",
"on": "sur",
"optional": "Optionnel",
"overall_health": "Santé globale",
"password_policy": "Politique de mot de passe",
"recorded": "Enregistré",
"refresh": "Rafraîchir",
"refresh_device": "Actualiser l'appareil",
"required": "Champs obligatoires",
"result": "Résultat",
"save": "Sauvegarder",
"saved": "Enregistré!",
"saving": "Économie...",
"schedule": "Programme",
"search": "Rechercher des appareils",
"second": "seconde",
"seconds": "secondes",
"seconds_elapsed": "Secondes écoulées",
"serial_number": "Numéro de série",
"show_all": "Montre tout",
"start": "Début",
"submit": "Soumettre",
"submitted": "Soumis",
"success": "Succès",
"system": "Système",
"table": "Table",
"timestamp": "Temps",
"to": "à",
"type": "Type",
"unable_to_connect": "Impossible de se connecter à l'appareil",
"unable_to_delete": "Impossible de supprimer",
"unknown": "Inconnu",
"up_to_date": "Appareils à jour",
"uptimes": "Disponibilités",
"uuid": "UUID",
"vendors": "Vendeurs",
"view_more": "Afficher plus",
"yes": "Oui"
},
@@ -166,7 +112,6 @@
"owner": "Propriétaire",
"title": "Configuration",
"type": "Type d'appareil",
"uuid": "Identifiant de configuration",
"view_json": "Afficher le JSON brut"
},
"configure": {
@@ -191,20 +136,6 @@
"severity": "Gravité",
"title": "Journaux"
},
"entity": {
"add_child": "Ajouter une entité enfant à {{entityName}}",
"add_failure": "Erreur, le serveur a renvoyé : {{error}}",
"add_root": "Ajouter une entité racine",
"add_success": "Entité créée avec succès !",
"cannot_delete": "Vous ne pouvez pas supprimer des entités qui ont des enfants. Supprimez les enfants de cette entité pour pouvoir la supprimer.",
"delete_success": "Entité supprimée avec succès",
"delete_warning": "Attention : cette opération ne peut pas être annulée",
"edit_failure": "Échec de la mise à jour : {{error}}",
"entities": "Entités",
"entity": "Entité",
"only_unassigned": "Uniquement non attribué",
"valid_serial": "Doit être un numéro de série valide (12 caractères HEX)"
},
"factory_reset": {
"redirector": "Conserver le redirecteur :",
"reset": "Réinitialiser",
@@ -212,35 +143,6 @@
"title": "Retour aux paramètres d'usine",
"warning": "Avertissement : Une fois que vous avez soumis, cela ne peut pas être annulé"
},
"firmware": {
"average_age": "Âge moyen du micrologiciel",
"choose_custom": "Choisir",
"details_title": "Image #{{image}} Détails",
"device_type": "Type d'appareil",
"device_types": "Types d'appareils",
"downloads": "Téléchargements",
"error_fetching_latest": "Erreur lors de la récupération du dernier firmware",
"from_release": "De",
"history_title": "Historique",
"image": "Image",
"image_date": "Date de l'image",
"installed_firmware": "Micrologiciel installé",
"latest_version_installed": "Dernière version installée",
"newer_firmware_available": "Révisions plus récentes disponibles",
"reinstall_latest": "Réinstaller",
"revision": "Révision",
"show_dev": "Afficher les versions des développeurs",
"size": "Taille",
"status": "État du micrologiciel",
"title": "Micrologiciel",
"to_release": "à",
"unknown_firmware_status": "État du micrologiciel inconnu",
"upgrade": "Améliorer",
"upgrade_command_submitted": "Commande de mise à niveau soumise avec succès",
"upgrade_to_latest": "Dernier",
"upgrade_to_version": "Mettre à niveau vers cette révision",
"upgrading": "Mise à niveau..."
},
"footer": {
"coreui_for_react": "CoreUI pour React",
"powered_by": "Alimenté par",
@@ -250,48 +152,13 @@
"sanity": "Santé mentale",
"title": "Santé"
},
"inventory": {
"add_child_venue": "Ajouter un lieu enfant à {{entityName}}",
"add_tag": "Ajouter une étiquette",
"add_tag_to": "Ajouter une balise d'inventaire à {{name}}",
"assign_error": "Erreur lors de la tentative d'attribution de balise",
"assign_to_entity": "Affecter à l'entité",
"error_retrieving": "Une erreur s'est produite lors de la récupération des balises d'inventaire",
"error_unassign": "Erreur lors de l'opération de désaffectation",
"subscriber": "Abonné",
"successful_assign": "Tag attribué avec succès",
"successful_tag_update": "Balise mise à jour avec succès",
"successful_unassign": "L'opération de désaffectation a réussi",
"tag_created": "Tag d'inventaire créé avec succès",
"tag_creation_error": "Erreur lors de la tentative de création d'une balise d'inventaire",
"tag_update_error": "Erreur lors de la mise à jour de la balise",
"tags_assigned_to": "Balises d'inventaire attribuées à {{name}}",
"unassign": "Annuler l'attribution",
"unassign_tag": "Désaffecter la balise de l'entité",
"unassigned_tags": "Balises non attribuées",
"venue": "Lieu"
},
"login": {
"change_password": "Changer le mot de passe",
"change_password_error": "Erreur lors du changement de mot de passe. Assurez-vous que le nouveau mot de passe est valide en visitant la page « Politique de mot de passe »",
"change_password_instructions": "Saisissez et confirmez votre nouveau mot de passe",
"changing_password": "Modification du mot de passe...",
"confirm_new_password": "Confirmer le nouveau mot de passe",
"different_passwords": "Vous devez saisir deux fois le même mot de passe",
"forgot_password_error": "Erreur lors de la tentative d'envoi de l'e-mail Mot de passe oublié. Veuillez vous assurer que cet identifiant est associé à un compte.",
"forgot_password_explanation": "Entrez votre nom d'utilisateur pour recevoir un e-mail contenant les instructions pour réinitialiser votre mot de passe",
"forgot_password_success": "Vous devriez bientôt recevoir un e-mail contenant les instructions pour réinitialiser votre mot de passe. S'il vous plaît assurez-vous de vérifier vos spams si vous ne trouvez pas l'e-mail",
"logging_in": "Se connecter...",
"login": "S'identifier",
"login_error": "Erreur de connexion, assurez-vous que les informations que vous fournissez sont valides",
"new_password": "Nouveau mot de passe",
"login_error": "Erreur de connexion, confirmez que votre nom d'utilisateur, mot de passe et URL de passerelle sont valides",
"password": "Mot de passe",
"please_enter_gateway": "Veuillez saisir une URL uCentralSec",
"please_enter_password": "s'il vous plait entrez votre mot de passe",
"please_enter_username": "s'il vous plaît entrez votre nom d'utilisateur",
"previously_used": "Le mot de passe a déjà été utilisé",
"send_forgot": "Envoyer un email",
"sending_ellipsis": "Envoi...",
"sign_in_to_account": "Connectez-vous à votre compte",
"url": "URL uCentralSec",
"username": "Nom d'utilisateur"
@@ -312,15 +179,10 @@
"scanning": "Balayage... ",
"waiting_directions": "Veuillez attendre le résultat de l'analyse. Cela peut prendre jusqu'à 25 secondes. Vous pouvez quitter et consulter les résultats du tableau des commandes plus tard."
},
"settings": {
"title": "Réglages"
},
"statistics": {
"data": "Données (Ko)",
"latest_statistics": "Dernières statistiques",
"lifetime_stats": "Statistiques à vie",
"no_interfaces": "Aucune statistique de durée de vie de l'interface disponible",
"show_latest": "Dernières statistiques",
"show_latest": "Afficher les dernières statistiques JSON",
"title": "statistiques"
},
"status": {
@@ -336,9 +198,6 @@
"uptime": "La disponibilité",
"used_total_memory": "{{used}} utilisé / {{total}} total"
},
"system": {
"error_fetching": "Erreur lors de la récupération des informations système"
},
"trace": {
"choose_network": "Choisir le réseau",
"directions": "Lancer une trace à distance de cet appareil pour une durée spécifique ou un nombre de paquets",
@@ -346,7 +205,6 @@
"packets": "Paquets",
"title": "Trace",
"trace": "Trace",
"trace_not_successful": "Trace non réussie : la passerelle a signalé l'erreur suivante : {{error}}",
"wait_for_file": "Souhaitez-vous attendre que le fichier de trace soit prêt ?",
"waiting_directions": "Veuillez attendre le fichier de données de trace. Cela peut prendre un certain temps. Vous pouvez quitter l'attente et récupérer le fichier de trace de la table des commandes plus tard.",
"waiting_seconds": "Temps écoulé : {{seconds}} secondes"
@@ -366,52 +224,5 @@
"upgrade": "Améliorer",
"wait_for_upgrade": "Souhaitez-vous attendre la fin de la mise à niveau ?",
"waiting_for_device": "En attente de la reconnexion de l'appareil"
},
"user": {
"avatar": "Votre avatar",
"avatar_file": "Votre Avatar (max. de 2 Mo)",
"create": "Créer un utilisateur",
"create_failure": "Erreur lors de la création de l'utilisateur. Veuillez vous assurer que cette adresse e-mail n'est pas déjà liée à un compte.",
"create_success": "L'utilisateur a été créé avec succès",
"creating": "Création de l'utilisateur...",
"delete_avatar": "Supprimer l'avatar",
"delete_failure": "Erreur lors de la tentative de suppression de l'utilisateur",
"delete_success": "Utilisateur supprimé avec succès !",
"delete_title": "Supprimer l'utilisateur",
"delete_warning": "Avertissement : Une fois que vous avez supprimé un utilisateur, vous ne pouvez plus revenir en arrière",
"deleting": "Suppression ...",
"description": "La description",
"edit": "Modifier l'utilisateur",
"email_address": "Adresse électronique",
"force_password_change": "Forcer le changement de mot de passe lors de la connexion",
"id": "Identifiant d'utilisateur.",
"last_login": "Dernière connexion",
"login_id": "Identifiant de connexion.",
"my_profile": "Mon profil",
"name": "Prénom",
"nickname": "Surnom",
"nickname_explanation": "Surnom (optionnel)",
"not_validated": "Pas valide",
"note": "Remarque",
"password": "Mot de passe",
"provide_email": "Veuillez fournir une adresse email valide",
"provide_password": "Veuillez fournir un mot de passe valide",
"save_avatar": "Enregistrer l'avatar",
"show_hide_password": "Afficher/Masquer le mot de passe",
"update_failure": "Assurez-vous que toutes vos données sont valides. Si vous modifiez le mot de passe, assurez-vous qu'il ne s'agit pas d'un ancien.",
"update_failure_title": "mise à jour a échoué",
"update_success": "L'utilisateur a bien été mis à jour",
"update_success_title": "Succès",
"user_role": "Rôle",
"users": "Utilisateurs",
"validated": "Validé"
},
"wifi_analysis": {
"association": "Association",
"associations": "Les associations",
"mode": "Mode",
"network_diagram": "Diagramme de réseau",
"radios": "Radios",
"title": "Analyse Wi-Fi"
}
}

View File

@@ -1,6 +1,5 @@
{
"actions": {
"actions": "Ações",
"blink": "Piscar",
"configure": "Configurar",
"connect": "Conectar",
@@ -10,7 +9,7 @@
"reboot": "Reiniciar",
"title": "Comandos",
"trace": "Vestígio",
"wifi_scan": "Wi-Fi Scan"
"wifi_scan": "Wifi Scan"
},
"blink": {
"blink": "Piscar",
@@ -22,25 +21,18 @@
},
"commands": {
"error": "Erro ao enviar comando!",
"event_queue": "Fila de Eventos",
"success": "Comando enviado com sucesso, você pode consultar o log de Comandos para ver o resultado",
"title": "Histórico de Comandos",
"unable_queue": "Incapaz de completar o pedido de fila de eventos"
"title": "Histórico de Comandos"
},
"common": {
"access_policy": "Política de Acesso",
"add": "Adicionar",
"adding_ellipsis": "Adicionando ...",
"are_you_sure": "Você tem certeza?",
"back_to_login": "Volte ao login",
"cancel": "Cancelar",
"certificate": "Certificado",
"certificates": "Certificados",
"clear": "Claro",
"close": "Perto",
"command": "Comando",
"commands": "comandos",
"commands_executed": "Comandos Executados",
"compatible": "Compatível",
"completed": "Concluído",
"config_id": "Config. Identidade",
@@ -48,109 +40,63 @@
"connected": "Conectado",
"copied": "Copiado!",
"copy_to_clipboard": "Copiar para área de transferência",
"create": "Crio",
"created": "Criado",
"created_by": "Criado Por",
"current": "Atual",
"custom_date": "Data personalizada",
"dashboard": "painel de controle",
"date": "Encontro",
"day": "dia",
"days": "dias",
"delete": "Excluir",
"delete_device": "Apagar dispositivo",
"details": "Detalhes",
"device": "Dispositivo nº{{serialNumber}}",
"device_dashboard": "Painel do dispositivo",
"device_delete": "Excluir dispositivo nº{{serialNumber}}",
"device_deleted": "Dispositivo excluído com sucesso",
"device_health": "Saúde do Dispositivo",
"device_list": "Lista de Dispositivos",
"device_page": "Visão",
"device_status": "Status do dispositivo",
"device_page": "Página do dispositivo",
"devices": "Devices",
"devices_using_latest": "Dispositivos que usam o firmware mais recente",
"devices_using_unknown": "Dispositivos que usam firmware desconhecido",
"dismiss": "Dispensar",
"do_now": "Faça agora!",
"download": "Baixar",
"duration": "Duração",
"edit": "Editar",
"edit_user": "Editar",
"email_address": "Endereço de e-mail",
"endpoint": "Ponto final",
"endpoints": "Pontos finais",
"error": "Erro",
"execute_now": "Você gostaria de executar este comando agora?",
"executed": "Executado",
"exit": "Saída",
"firmware": "Firmware",
"firmware_dashboard": "Painel de Firmware",
"firmware_installed": "Firmware Instalado",
"forgot_password": "Esqueceu sua senha?",
"forgot_password_title": "Esqueceu a senha",
"from": "De",
"general_error": "Erro de API, consulte o seu administrador",
"hide": "Ocultar",
"hour": "hora",
"hours": "horas",
"id": "identidade",
"ip_address": "Endereço de IP",
"last_dashboard_refresh": "Última atualização do painel",
"later_tonight": "Logo à noite",
"latest": "Mais recentes",
"list": "Lista",
"loading_ellipsis": "Carregando...",
"loading_more_ellipsis": "Carregando mais ...",
"logout": "Sair",
"mac": "Endereço MAC",
"manufacturer": "Fabricante",
"memory_used": "Memória Usada",
"minute": "minuto",
"minutes": "minutos",
"na": "N / D",
"need_date": "Você precisa de um encontro ...",
"no": "Não",
"no_devices_found": "Nenhum dispositivo encontrado",
"no_items": "Nenhum item",
"not_connected": "Não conectado",
"of_connected": "% de dispositivos",
"off": "Fora",
"on": "em",
"optional": "Opcional",
"overall_health": "Saúde geral",
"password_policy": "Política de Senha",
"recorded": "Gravado",
"refresh": "REFRESH",
"refresh_device": "Atualizar dispositivo",
"required": "Requeridos",
"result": "Resultado",
"save": "Salve",
"saved": "Salvou!",
"saving": "Salvando ...",
"schedule": "Cronograma",
"search": "Dispositivos de pesquisa",
"second": "segundo",
"seconds": "segundos",
"seconds_elapsed": "Segundos decorridos",
"serial_number": "Número de série",
"show_all": "mostre tudo",
"start": "Começar",
"submit": "Enviar",
"submitted": "Submetido",
"success": "Sucesso",
"system": "Sistema",
"table": "Mesa",
"timestamp": "tempo",
"to": "Para",
"type": "Tipo",
"unable_to_connect": "Incapaz de conectar ao dispositivo",
"unable_to_delete": "Incapaz de deletar",
"unknown": "Desconhecido",
"up_to_date": "Dispositivos atualizados",
"uptimes": "Uptimes",
"uuid": "UUID",
"vendors": "Vendedores",
"view_more": "Veja mais",
"yes": "sim"
},
@@ -166,7 +112,6 @@
"owner": "Proprietário",
"title": "Configuração",
"type": "Tipo de dispositivo",
"uuid": "ID de configuração",
"view_json": "Exibir JSON bruto"
},
"configure": {
@@ -191,20 +136,6 @@
"severity": "Gravidade",
"title": "Toras"
},
"entity": {
"add_child": "Adicionar Entidade Filha a {{entityName}}",
"add_failure": "Erro, o servidor retornou: {{error}}",
"add_root": "Adicionar Entidade Raiz",
"add_success": "Entidade criada com sucesso!",
"cannot_delete": "Você não pode excluir entidades que têm filhos. Exclua os filhos desta entidade para poder excluí-la.",
"delete_success": "Entidade excluída com sucesso",
"delete_warning": "Aviso: esta operação não pode ser revertida",
"edit_failure": "Atualização malsucedida: {{error}}",
"entities": "Entidades",
"entity": "Entidade",
"only_unassigned": "Apenas não atribuídos",
"valid_serial": "Precisa ser um número de série válido (12 caracteres HEX)"
},
"factory_reset": {
"redirector": "Manter redirecionador:",
"reset": "Restabelecer",
@@ -212,35 +143,6 @@
"title": "Restauração de fábrica",
"warning": "Aviso: depois de enviar, isso não pode ser revertido"
},
"firmware": {
"average_age": "Idade Média do Firmware",
"choose_custom": "Escolher",
"details_title": "Detalhes da imagem #{{image}} ",
"device_type": "Tipo de dispositivo",
"device_types": "Tipos de dispositivos",
"downloads": "Transferências",
"error_fetching_latest": "Erro ao buscar o firmware mais recente",
"from_release": "De",
"history_title": "História",
"image": "Imagem",
"image_date": "Data da Imagem",
"installed_firmware": "Firmware Instalado",
"latest_version_installed": "Última versão instalada",
"newer_firmware_available": "Novas revisões disponíveis",
"reinstall_latest": "Reinstalar",
"revision": "Revisão",
"show_dev": "Mostrar lançamentos de desenvolvimento",
"size": "Tamanho",
"status": "Status do firmware",
"title": "Firmware",
"to_release": "Para",
"unknown_firmware_status": "Status de firmware desconhecido",
"upgrade": "Melhorar",
"upgrade_command_submitted": "Comando de atualização enviado com sucesso",
"upgrade_to_latest": "Mais recentes",
"upgrade_to_version": "Atualize para esta revisão",
"upgrading": "Atualizando ..."
},
"footer": {
"coreui_for_react": "CoreUI para React",
"powered_by": "Distribuído por",
@@ -250,48 +152,13 @@
"sanity": "Sanidade",
"title": "Saúde"
},
"inventory": {
"add_child_venue": "Adicionar Local Infantil a {{entityName}}",
"add_tag": "Adicionar etiqueta",
"add_tag_to": "Adicionar tag de estoque a {{name}}",
"assign_error": "Erro ao tentar atribuir tag",
"assign_to_entity": "Atribuir à Entidade",
"error_retrieving": "Ocorreu um erro ao recuperar as tags de inventário",
"error_unassign": "Erro durante operação de cancelamento de atribuição",
"subscriber": "Assinante",
"successful_assign": "Tag atribuída com sucesso",
"successful_tag_update": "Tag atualizada com sucesso",
"successful_unassign": "A operação de cancelamento da atribuição foi bem-sucedida",
"tag_created": "Tag de inventário criada com sucesso",
"tag_creation_error": "Erro ao tentar criar etiqueta de inventário",
"tag_update_error": "Erro ao atualizar tag",
"tags_assigned_to": "Tags de inventário atribuídas a {{name}}",
"unassign": "Cancelar atribuição",
"unassign_tag": "Cancelar a atribuição de tag da entidade",
"unassigned_tags": "Tags não atribuídas",
"venue": "Local"
},
"login": {
"change_password": "Mudar senha",
"change_password_error": "Erro ao alterar a senha. Certifique-se de que a nova senha é válida visitando a página 'Política de senha'",
"change_password_instructions": "Digite e confirme sua nova senha",
"changing_password": "Alterando senha ...",
"confirm_new_password": "confirme a nova senha",
"different_passwords": "Você precisa inserir a mesma senha duas vezes",
"forgot_password_error": "Erro ao tentar enviar e-mail Esqueci a senha. Certifique-se de que este userId esteja associado a uma conta.",
"forgot_password_explanation": "Digite seu nome de usuário para receber um e-mail contendo as instruções para redefinir sua senha",
"forgot_password_success": "Em breve, você receberá um e-mail com as instruções para redefinir sua senha. Certifique-se de verificar o seu spam se você não conseguir encontrar o e-mail",
"logging_in": "Fazendo login ...",
"login": "Entrar",
"login_error": "Erro de login, certifique-se de que as informações que você está fornecendo são válidas",
"new_password": "Nova senha",
"login_error": "Erro de login, confirme se seu nome de usuário, senha e url de gateway são válidos",
"password": "Senha",
"please_enter_gateway": "Insira um URL uCentralSec",
"please_enter_password": "Por favor, insira sua senha",
"please_enter_username": "Por favor insira seu nome de usuário",
"previously_used": "A senha foi usada anteriormente",
"send_forgot": "ENVIAR EMAIL",
"sending_ellipsis": "Enviando ...",
"sign_in_to_account": "Faça login em sua conta",
"url": "URL uCentralSec",
"username": "Nome de usuário"
@@ -307,20 +174,15 @@
"directions": "Inicie uma verificação de wi-fi deste dispositivo, o que deve levar aproximadamente 25 segundos.",
"re_scan": "Verificar novamente",
"result_directions": "Clique no botão '$ t (scan.re_scan)' se desejar fazer uma varredura com a mesma configuração da anterior.",
"results": "Resultados da verificação de Wi-Fi",
"results": "Resultados da verificação de wi-fi",
"scan": "Varredura",
"scanning": "Scanning... ",
"waiting_directions": "Por favor, aguarde o resultado da verificação. Isso pode levar até 25 segundos. Você pode sair e ver os resultados da tabela de comandos mais tarde."
},
"settings": {
"title": "Definições"
},
"statistics": {
"data": "Dados (KB)",
"latest_statistics": "Estatísticas mais recentes",
"lifetime_stats": "Estatísticas de vida",
"no_interfaces": "Nenhuma estatística de tempo de vida da interface disponível",
"show_latest": "Últimas estatísticas",
"show_latest": "Mostrar estatísticas mais recentes JSON",
"title": "Estatisticas"
},
"status": {
@@ -336,9 +198,6 @@
"uptime": "Tempo de atividade",
"used_total_memory": "{{used}} usado / {{total}} total"
},
"system": {
"error_fetching": "Erro ao buscar informações do sistema"
},
"trace": {
"choose_network": "Escolha a rede",
"directions": "Lançar um rastreamento remoto deste dispositivo para uma duração específica ou um número de pacotes",
@@ -346,7 +205,6 @@
"packets": "Pacotes",
"title": "Vestígio",
"trace": "Vestígio",
"trace_not_successful": "O rastreamento não foi bem-sucedido: o gateway relatou o seguinte erro: {{error}}",
"wait_for_file": "Você gostaria de esperar até que o arquivo de rastreamento esteja pronto?",
"waiting_directions": "Aguarde o arquivo de dados de rastreamento. Isto pode tomar algum tempo. Você pode sair da espera e recuperar o arquivo de rastreamento da tabela de comandos mais tarde.",
"waiting_seconds": "Tempo decorrido: {{seconds}} segundos"
@@ -366,52 +224,5 @@
"upgrade": "Melhorar",
"wait_for_upgrade": "Você gostaria de esperar a conclusão da atualização?",
"waiting_for_device": "Esperando que o dispositivo se reconecte"
},
"user": {
"avatar": "Seu avatar",
"avatar_file": "Seu avatar (máx. De 2 MB)",
"create": "Criar usuário",
"create_failure": "Erro ao criar usuário. Certifique-se de que este endereço de e-mail ainda não esteja vinculado a uma conta.",
"create_success": "Usuário criado com sucesso",
"creating": "Criando usuário ...",
"delete_avatar": "Apagar Avatar",
"delete_failure": "Erro ao tentar excluir usuário",
"delete_success": "Usuário excluído com sucesso!",
"delete_title": "Deletar usuário",
"delete_warning": "Aviso: depois de excluir um usuário, você não pode reverter",
"deleting": "Excluindo ...",
"description": "Descrição",
"edit": "Editar usuário",
"email_address": "Endereço de e-mail",
"force_password_change": "Forçar mudança de senha no login",
"id": "ID do usuário.",
"last_login": "Último login",
"login_id": "Identificação de usuário.",
"my_profile": "Meu perfil",
"name": "Nome",
"nickname": "Apelido",
"nickname_explanation": "Apelido (opcional)",
"not_validated": "Não validado",
"note": "Nota",
"password": "Senha",
"provide_email": "Por favor, forneça um endereço de e-mail válido",
"provide_password": "Forneça uma senha válida",
"save_avatar": "Salvar Avatar",
"show_hide_password": "Mostrar / ocultar senha",
"update_failure": "Certifique-se de que todos os seus dados são válidos. Se você estiver modificando a senha, certifique-se de que não seja uma senha antiga.",
"update_failure_title": "Atualização falhou",
"update_success": "Usuário atualizado com sucesso",
"update_success_title": "Sucesso",
"user_role": "Função",
"users": "Comercial",
"validated": "Validado"
},
"wifi_analysis": {
"association": "Associação",
"associations": "Associações",
"mode": "Modo",
"network_diagram": "Diagrama de rede",
"radios": "Rádios",
"title": "Análise de Wi-Fi"
}
}

View File

@@ -2,9 +2,8 @@ import React from 'react';
import { HashRouter, Switch } from 'react-router-dom';
import 'scss/style.scss';
import Router from 'router';
import { AuthProvider } from 'ucentral-libs';
import { AuthProvider } from 'contexts/AuthProvider';
import { checkIfJson } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
const loading = (
<div className="pt-3 text-center">
@@ -19,11 +18,7 @@ const App = () => {
: {};
return (
<AuthProvider
axiosInstance={axiosInstance}
token={storageToken ?? ''}
apiEndpoints={apiEndpoints}
>
<AuthProvider token={storageToken ?? ''} apiEndpoints={apiEndpoints}>
<HashRouter>
<React.Suspense fallback={loading}>
<Switch>

View File

@@ -0,0 +1,32 @@
export const logo = [
'608 134',
`
<title>coreui react pro</title>
<g>
<g style="fill:#00a1ff">
<path d="M362.0177,90.1512,353.25,69.4149a.2507.2507,0,0,0-.2559-.1914H343.01a.2263.2263,0,0,0-.2559.2559V90.0233a.5657.5657,0,0,1-.64.64h-1.2163a.5652.5652,0,0,1-.64-.64V46.5028a.5655.5655,0,0,1,.64-.64H353.442a9.9792,9.9792,0,0,1,7.7437,3.2324A12.2,12.2,0,0,1,364.13,57.64a12.4389,12.4389,0,0,1-2.24,7.584,9.37,9.37,0,0,1-6.08,3.7441c-.1709.086-.2139.1915-.128.3194l8.7041,20.6084.064.2558q0,.5127-.5757.5118h-1.1523A.703.703,0,0,1,362.0177,90.1512ZM342.754,48.3593v18.496a.2259.2259,0,0,0,.2559.2559h10.3037a7.6713,7.6713,0,0,0,6.0166-2.5918,9.8807,9.8807,0,0,0,2.3037-6.8164,10.2875,10.2875,0,0,0-2.272-6.9756,7.6033,7.6033,0,0,0-6.0483-2.624H343.01A.2263.2263,0,0,0,342.754,48.3593Z"/>
<path d="M401.3263,48.1034H381.2945a.2262.2262,0,0,0-.2558.2559v18.496a.2259.2259,0,0,0,.2558.2559h13.8238a.5664.5664,0,0,1,.6406.64v.96a.5663.5663,0,0,1-.6406.6406H381.2945a.2263.2263,0,0,0-.2558.2559v18.56a.2258.2258,0,0,0,.2558.2558h20.0318a.5671.5671,0,0,1,.6406.6407v.96a.566.566,0,0,1-.6406.64H379.1827a.5653.5653,0,0,1-.64-.64V46.5028a.5656.5656,0,0,1,.64-.64h22.1436a.5664.5664,0,0,1,.6406.64v.96A.5663.5663,0,0,1,401.3263,48.1034Z"/>
<path d="M439.047,90.1512l-2.4317-8.832a.2971.2971,0,0,0-.32-.1924H419.5274a.2957.2957,0,0,0-.32.1924l-2.3681,8.7676a.6577.6577,0,0,1-.7036.5762H414.919a.5385.5385,0,0,1-.5756-.7041l12.0317-43.584a.6436.6436,0,0,1,.7041-.5117h1.6a.6442.6442,0,0,1,.7041.5117l12.16,43.584.0644.1923q0,.5127-.64.5118h-1.2163A.6428.6428,0,0,1,439.047,90.1512ZM419.9435,78.9188a.3031.3031,0,0,0,.2236.0967h15.4883a.3048.3048,0,0,0,.2236-.0967c.0645-.0635.0742-.1162.0322-.1592l-7.872-28.9287c-.043-.0849-.086-.1279-.128-.1279s-.0859.043-.1279.1279L419.9112,78.76C419.8683,78.8026,419.879,78.8553,419.9435,78.9188Z"/>
<path d="M456.6017,87.911a11.6372,11.6372,0,0,1-3.3277-8.7041V57.1913a11.4158,11.4158,0,0,1,3.36-8.5762,12.0941,12.0941,0,0,1,8.8-3.2637,12.2566,12.2566,0,0,1,8.8643,3.2315,11.3927,11.3927,0,0,1,3.36,8.6084v.64a.5663.5663,0,0,1-.6406.6407l-1.28.0634q-.6408,0-.64-.5761v-.8321a9.289,9.289,0,0,0-2.6558-6.9121,10.6734,10.6734,0,0,0-14.0161,0,9.2854,9.2854,0,0,0-2.6563,6.9121V79.3993a9.2808,9.2808,0,0,0,2.6563,6.9121,10.67,10.67,0,0,0,14.0161,0,9.2843,9.2843,0,0,0,2.6558-6.9121v-.7686q0-.5757.64-.5752l1.28.0635a.5667.5667,0,0,1,.6406.6406v.5118a11.4952,11.4952,0,0,1-3.36,8.64,13.6227,13.6227,0,0,1-17.6963,0Z"/>
<path d="M514.4376,46.5028v.96a.5658.5658,0,0,1-.64.6406H503.046a.2263.2263,0,0,0-.2559.2559v41.664a.566.566,0,0,1-.6406.64h-1.2158a.5652.5652,0,0,1-.64-.64V48.3593a.2266.2266,0,0,0-.2558-.2559H489.8619a.5656.5656,0,0,1-.64-.6406v-.96a.5656.5656,0,0,1,.64-.64H513.798A.5658.5658,0,0,1,514.4376,46.5028Z"/>
<path d="M522.0665,89.5116a2.8385,2.8385,0,0,1-.8-2.0488,2.9194,2.9194,0,0,1,.8-2.1114,2.7544,2.7544,0,0,1,2.08-.832,2.8465,2.8465,0,0,1,2.9438,2.9434,2.7541,2.7541,0,0,1-.832,2.08,2.9221,2.9221,0,0,1-2.1118.8008A2.754,2.754,0,0,1,522.0665,89.5116Z"/>
<path d="M542.4054,88.0077a11.3123,11.3123,0,0,1-3.2-8.416v-5.44a.5656.5656,0,0,1,.64-.64h1.2158a.5661.5661,0,0,1,.64.64v5.5039a9.1424,9.1424,0,0,0,2.5283,6.72,8.9745,8.9745,0,0,0,6.6875,2.5605,8.7908,8.7908,0,0,0,9.28-9.28V46.5028a.5655.5655,0,0,1,.64-.64h1.2163a.566.566,0,0,1,.64.64V79.5917a11.2545,11.2545,0,0,1-3.2325,8.416,13.0618,13.0618,0,0,1-17.0556,0Z"/>
<path d="M580.35,88.1034a10.4859,10.4859,0,0,1-3.36-8.1279v-1.792a.5663.5663,0,0,1,.64-.6407h1.0884a.5668.5668,0,0,1,.64.6407v1.6a8.5459,8.5459,0,0,0,2.752,6.6562,10.5353,10.5353,0,0,0,7.36,2.4961,9.8719,9.8719,0,0,0,6.9761-2.3681,8.2161,8.2161,0,0,0,2.56-6.336,8.4,8.4,0,0,0-1.12-4.416,11.3812,11.3812,0,0,0-3.3281-3.3926,71.6714,71.6714,0,0,0-6.1763-3.7119,71.0479,71.0479,0,0,1-6.24-3.84,12.1711,12.1711,0,0,1-3.4238-3.68,10.2614,10.2614,0,0,1-1.28-5.3438,9.8579,9.8579,0,0,1,3.0718-7.7441,12.0122,12.0122,0,0,1,8.32-2.752q5.6954,0,8.96,3.1036a10.8251,10.8251,0,0,1,3.2642,8.2246v1.6a.5658.5658,0,0,1-.64.64h-1.1519a.5652.5652,0,0,1-.64-.64V56.8075a8.8647,8.8647,0,0,0-2.624-6.6885,9.9933,9.9933,0,0,0-7.232-2.5273,9.37,9.37,0,0,0-6.5278,2.1435,7.8224,7.8224,0,0,0-2.3682,6.1123,7.8006,7.8006,0,0,0,1.0244,4.16,10.387,10.387,0,0,0,3.0078,3.0391,62.8714,62.8714,0,0,0,5.9522,3.4882,71.0575,71.0575,0,0,1,6.72,4.2559,13.4674,13.4674,0,0,1,3.648,3.9365,10.049,10.049,0,0,1,1.28,5.1836,10.7177,10.7177,0,0,1-3.2637,8.1924q-3.2637,3.0717-8.832,3.0723Q583.71,91.1757,580.35,88.1034Z"/>
</g>
<g style="fill:#3c4b64">
<g>
<path d="M99.835,36.0577l-39-22.5167a12,12,0,0,0-12,0l-39,22.5166a12.0339,12.0339,0,0,0-6,10.3924V91.4833a12.0333,12.0333,0,0,0,6,10.3923l39,22.5167a12,12,0,0,0,12,0l39-22.5167a12.0331,12.0331,0,0,0,6-10.3923V46.45A12.0334,12.0334,0,0,0,99.835,36.0577Zm-2,55.4256a4,4,0,0,1-2,3.4641l-39,22.5167a4.0006,4.0006,0,0,1-4,0l-39-22.5167a4,4,0,0,1-2-3.4641V46.45a4,4,0,0,1,2-3.4642l39-22.5166a4,4,0,0,1,4,0l39,22.5166a4,4,0,0,1,2,3.4642Z"/>
<path d="M77.8567,82.0046h-2.866a4,4,0,0,0-1.9247.4934L55.7852,91.9833,35.835,80.4648V57.4872l19.95-11.5185,17.2893,9.4549a3.9993,3.9993,0,0,0,1.9192.4906h2.8632a2,2,0,0,0,2-2V51.2024a2,2,0,0,0-1.04-1.7547L59.628,38.9521a8.0391,8.0391,0,0,0-7.8428.09L31.8346,50.56a8.0246,8.0246,0,0,0-4,6.9287v22.976a8,8,0,0,0,4,6.9283l19.95,11.5186a8.0429,8.0429,0,0,0,7.8433.0879l19.19-10.5312a2,2,0,0,0,1.0378-1.7533v-2.71A2,2,0,0,0,77.8567,82.0046Z"/>
</g>
<g>
<path d="M172.58,45.3618a15.0166,15.0166,0,0,0-15,14.9995V77.6387a15,15,0,0,0,30,0V60.3613A15.0166,15.0166,0,0,0,172.58,45.3618Zm7,32.2769a7,7,0,0,1-14,0V60.3613a7,7,0,0,1,14,0Z"/>
<path d="M135.9138,53.4211a7.01,7.01,0,0,1,7.8681,6.0752.9894.9894,0,0,0,.9843.865h6.03a1.0108,1.0108,0,0,0,.9987-1.0971,15.0182,15.0182,0,0,0-15.7162-13.8837,15.2881,15.2881,0,0,0-14.2441,15.4163V77.2037A15.288,15.288,0,0,0,136.0792,92.62a15.0183,15.0183,0,0,0,15.7162-13.8842,1.0107,1.0107,0,0,0-.9987-1.0971h-6.03a.9894.9894,0,0,0-.9843.865,7.01,7.01,0,0,1-7.8679,6.0757,7.1642,7.1642,0,0,1-6.0789-7.1849V60.6057A7.1638,7.1638,0,0,1,135.9138,53.4211Z"/>
<path d="M218.7572,72.9277a12.1585,12.1585,0,0,0,7.1843-11.0771V58.1494A12.1494,12.1494,0,0,0,213.7921,46H196.835a1,1,0,0,0-1,1V91a1,1,0,0,0,1,1h6a1,1,0,0,0,1-1V74h6.6216l7.9154,17.4138a1,1,0,0,0,.91.5862h6.5911a1,1,0,0,0,.91-1.4138Zm-.8157-11.0771A4.1538,4.1538,0,0,1,213.7926,66h-9.8511V54h9.8511a4.1538,4.1538,0,0,1,4.1489,4.1494Z"/>
<path d="M260.835,46h-26a1,1,0,0,0-1,1V91a1,1,0,0,0,1,1h26a1,1,0,0,0,1-1V85a1,1,0,0,0-1-1h-19V72h13a1,1,0,0,0,1-1V65a1,1,0,0,0-1-1h-13V54h19a1,1,0,0,0,1-1V47A1,1,0,0,0,260.835,46Z"/>
<path d="M298.835,46h-6a1,1,0,0,0-1,1V69.6475a7.0066,7.0066,0,1,1-14,0V47a1,1,0,0,0-1-1h-6a1,1,0,0,0-1,1V69.6475a15.0031,15.0031,0,1,0,30,0V47A1,1,0,0,0,298.835,46Z"/>
<rect x="307.835" y="46" width="8" height="38" rx="1"/>
</g>
</g>
</g>
`,
];

View File

@@ -1,4 +1,28 @@
import {
cibSkype,
cibFacebook,
cibTwitter,
cibLinkedin,
cibFlickr,
cibTumblr,
cibXing,
cibGithub,
cibStackoverflow,
cibYoutube,
cibDribbble,
cibInstagram,
cibPinterest,
cibVk,
cibYahoo,
cibBehance,
cibReddit,
cibVimeo,
cibCcMastercard,
cibCcVisa,
cibStripe,
cibPaypal,
cibGooglePay,
cibCcAmex,
cifUs,
cifBr,
cifIn,
@@ -99,7 +123,10 @@ import {
cilWarning,
} from '@coreui/icons';
import { logo } from './CoreuiLogo';
export const icons = {
logo,
cilAlignCenter,
cilAlignLeft,
cilAlignRight,
@@ -198,4 +225,28 @@ export const icons = {
cifFr,
cifEs,
cifPl,
cibSkype,
cibFacebook,
cibTwitter,
cibLinkedin,
cibFlickr,
cibTumblr,
cibXing,
cibGithub,
cibStackoverflow,
cibYoutube,
cibDribbble,
cibInstagram,
cibPinterest,
cibVk,
cibYahoo,
cibBehance,
cibReddit,
cibVimeo,
cibCcMastercard,
cibCcVisa,
cibStripe,
cibPaypal,
cibGooglePay,
cibCcAmex,
};

View File

@@ -18,10 +18,14 @@ import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { dateToUnix } from 'utils/helper';
import 'react-widgets/styles.css';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import styles from './index.module.scss';
const BlinkModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
@@ -139,9 +143,9 @@ const BlinkModal = ({ show, toggleModal }) => {
</CFormGroup>
</CCol>
</CFormGroup>
<CRow className="pt-1">
<CRow className={styles.spacedRow}>
<CCol md="8">
<p>{t('blink.execute_now')}</p>
<p className={styles.spacedText}>{t('blink.execute_now')}</p>
</CCol>
<CCol>
<CSwitch
@@ -154,8 +158,8 @@ const BlinkModal = ({ show, toggleModal }) => {
/>
</CCol>
</CRow>
<CRow hidden={isNow} className="pt-3">
<CCol md="4" className="pt-2">
<CRow hidden={isNow} className={styles.spacedRow}>
<CCol md="4" className={styles.spacedDate}>
<p>{t('common.custom_date')}</p>
</CCol>
<CCol xs="12" md="8">

View File

@@ -0,0 +1,7 @@
.spacedRow {
margin-top: 20px;
}
.spacedDate {
margin-top: 7px;
}

View File

@@ -1,36 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
CButton,
CModal,
CModalHeader,
CModalBody,
CModalTitle,
CModalFooter,
} from '@coreui/react';
const DetailsModal = ({ t, show, toggle, details, commandUuid }) => (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle className="text-dark">{commandUuid}</CModalTitle>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(details, null, 4)}</pre>
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
DetailsModal.propTypes = {
t: PropTypes.func.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
details: PropTypes.instanceOf(Object).isRequired,
commandUuid: PropTypes.string.isRequired,
};
export default DetailsModal;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { CCollapse, CCardBody } from '@coreui/react';
import PropTypes from 'prop-types';
import { Translation } from 'react-i18next';
const DeviceCommandsCollapse = ({ details, responses, index, item, getDetails, getResponse }) => (
<Translation>
{(t) => (
<div>
<CCollapse show={details.includes(index)}>
<CCardBody>
<h5>{t('common.result')}</h5>
<div>{getDetails(item, index)}</div>
</CCardBody>
</CCollapse>
<CCollapse show={responses.includes(index)}>
<CCardBody>
<h5>{t('common.details')}</h5>
<div>{getResponse(item, index)}</div>
</CCardBody>
</CCollapse>
</div>
)}
</Translation>
);
DeviceCommandsCollapse.propTypes = {
details: PropTypes.instanceOf(Array).isRequired,
responses: PropTypes.instanceOf(Array).isRequired,
index: PropTypes.number.isRequired,
getDetails: PropTypes.func.isRequired,
getResponse: PropTypes.func.isRequired,
item: PropTypes.instanceOf(Object).isRequired,
};
export default DeviceCommandsCollapse;

View File

@@ -16,11 +16,13 @@ import DatePicker from 'react-widgets/DatePicker';
import { cilCloudDownload, cilSync, cilCalendarCheck } from '@coreui/icons';
import { prettyDate, dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import eventBus from 'utils/eventBus';
import ConfirmModal from 'components/ConfirmModal';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import WifiScanResultModalWidget from 'components/WifiScanResultModal';
import DetailsModal from './DetailsModal';
import DeviceCommandsCollapse from './DeviceCommandsCollapse';
import styles from './index.module.scss';
const DeviceCommands = () => {
@@ -34,12 +36,11 @@ const DeviceCommands = () => {
// Delete modal related
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [uuidDelete, setUuidDelete] = useState('');
// Details modal related
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [detailsUuid, setDetailsUuid] = useState('');
const [modalDetails, setModalDetails] = useState({});
// Main collapsible
const [collapse, setCollapse] = useState(false);
// Two other open collapsible lists
const [details, setDetails] = useState([]);
const [responses, setResponses] = useState([]);
// General states
const [commands, setCommands] = useState([]);
const [loading, setLoading] = useState(false);
@@ -64,10 +65,6 @@ const DeviceCommands = () => {
setShowConfirmModal(!showConfirmModal);
};
const toggleDetailsModal = () => {
setShowDetailsModal(!showDetailsModal);
};
const showMoreCommands = () => {
setCommandLimit(commandLimit + 50);
};
@@ -174,7 +171,7 @@ const DeviceCommands = () => {
});
};
const toggleDetails = (item) => {
const toggleDetails = (item, index) => {
if (item.command === 'wifiscan') {
setChosenWifiScan(item.results.status.scan);
setChosenWifiScanDate(item.completed);
@@ -182,22 +179,52 @@ const DeviceCommands = () => {
} else if (item.command === 'trace' && item.waitingForFile === 0) {
downloadTrace(item.UUID);
} else {
setModalDetails(item.results ?? item);
setDetailsUuid(item.UUID);
toggleDetailsModal();
const position = details.indexOf(index);
let newDetails = details.slice();
if (position !== -1) {
newDetails.splice(position, 1);
} else {
newDetails = [...details, index];
}
setDetails(newDetails);
}
};
const toggleResponse = (item) => {
setModalDetails(item);
setDetailsUuid(item.UUID);
toggleDetailsModal();
const toggleResponse = (item, index) => {
const position = responses.indexOf(index);
let newResponses = responses.slice();
if (position !== -1) {
newResponses.splice(position, 1);
} else {
newResponses = [...newResponses, index];
}
setResponses(newResponses);
};
const refreshCommands = () => {
getCommands();
};
const getDetails = (command, index) => {
if (!details.includes(index)) {
return <pre className="ignore" />;
}
if (command.results) {
const result = command.results;
if (result) return <pre className="ignore">{JSON.stringify(result, null, 4)}</pre>;
}
return <pre className="ignore">{JSON.stringify(command, null, 4)}</pre>;
};
const getResponse = (commandDetails, index) => {
if (!responses.includes(index)) {
return <pre className="ignore" />;
}
return <pre className="ignore">{JSON.stringify(commandDetails, null, 4)}</pre>;
};
const columns = [
{ key: 'UUID', label: t('common.id'), _style: { width: '28%' } },
{ key: 'command', label: t('common.command'), _style: { width: '10%' } },
@@ -264,15 +291,20 @@ const DeviceCommands = () => {
<CCollapse show={collapse}>
<CRow>
<CCol />
<CCol className="text-right">
<div>
<CCol>
<div className={styles.alignRight}>
<CButton onClick={refreshCommands} size="sm">
<CIcon name="cil-sync" content={cilSync} className="text-white" size="2xl" />
<CIcon
name="cil-sync"
content={cilSync}
className={styles.whiteIcon}
size="2xl"
/>
</CButton>
</div>
</CCol>
</CRow>
<CRow className="mb-2">
<CRow className={styles.datepickerRow}>
<CCol>
From:
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
@@ -288,7 +320,7 @@ const DeviceCommands = () => {
loading={loading}
items={commands ?? []}
fields={columns}
className="text-white"
className={styles.whiteIcon}
sorterValue={{ column: 'created', desc: 'true' }}
scopedSlots={{
completed: (item) => (
@@ -323,11 +355,15 @@ const DeviceCommands = () => {
>
<CButton
color="primary"
variant="outline"
variant={details.includes(index) ? '' : 'outline'}
disabled={
item.completed === 0 ||
(item.command === 'trace' && item.waitingForFile !== 0)
}
shape="square"
size="sm"
onClick={() => {
toggleDetails(item);
toggleDetails(item, index);
}}
>
{item.command === 'trace' ? (
@@ -342,11 +378,11 @@ const DeviceCommands = () => {
<CPopover content={t('common.details')}>
<CButton
color="primary"
variant="outline"
variant={responses.includes(index) ? '' : 'outline'}
shape="square"
size="sm"
onClick={() => {
toggleResponse(item);
toggleResponse(item, index);
}}
>
<CIcon name="cilList" size="lg" />
@@ -371,6 +407,16 @@ const DeviceCommands = () => {
</CRow>
</td>
),
details: (item, index) => (
<DeviceCommandsCollapse
details={details}
responses={responses}
index={index}
getDetails={getDetails}
getResponse={getResponse}
item={item}
/>
),
}}
/>
<CRow className={styles.loadMoreSpacing}>
@@ -390,7 +436,7 @@ const DeviceCommands = () => {
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
className={styles.whiteIcon}
size="lg"
/>
</CButton>
@@ -404,13 +450,6 @@ const DeviceCommands = () => {
date={chosenWifiScanDate}
/>
<ConfirmModal show={showConfirmModal} toggle={toggleConfirmModal} action={deleteCommand} />
<DetailsModal
t={t}
show={showDetailsModal}
toggle={toggleDetailsModal}
details={modalDetails}
commandUuid={detailsUuid}
/>
</CWidgetDropdown>
);
};

View File

@@ -2,10 +2,22 @@
padding: 20px;
}
.datepickerRow {
margin-bottom: 10px;
}
.scrollableBox {
height: 200px;
}
.whiteIcon {
color: white;
}
.alignRight {
float: right;
}
.customIconHeight {
height: 19px;
}

View File

@@ -17,11 +17,13 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import { checkIfJson } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import styles from './index.module.scss';
const ConfigureModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
@@ -124,7 +126,7 @@ const ConfigureModal = ({ show, toggleModal }) => {
};
return (
<CModal show={show} onClose={toggleModal} size="lg">
<CModal show={show} onClose={toggleModal}>
<CModalHeader closeButton>
<CModalTitle>{t('configure.title')}</CModalTitle>
</CModalHeader>
@@ -134,7 +136,7 @@ const ConfigureModal = ({ show, toggleModal }) => {
<div>
<CModalBody>
<CRow>
<CCol md="10" className="mt-1">
<CCol md="10" className={styles.spacedColumn}>
<h6>{t('configure.enter_new')}</h6>
</CCol>
<CCol>
@@ -149,7 +151,7 @@ const ConfigureModal = ({ show, toggleModal }) => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-4">
<CRow className={styles.spacedRow}>
<CCol>
<CForm>
<CTextarea
@@ -167,7 +169,7 @@ const ConfigureModal = ({ show, toggleModal }) => {
</CForm>
</CCol>
</CRow>
<CRow className="mt-4">
<CRow className={styles.spacedRow}>
<CCol>{t('configure.choose_file')}</CCol>
<CCol>
<CInputFile

View File

@@ -0,0 +1,7 @@
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 3px;
}

View File

@@ -11,6 +11,7 @@ import {
CBadge,
} from '@coreui/react';
import PropTypes from 'prop-types';
import styles from './index.module.scss';
const ConfirmModal = ({ show, toggle, action }) => {
const { t } = useTranslation();
@@ -62,7 +63,7 @@ const ConfirmModal = ({ show, toggle, action }) => {
}, [show]);
return (
<CModal className="text-dark" show={show} onClose={toggle}>
<CModal className={styles.modal} show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle>{t('delete_command.title')}</CModalTitle>
</CModalHeader>

View File

@@ -0,0 +1,3 @@
.modal {
color: #3c4b64;
}

View File

@@ -0,0 +1,37 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import CIcon from '@coreui/icons-react';
import { cilClone } from '@coreui/icons';
import PropTypes from 'prop-types';
import { CButton, CPopover } from '@coreui/react';
const CopyToClipboardButton = ({ content, size }) => {
const { t } = useTranslation();
const [result, setResult] = useState('');
const copyToClipboard = () => {
navigator.clipboard.writeText(content);
setResult(t('common.copied'));
};
return (
<CPopover content={t('common.copy_to_clipboard')}>
<CButton onClick={copyToClipboard} size={size}>
<CIcon content={cilClone} />
{' '}
{result || ''}
</CButton>
</CPopover>
);
};
CopyToClipboardButton.propTypes = {
content: PropTypes.string.isRequired,
size: PropTypes.string,
};
CopyToClipboardButton.defaultProps = {
size: 'sm',
};
export default CopyToClipboardButton;

View File

@@ -1,168 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalBody } from '@coreui/react';
import { CreateUserForm, useFormFields, useAuth, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { testRegex, validateEmail } from 'utils/helper';
const initialState = {
name: {
value: '',
error: false,
optional: true,
},
email: {
value: '',
error: false,
},
currentPassword: {
value: '',
error: false,
},
changePassword: {
value: 'on',
error: false,
},
userRole: {
value: 'admin',
error: false,
},
notes: {
value: '',
error: false,
optional: true,
},
description: {
value: '',
error: false,
optional: true,
},
};
const CreateUserModal = ({ show, toggle, getUsers }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [policies, setPolicies] = useState({
passwordPolicy: '',
passwordPattern: '',
accessPolicy: '',
});
const [formFields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialState);
const toggleChange = () => {
updateField('changePassword', { value: !formFields.changePassword.value });
};
const createUser = () => {
setLoading(true);
const parameters = {
id: 0,
};
let validationSuccess = true;
for (const [key, value] of Object.entries(formFields)) {
if (!value.optional && value.value === '') {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'currentPassword' && !testRegex(value.value, policies.passwordPattern)) {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'email' && !validateEmail(value.value)) {
validationSuccess = false;
updateField(key, { value: value.value, error: true });
} else if (key === 'notes') {
parameters[key] = [{ note: value.value }];
} else if (key === 'changePassword') {
parameters[key] = value.value === 'on';
} else {
parameters[key] = value.value;
}
}
if (validationSuccess) {
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.post(`${endpoints.ucentralsec}/api/v1/user/0`, parameters, {
headers,
})
.then(() => {
getUsers();
setFormFields(initialState);
addToast({
title: t('common.success'),
body: t('user.create_success'),
color: 'success',
autohide: true,
});
toggle();
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('user.create_failure'),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
};
const getPasswordPolicy = () => {
axiosInstance
.post(`${endpoints.ucentralsec}/api/v1/oauth2?requirements=true`, {})
.then((response) => {
const newPolicies = response.data;
newPolicies.accessPolicy = `${endpoints.ucentralsec}${newPolicies.accessPolicy}`;
newPolicies.passwordPolicy = `${endpoints.ucentralsec}${newPolicies.passwordPolicy}`;
setPolicies(response.data);
})
.catch(() => {});
};
useEffect(() => {
if (policies.passwordPattern.length === 0) getPasswordPolicy();
}, []);
useEffect(() => {
setFormFields(initialState);
}, [show]);
return (
<CModal show={show} onClose={toggle} size="xl">
<CModalHeader>{t('user.create')}</CModalHeader>
<CModalBody>
<CreateUserForm
t={t}
fields={formFields}
updateField={updateFieldWithId}
createUser={createUser}
loading={loading}
policies={policies}
toggleChange={toggleChange}
/>
</CModalBody>
</CModal>
);
};
CreateUserModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
getUsers: PropTypes.func.isRequired,
};
export default React.memo(CreateUserModal);

View File

@@ -3,10 +3,13 @@ import { useTranslation } from 'react-i18next';
import { CModal, CModalHeader, CModalTitle, CModalBody, CCol, CRow } from '@coreui/react';
import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { ConfirmFooter, useAuth, useDevice } from 'ucentral-libs';
import ConfirmFooter from 'components/ConfirmFooter';
import { dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { useDevice } from 'contexts/DeviceProvider';
import { useAuth } from 'contexts/AuthProvider';
import eventBus from 'utils/eventBus';
import styles from './index.module.scss';
const DeleteLogModal = ({ show, toggle, object }) => {
const { t } = useTranslation();
@@ -53,7 +56,7 @@ const DeleteLogModal = ({ show, toggle, object }) => {
}, [show]);
return (
<CModal className="text-dark" show={show} onClose={toggle}>
<CModal className={styles.modal} show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle>
{object === 'healthchecks'
@@ -63,8 +66,8 @@ const DeleteLogModal = ({ show, toggle, object }) => {
</CModalHeader>
<CModalBody>
<h6>{t('delete_logs.explanation', { object })}</h6>
<CRow className="pt-3">
<CCol md="4" className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="4" className={styles.spacedDate}>
<p>{t('common.date')}:</p>
</CCol>
<CCol xs="12" md="8">
@@ -80,7 +83,6 @@ const DeleteLogModal = ({ show, toggle, object }) => {
</CRow>
</CModalBody>
<ConfirmFooter
t={t}
isShown={show}
isLoading={loading}
action={deleteLog}

View File

@@ -0,0 +1,11 @@
.modal {
color: #3c4b64;
}
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 7px;
}

View File

@@ -1,26 +1,24 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CButton, CCard, CCardHeader, CCardBody, CRow, CCol } from '@coreui/react';
import axiosInstance from 'utils/axiosInstance';
import { LoadingButton, useAuth, useDevice, useToast } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import LoadingButton from 'components/LoadingButton';
import RebootModal from 'components/RebootModal';
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
import FirmwareUpgradeModal from 'components/FirmwareUpgradeModal';
import ConfigureModal from 'components/ConfigureModal';
import TraceModal from 'components/TraceModal';
import WifiScanModal from 'components/WifiScanModal';
import BlinkModal from 'components/BlinkModal';
import FactoryResetModal from 'components/FactoryResetModal';
import EventQueueModal from 'components/EventQueueModal';
import styles from './index.module.scss';
const DeviceActions = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const { deviceSerialNumber } = useDevice();
const [upgradeStatus, setUpgradeStatus] = useState({
loading: false,
});
const [device, setDevice] = useState({});
const [showRebootModal, setShowRebootModal] = useState(false);
const [showBlinkModal, setShowBlinkModal] = useState(false);
const [showUpgradeModal, setShowUpgradeModal] = useState(false);
@@ -29,7 +27,6 @@ const DeviceActions = () => {
const [connectLoading, setConnectLoading] = useState(false);
const [showConfigModal, setConfigModal] = useState(false);
const [showFactoryModal, setShowFactoryModal] = useState(false);
const [showQueueModal, setShowQueueModal] = useState(false);
const toggleRebootModal = () => {
setShowRebootModal(!showRebootModal);
@@ -59,10 +56,6 @@ const DeviceActions = () => {
setShowFactoryModal(!showFactoryModal);
};
const toggleQueueModal = () => {
setShowQueueModal(!showQueueModal);
};
const getRttysInfo = () => {
setConnectLoading(true);
const options = {
@@ -88,43 +81,6 @@ const DeviceActions = () => {
});
};
const getDeviceInformation = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.ucentralgw}/api/v1/device/${deviceSerialNumber}`, options)
.then((response) => {
setDevice(response.data);
})
.catch(() => {});
};
useEffect(() => {
if (upgradeStatus.result !== undefined) {
addToast({
title: upgradeStatus.result.success ? t('common.success') : t('common.error'),
body: upgradeStatus.result.success
? t('firmware.upgrade_command_submitted')
: upgradeStatus.result.error,
color: upgradeStatus.result.success ? 'success' : 'danger',
autohide: true,
});
setUpgradeStatus({
loading: false,
});
setShowUpgradeModal(false);
}
}, [upgradeStatus]);
useEffect(() => {
getDeviceInformation();
}, [deviceSerialNumber]);
return (
<CCard>
<CCardHeader>
@@ -143,7 +99,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol>
<CButton block color="primary" onClick={toggleUpgradeModal}>
{t('actions.firmware_upgrade')}
@@ -155,7 +111,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol>
<CButton block color="primary" onClick={toggleScanModal}>
{t('actions.wifi_scan')}
@@ -167,7 +123,7 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol>
<LoadingButton
isLoading={connectLoading}
@@ -182,32 +138,14 @@ const DeviceActions = () => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CCol>
<CButton block color="primary" onClick={toggleQueueModal}>
{t('commands.event_queue')}
</CButton>
</CCol>
<CCol />
</CRow>
</CCardBody>
<RebootModal show={showRebootModal} toggleModal={toggleRebootModal} />
<BlinkModal show={showBlinkModal} toggleModal={toggleBlinkModal} />
<DeviceFirmwareModal
t={t}
endpoints={endpoints}
currentToken={currentToken}
device={device}
show={showUpgradeModal}
toggleFirmwareModal={toggleUpgradeModal}
setUpgradeStatus={setUpgradeStatus}
upgradeStatus={upgradeStatus}
/>
<FirmwareUpgradeModal show={showUpgradeModal} toggleModal={toggleUpgradeModal} />
<TraceModal show={showTraceModal} toggleModal={toggleTraceModal} />
<WifiScanModal show={showScanModal} toggleModal={toggleScanModal} />
<ConfigureModal show={showConfigModal} toggleModal={toggleConfigModal} />
<FactoryResetModal show={showFactoryModal} toggleModal={toggleFactoryResetModal} />
<EventQueueModal show={showQueueModal} toggle={toggleQueueModal} />
</CCard>
);
};

View File

@@ -0,0 +1,3 @@
.spacedRow {
margin-top: 10px;
}

View File

@@ -10,13 +10,14 @@ import {
} from '@coreui/react';
import PropTypes from 'prop-types';
import { Translation } from 'react-i18next';
import styles from './index.module.scss';
const DeviceConfigurationModal = ({ show, toggle, configuration }) => (
<Translation>
{(t) => (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle className="text-dark">{t('configuration.title')}</CModalTitle>
<CModalTitle className={styles.modalTitle}>{t('configuration.title')}</CModalTitle>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(configuration, null, 4)}</pre>

View File

@@ -16,29 +16,21 @@ import CIcon from '@coreui/icons-react';
import { cilWindowMaximize } from '@coreui/icons';
import { prettyDate } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import {
CopyToClipboardButton,
HideTextButton,
NotesTable,
useAuth,
useDevice,
} from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import CopyToClipboardButton from 'components/CopyToClipboardButton';
import DeviceNotes from 'components/DeviceNotes';
import DeviceConfigurationModal from './DeviceConfigurationModal';
import styles from './index.module.scss';
const DeviceConfiguration = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [collapse, setCollapse] = useState(false);
const [showModal, setShowModal] = useState(false);
const [device, setDevice] = useState(null);
const toggleShowPassword = () => {
setShowPassword(!showPassword);
};
const toggle = (e) => {
setCollapse(!collapse);
e.preventDefault();
@@ -67,39 +59,6 @@ const DeviceConfiguration = () => {
.catch(() => {});
};
const saveNote = (currentNote) => {
setLoading(true);
const parameters = {
serialNumber: deviceSerialNumber,
notes: [{ note: currentNote }],
};
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.put(
`${endpoints.ucentralgw}/api/v1/device/${encodeURIComponent(deviceSerialNumber)}`,
parameters,
{ headers },
)
.then(() => {
getDevice();
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
const getPassword = () => {
const password = device.devicePassword === '' ? 'openwifi' : device.devicePassword;
return showPassword ? password : '******';
};
useEffect(() => {
if (deviceSerialNumber) getDevice();
}, [deviceSerialNumber]);
@@ -113,34 +72,36 @@ const DeviceConfiguration = () => {
<CCol>
<div className="text-value-lg">{t('configuration.title')}</div>
</CCol>
<CCol className="text-right">
<CCol>
<div className={styles.alignRight}>
<CPopover content={t('configuration.view_json')}>
<CButton color="secondary" onClick={toggleModal} size="sm">
<CIcon content={cilWindowMaximize} />
</CButton>
</CPopover>
</div>
</CCol>
</CRow>
</CCardHeader>
<CCardBody>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.uuid')} : </CLabel>
<CLabel>{t('common.uuid')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{device.UUID}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('common.serial_number')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{device.serialNumber}
<CopyToClipboardButton t={t} size="sm" content={device.serialNumber} />
<CopyToClipboardButton size="sm" content={device.serialNumber} />
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.type')} : </CLabel>
</CCol>
@@ -148,15 +109,7 @@ const DeviceConfiguration = () => {
{device.deviceType}
</CCol>
</CRow>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('firmware.revision')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{device.firmware}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.last_configuration_change')} : </CLabel>
</CCol>
@@ -164,7 +117,7 @@ const DeviceConfiguration = () => {
{prettyDate(device.lastConfigurationChange)}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('common.mac')} :</CLabel>
</CCol>
@@ -172,47 +125,7 @@ const DeviceConfiguration = () => {
{device.macAddress}
</CCol>
</CRow>
<CRow className="mt-2 mb-4">
<CCol md="3">
<CLabel className="align-middle">{t('configuration.device_password')} : </CLabel>
</CCol>
<CCol xs="12" md="2">
{getPassword()}
</CCol>
<CCol md="7">
<HideTextButton t={t} toggle={toggleShowPassword} show={showPassword} />
<CopyToClipboardButton
t={t}
size="sm"
content={device?.devicePassword === '' ? 'openwifi' : device.devicePassword}
/>
</CCol>
</CRow>
<NotesTable
t={t}
notes={device.notes}
loading={loading}
addNote={saveNote}
descriptionColumn={false}
/>
<CCollapse show={collapse}>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('configuration.last_configuration_download')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{prettyDate(device.lastConfigurationDownload)}
</CCol>
</CRow>
<CRow className="mt-2">
<CCol md="3">
<CLabel>{t('common.manufacturer')} :</CLabel>
</CCol>
<CCol xs="12" md="9">
{device.manufacturer}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.created')} : </CLabel>
</CCol>
@@ -220,7 +133,41 @@ const DeviceConfiguration = () => {
{prettyDate(device.createdTimestamp)}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3" className={styles.topPadding}>
<CLabel>{t('configuration.device_password')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{device.devicePassword === '' ? 'openwifi' : device.devicePassword}
<CopyToClipboardButton
size="sm"
content={device?.devicePassword === '' ? 'openwifi' : device.devicePassword}
/>
</CCol>
</CRow>
<DeviceNotes
notes={device.notes}
refreshNotes={getDevice}
serialNumber={deviceSerialNumber}
/>
<CCollapse show={collapse}>
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.last_configuration_download')} : </CLabel>
</CCol>
<CCol xs="12" md="9">
{prettyDate(device.lastConfigurationDownload)}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('common.manufacturer')} :</CLabel>
</CCol>
<CCol xs="12" md="9">
{device.manufacturer}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.owner')} :</CLabel>
</CCol>
@@ -228,7 +175,7 @@ const DeviceConfiguration = () => {
{device.owner}
</CCol>
</CRow>
<CRow className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.location')} :</CLabel>
</CCol>
@@ -240,7 +187,7 @@ const DeviceConfiguration = () => {
<CCardFooter>
<CButton show={collapse ? 'true' : 'false'} onClick={toggle} block>
<CIcon
className="text-dark"
className={styles.blackIcon}
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
size="lg"
/>

View File

@@ -0,0 +1,20 @@
.alignRight {
float: right;
}
.blackIcon {
color: black;
}
.modalTitle {
color: black;
}
.topPadding {
padding-top: 5px;
}
.spacedRow {
margin-top: 5px;
margin-bottom: 5px;
}

View File

@@ -1,112 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DeviceFirmwareModal as Modal, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
const DeviceFirmwareModal = ({
device,
show,
toggleFirmwareModal,
setUpgradeStatus,
upgradeStatus,
}) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [firmwareVersions, setFirmwareVersions] = useState([]);
const getFirmwareList = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.ucentralfms}/api/v1/firmwares?deviceType=${device.compatible}`, {
headers,
})
.then((response) => {
const sortedFirmware = response.data.firmwares.sort((a, b) => {
const firstDate = a.imageDate;
const secondDate = b.imageDate;
if (firstDate < secondDate) return 1;
return firstDate > secondDate ? -1 : 0;
});
setFirmwareVersions(sortedFirmware);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
const upgradeToVersion = (uri) => {
setUpgradeStatus({
loading: true,
});
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
const parameters = {
serialNumber: device.serialNumber,
when: 0,
uri,
};
axiosInstance
.post(`${endpoints.ucentralgw}/api/v1/device/${device.serialNumber}/upgrade`, parameters, {
headers,
})
.then((response) => {
setUpgradeStatus({
loading: false,
result: {
success: response.data.errorCode === 0,
error: response.data.errorCode === 0 ? '' : t('firmware.error_fetching_latest'),
},
});
})
.catch(() => {
setUpgradeStatus({
loading: false,
result: {
success: false,
error: t('common.general_error'),
},
});
});
};
useEffect(() => {
if (show && device.compatible) getFirmwareList();
}, [device, show]);
return (
<Modal
t={t}
device={device}
show={show}
toggle={toggleFirmwareModal}
firmwareVersions={firmwareVersions}
upgradeToVersion={upgradeToVersion}
loading={loading}
upgradeStatus={upgradeStatus}
/>
);
};
DeviceFirmwareModal.propTypes = {
device: PropTypes.instanceOf(Object).isRequired,
show: PropTypes.bool.isRequired,
toggleFirmwareModal: PropTypes.func.isRequired,
setUpgradeStatus: PropTypes.func.isRequired,
upgradeStatus: PropTypes.instanceOf(Object).isRequired,
};
export default React.memo(DeviceFirmwareModal);

View File

@@ -16,10 +16,13 @@ import CIcon from '@coreui/icons-react';
import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import DeleteLogModal from 'components/DeleteLogModal';
import styles from './index.module.scss';
const DeviceHealth = () => {
const { t } = useTranslation();
@@ -195,10 +198,10 @@ const DeviceHealth = () => {
color={barColor}
inverse="true"
footerSlot={
<div className="p-4">
<CProgress className="mb-3" color="white" value={sanityLevel ?? 0} />
<div className={styles.footer}>
<CProgress className={styles.progressBar} color="white" value={sanityLevel ?? 0} />
<CCollapse show={collapse}>
<div className="text-right">
<div className={styles.alignRight}>
<CPopover content={t('common.delete')}>
<CButton
color="light"
@@ -212,7 +215,7 @@ const DeviceHealth = () => {
</CButton>
</CPopover>
</div>
<CRow className="mb-3">
<CRow className={styles.spacedRow}>
<CCol>
{t('common.from')}
:
@@ -224,23 +227,23 @@ const DeviceHealth = () => {
<DatePicker includeTime onChange={(date) => modifyEnd(date)} />
</CCol>
</CRow>
<CCard className="p-0">
<div className="overflow-auto" style={{ height: '250px' }}>
<CCard>
<div className={[styles.scrollable, 'overflow-auto'].join(' ')}>
<CDataTable
border
items={healthChecks ?? []}
fields={columns}
className="text-white"
className={styles.dataTable}
loading={loading}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
UUID: (item) => <td className="align-middle">{item.UUID}</td>,
recorded: (item) => (
<td className="align-middle">{prettyDate(item.recorded)}</td>
),
sanity: (item) => <td className="align-middle">{`${item.sanity}%`}</td>,
show_details: (item, index) => (
<td className="align-middle">
recorded: (item) => <td>{prettyDate(item.recorded)}</td>,
sanity: (item) => <td>{`${item.sanity}%`}</td>,
show_details: (item, index) => {
if (item.sanity === 100) {
return <></>;
}
return (
<td className="py-2">
<CButton
color="primary"
variant={details.includes(index) ? '' : 'outline'}
@@ -253,7 +256,8 @@ const DeviceHealth = () => {
<CIcon name="cilList" size="lg" />
</CButton>
</td>
),
);
},
details: (item, index) => (
<CCollapse show={details.includes(index)}>
<CCardBody>
@@ -264,8 +268,8 @@ const DeviceHealth = () => {
),
}}
/>
<CRow className={styles.loadMoreRow}>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
@@ -273,15 +277,15 @@ const DeviceHealth = () => {
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</CRow>
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
className={styles.icon}
size="lg"
/>
</CButton>

View File

@@ -0,0 +1,31 @@
.icon {
color: white;
}
.dataTable {
color: white;
}
.footer {
padding: 20px;
}
.progressBar {
margin-bottom: 20px;
}
.spacedRow {
margin-bottom: 10px;
}
.loadMoreRow {
margin-bottom: 1%;
}
.scrollable {
height: 250px;
}
.alignRight {
float: right;
}

View File

@@ -1,112 +1,44 @@
import React, { useEffect, useState } from 'react';
import {
CBadge,
CCardBody,
CDataTable,
CButton,
CLink,
CCard,
CCardHeader,
CRow,
CCol,
CPopover,
CSelect,
} from '@coreui/react';
import ReactPaginate from 'react-paginate';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { cilSync, cilInfo, cilBadge, cilBan } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import { useAuth } from 'contexts/AuthProvider';
import axiosInstance from 'utils/axiosInstance';
import { cleanBytesString } from 'utils/helper';
import meshIcon from 'assets/icons/Mesh.png';
import apIcon from 'assets/icons/AP.png';
import internetSwitch from 'assets/icons/Switch.png';
import iotIcon from 'assets/icons/IotIcon.png';
import { getItem, setItem } from 'utils/localStorageHelper';
import DeviceSearchBar from 'components/DeviceSearchBar';
import DeviceFirmwareModal from 'components/DeviceFirmwareModal';
import FirmwareHistoryModal from 'components/FirmwareHistoryModal';
import { DeviceListTable, useAuth, useToast } from 'ucentral-libs';
import meshIcon from '../../assets/icons/Mesh.png';
import apIcon from '../../assets/icons/AP.png';
import internetSwitch from '../../assets/icons/Switch.png';
import iotIcon from '../../assets/icons/IotIcon.png';
import styles from './index.module.scss';
const DeviceList = () => {
const { t } = useTranslation();
const { addToast } = useToast();
const history = useHistory();
const { search } = useLocation();
const page = new URLSearchParams(search).get('page');
const { currentToken, endpoints } = useAuth();
const [upgradeStatus, setUpgradeStatus] = useState({
loading: false,
});
const [deleteStatus, setDeleteStatus] = useState({
loading: false,
});
const [deviceCount, setDeviceCount] = useState(0);
const [loadedSerials, setLoadedSerials] = useState(false);
const [serialNumbers, setSerialNumbers] = useState([]);
const [page, setPage] = useState(0);
const [pageCount, setPageCount] = useState(0);
const [devicesPerPage, setDevicesPerPage] = useState(getItem('devicesPerPage') || '10');
const [devicesPerPage, setDevicesPerPage] = useState(getItem('devicesPerPage') || 10);
const [devices, setDevices] = useState([]);
const [loading, setLoading] = useState(true);
const [showHistoryModal, setHistoryModal] = useState(false);
const [showFirmwareModal, setShowFirmwareModal] = useState(false);
const [firmwareDevice, setFirmwareDevice] = useState({
deviceType: '',
serialNumber: '',
});
const deviceIcons = {
meshIcon,
apIcon,
internetSwitch,
iotIcon,
};
const toggleFirmwareModal = (device) => {
setShowFirmwareModal(!showFirmwareModal);
if (device !== undefined) setFirmwareDevice(device);
};
const toggleHistoryModal = (device) => {
setHistoryModal(!showHistoryModal);
if (device !== undefined) setFirmwareDevice(device);
};
const getDeviceInformation = (selectedPage = page, devicePerPage = devicesPerPage) => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
let fullDevices;
axiosInstance
.get(
`${
endpoints.ucentralgw
}/api/v1/devices?deviceWithStatus=true&limit=${devicePerPage}&offset=${
devicePerPage * selectedPage + 1
}`,
options,
)
.then((response) => {
fullDevices = response.data.devicesWithStatus;
const serialsToGet = fullDevices.map((device) => device.serialNumber);
return axiosInstance.get(
`${endpoints.ucentralfms}/api/v1/firmwareAge?select=${serialsToGet}`,
options,
);
})
.then((response) => {
fullDevices = fullDevices.map((device, index) => {
const foundAgeDate = response.data.ages[index].age !== undefined;
if (foundAgeDate) {
return {
...device,
firmwareInfo: {
age: response.data.ages[index].age,
latest: response.data.ages[index].latest,
},
};
}
return device;
});
setDevices(fullDevices);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
const getCount = () => {
const getSerialNumbers = () => {
setLoading(true);
const headers = {
@@ -115,22 +47,40 @@ const DeviceList = () => {
};
axiosInstance
.get(`${endpoints.ucentralgw}/api/v1/devices?countOnly=true`, {
.get(`${endpoints.ucentralgw}/api/v1/devices?serialOnly=true`, {
headers,
})
.then((response) => {
const devicesCount = response.data.count;
const pagesCount = Math.ceil(devicesCount / devicesPerPage);
setPageCount(pagesCount);
setDeviceCount(devicesCount);
setSerialNumbers(response.data.serialNumbers);
setLoadedSerials(true);
})
.catch(() => {
setLoading(false);
});
};
let selectedPage = page;
const getDeviceInformation = () => {
setLoading(true);
if (page >= pagesCount) {
history.push(`/devices?page=${pagesCount - 1}`);
selectedPage = pagesCount - 1;
}
getDeviceInformation(selectedPage);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
const startIndex = page * devicesPerPage;
const endIndex = parseInt(startIndex, 10) + parseInt(devicesPerPage, 10);
const serialsToGet = serialNumbers
.slice(startIndex, endIndex)
.map((x) => encodeURIComponent(x))
.join(',');
axiosInstance
.get(`${endpoints.ucentralgw}/api/v1/devices?deviceWithStatus=true&select=${serialsToGet}`, {
headers,
})
.then((response) => {
setDevices(response.data.devicesWithStatus);
setLoading(false);
})
.catch(() => {
setLoading(false);
@@ -140,11 +90,9 @@ const DeviceList = () => {
const refreshDevice = (serialNumber) => {
setLoading(true);
const options = {
headers: {
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
@@ -152,7 +100,9 @@ const DeviceList = () => {
`${endpoints.ucentralgw}/api/v1/devices?deviceWithStatus=true&select=${encodeURIComponent(
serialNumber,
)}`,
options,
{
headers,
},
)
.then((response) => {
const device = response.data.devicesWithStatus[0];
@@ -170,182 +120,29 @@ const DeviceList = () => {
const updateDevicesPerPage = (value) => {
setItem('devicesPerPage', value);
setDevicesPerPage(value);
const newPageCount = Math.ceil(deviceCount / value);
setPageCount(newPageCount);
let selectedPage = page;
if (page >= newPageCount) {
history.push(`/devices?page=${newPageCount - 1}`);
selectedPage = newPageCount - 1;
}
getDeviceInformation(selectedPage, value);
};
const updatePageCount = ({ selected: selectedPage }) => {
history.push(`/devices?page=${selectedPage}`);
getDeviceInformation(selectedPage);
};
const upgradeToLatest = (device) => {
setUpgradeStatus({
loading: true,
});
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.ucentralfms}/api/v1/firmwares?deviceType=${device.compatible}&latestOnly=true`,
options,
)
.then((response) => {
if (response.data.uri) {
const parameters = {
serialNumber: device.serialNumber,
when: 0,
uri: response.data.uri,
};
return axiosInstance.post(
`${endpoints.ucentralgw}/api/v1/device/${device.serialNumber}/upgrade`,
parameters,
options,
);
}
setUpgradeStatus({
loading: false,
result: {
success: false,
error: t('firmware.error_fetching_latest'),
},
});
return null;
})
.then((response) => {
if (response) {
setUpgradeStatus({
loading: false,
result: {
success: response.data.errorCode === 0,
error: response.data.errorCode === 0 ? '' : t('firmware.error_fetching_latest'),
},
});
}
})
.catch(() => {
setUpgradeStatus({
loading: false,
result: {
success: false,
error: t('common.general_error'),
},
});
});
};
const connectRtty = (serialNumber) => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.ucentralgw}/api/v1/device/${encodeURIComponent(serialNumber)}/rtty`,
options,
)
.then((response) => {
const url = `https://${response.data.server}:${response.data.viewport}/connect/${response.data.connectionId}`;
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null;
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('common.unable_to_connect'),
color: 'danger',
autohide: true,
});
});
};
const deleteDevice = (serialNumber) => {
setDeleteStatus({
loading: true,
});
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.delete(`${endpoints.ucentralgw}/api/v1/device/${encodeURIComponent(serialNumber)}`, options)
.then(() => {
addToast({
title: t('common.success'),
body: t('common.device_deleted'),
color: 'success',
autohide: true,
});
getCount();
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('common.unable_to_delete'),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setDeleteStatus({
loading: false,
});
});
setPage(selectedPage);
};
useEffect(() => {
if (page === undefined || page === null || Number.isNaN(page)) {
history.push(`/devices?page=0`);
}
getCount();
getSerialNumbers();
}, []);
useEffect(() => {
if (upgradeStatus.result !== undefined) {
addToast({
title: upgradeStatus.result.success ? t('common.success') : t('common.error'),
body: upgradeStatus.result.success
? t('firmware.upgrade_command_submitted')
: upgradeStatus.result.error,
color: upgradeStatus.result.success ? 'success' : 'danger',
autohide: true,
});
setUpgradeStatus({
loading: false,
});
setShowFirmwareModal(false);
if (loadedSerials) getDeviceInformation();
}, [serialNumbers, page, devicesPerPage, loadedSerials]);
useEffect(() => {
if (loadedSerials) {
const count = Math.ceil(serialNumbers.length / devicesPerPage);
setPageCount(count);
}
}, [upgradeStatus]);
}, [devicesPerPage, loadedSerials]);
return (
<div>
<DeviceListTable
currentPage={page}
t={t}
searchBar={<DeviceSearchBar />}
<DeviceListDisplay
devices={devices}
loading={loading}
updateDevicesPerPage={updateDevicesPerPage}
@@ -354,31 +151,288 @@ const DeviceList = () => {
updatePage={updatePageCount}
pageRangeDisplayed={5}
refreshDevice={refreshDevice}
toggleFirmwareModal={toggleFirmwareModal}
toggleHistoryModal={toggleHistoryModal}
upgradeToLatest={upgradeToLatest}
upgradeStatus={upgradeStatus}
deviceIcons={deviceIcons}
connectRtty={connectRtty}
deleteDevice={deleteDevice}
deleteStatus={deleteStatus}
t={t}
/>
<DeviceFirmwareModal
endpoints={endpoints}
currentToken={currentToken}
device={firmwareDevice}
show={showFirmwareModal}
toggleFirmwareModal={toggleFirmwareModal}
setUpgradeStatus={setUpgradeStatus}
upgradeStatus={upgradeStatus}
/>
<FirmwareHistoryModal
serialNumber={firmwareDevice.serialNumber}
show={showHistoryModal}
toggle={toggleHistoryModal}
/>
</div>
);
};
const DeviceListDisplay = ({
devices,
devicesPerPage,
loading,
updateDevicesPerPage,
pageCount,
updatePage,
refreshDevice,
t,
}) => {
const columns = [
{ key: 'deviceType', label: '', filter: false, sorter: false, _style: { width: '5%' } },
{ key: 'verifiedCertificate', label: t('common.certificate'), _style: { width: '1%' } },
{ key: 'serialNumber', label: t('common.serial_number'), _style: { width: '5%' } },
{ key: 'UUID', label: t('common.config_id'), _style: { width: '5%' } },
{ key: 'firmware', label: t('common.firmware'), filter: false },
{ key: 'compatible', label: t('common.compatible'), filter: false, _style: { width: '20%' } },
{ key: 'txBytes', label: 'Tx', filter: false, _style: { width: '12%' } },
{ key: 'rxBytes', label: 'Rx', filter: false, _style: { width: '12%' } },
{ key: 'ipAddress', label: t('common.ip_address'), _style: { width: '16%' } },
{
key: 'show_details',
label: '',
_style: { width: '3%' },
sorter: false,
filter: false,
},
{
key: 'refresh',
label: '',
_style: { width: '2%' },
sorter: false,
filter: false,
},
];
const getDeviceIcon = (deviceType) => {
if (deviceType === 'AP_Default' || deviceType === 'AP') {
return <img src={apIcon} className={styles.icon} alt="AP" />;
}
if (deviceType === 'MESH') {
return <img src={meshIcon} className={styles.icon} alt="MESH" />;
}
if (deviceType === 'SWITCH') {
return <img src={internetSwitch} className={styles.icon} alt="SWITCH" />;
}
if (deviceType === 'IOT') {
return <img src={iotIcon} className={styles.icon} alt="SWITCH" />;
}
return null;
};
const getCertBadge = (cert) => {
if (cert === 'NO_CERTIFICATE') {
return (
<div className={styles.certificateWrapper}>
<CIcon className={styles.badge} name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
<CIcon
className={styles.badCertificate}
name="cil-ban"
content={cilBan}
size="3xl"
alt="AP"
/>
</div>
);
}
let color = 'transparent';
switch (cert) {
case 'VALID_CERTIFICATE':
color = 'danger';
break;
case 'MISMATCH_SERIAL':
return (
<CBadge color={color} className={styles.mismatchBackground}>
<CIcon name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
</CBadge>
);
case 'VERIFIED':
color = 'success';
break;
default:
return (
<div className={styles.certificateWrapper}>
<CIcon
className={styles.badge}
name="cil-badge"
content={cilBadge}
size="2xl"
alt="AP"
/>
<CIcon
className={styles.badCertificate}
name="cil-ban"
content={cilBan}
size="3xl"
alt="AP"
/>
</div>
);
}
return (
<CBadge color={color}>
<CIcon name="cil-badge" content={cilBadge} size="2xl" alt="AP" />
</CBadge>
);
};
const getStatusBadge = (status) => {
if (status) {
return 'success';
}
return 'danger';
};
return (
<>
<CCard>
<CCardHeader>
<CRow>
<CCol />
<CCol xs={1}>
<CSelect
custom
defaultValue={devicesPerPage}
onChange={(e) => updateDevicesPerPage(e.target.value)}
disabled={loading}
>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</CSelect>
</CCol>
</CRow>
</CCardHeader>
<CCardBody>
<CDataTable
items={devices ?? []}
fields={columns}
hover
border
loading={loading}
scopedSlots={{
serialNumber: (item) => (
<td className={styles.column}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
{item.serialNumber}
</CLink>
</td>
),
deviceType: (item) => (
<td className={styles.column}>
<CPopover
content={item.connected ? t('common.connected') : t('common.not_connected')}
placement="top"
>
<CBadge color={getStatusBadge(item.connected)}>
{getDeviceIcon(item.deviceType) ?? item.deviceType}
</CBadge>
</CPopover>
</td>
),
verifiedCertificate: (item) => (
<td className={styles.column}>
<CPopover
content={item.verifiedCertificate ?? t('common.unknown')}
placement="top"
>
{getCertBadge(item.verifiedCertificate)}
</CPopover>
</td>
),
firmware: (item) => (
<td>
<CPopover
content={item.firmware ? item.firmware : t('common.na')}
placement="top"
>
<p style={{ width: '225px' }} className="text-truncate">
{item.firmware}
</p>
</CPopover>
</td>
),
compatible: (item) => (
<td>
<CPopover
content={item.compatible ? item.compatible : t('common.na')}
placement="top"
>
<p style={{ width: '150px' }} className="text-truncate">
{item.compatible}
</p>
</CPopover>
</td>
),
txBytes: (item) => <td>{cleanBytesString(item.txBytes)}</td>,
rxBytes: (item) => <td>{cleanBytesString(item.rxBytes)}</td>,
ipAddress: (item) => (
<td>
<CPopover
content={item.ipAddress ? item.ipAddress : t('common.na')}
placement="top"
>
<p style={{ width: '150px' }} className="text-truncate">
{item.ipAddress}
</p>
</CPopover>
</td>
),
refresh: (item) => (
<td className="py-2">
<CPopover content={t('common.refresh_device')}>
<CButton
onClick={() => refreshDevice(item.serialNumber)}
color="primary"
variant="outline"
size="sm"
>
<CIcon name="cil-sync" content={cilSync} size="sm" />
</CButton>
</CPopover>
</td>
),
show_details: (item) => (
<td className="py-2">
<CPopover content={t('configuration.details')}>
<CLink
className="c-subheader-nav-link"
aria-current="page"
to={() => `/devices/${item.serialNumber}`}
>
<CButton color="primary" variant="outline" shape="square" size="sm">
<CIcon name="cil-info" content={cilInfo} size="sm" />
</CButton>
</CLink>
</CPopover>
</td>
),
}}
/>
<ReactPaginate
previousLabel="← Previous"
nextLabel="Next →"
pageCount={pageCount}
onPageChange={updatePage}
breakClassName="page-item"
breakLinkClassName="page-link"
containerClassName="pagination"
pageClassName="page-item"
pageLinkClassName="page-link"
previousClassName="page-item"
previousLinkClassName="page-link"
nextClassName="page-item"
nextLinkClassName="page-link"
activeClassName="active"
/>
</CCardBody>
</CCard>
</>
);
};
DeviceListDisplay.propTypes = {
devices: PropTypes.instanceOf(Array).isRequired,
updateDevicesPerPage: PropTypes.func.isRequired,
pageCount: PropTypes.number.isRequired,
updatePage: PropTypes.func.isRequired,
devicesPerPage: PropTypes.string.isRequired,
refreshDevice: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
};
export default DeviceList;

View File

@@ -0,0 +1,29 @@
.icon {
height: 32px;
width: 32px;
}
.column {
text-align: center;
}
.certificateWrapper {
position: relative;
}
.badge {
position: absolute;
left: 31%;
margin-top: 8%;
}
.badCertificate {
position: absolute;
z-index: 99;
left: 22%;
color: #e55353;
}
.mismatchBackground {
background-color: #ffff5c;
}

View File

@@ -16,9 +16,12 @@ import { useTranslation } from 'react-i18next';
import DatePicker from 'react-widgets/DatePicker';
import { prettyDate, dateToUnix } from 'utils/helper';
import axiosInstance from 'utils/axiosInstance';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import DeleteLogModal from 'components/DeleteLogModal';
import styles from './index.module.scss';
const DeviceLogs = () => {
const { t } = useTranslation();
@@ -176,9 +179,9 @@ const DeviceLogs = () => {
color="gradient-info"
header={t('device_logs.title')}
footerSlot={
<div className="p-4">
<div className={styles.footer}>
<CCollapse show={collapse}>
<div className="text-right">
<div className={styles.alignRight}>
<CPopover content={t('common.delete')}>
<CButton
color="light"
@@ -192,7 +195,7 @@ const DeviceLogs = () => {
</CButton>
</CPopover>
</div>
<CRow className="mb-3">
<CRow className={styles.datepickerRow}>
<CCol>
{t('common.from')}
<DatePicker includeTime onChange={(date) => modifyStart(date)} />
@@ -203,12 +206,12 @@ const DeviceLogs = () => {
</CCol>
</CRow>
<CCard>
<div className="overflow-auto" style={{ height: '250px' }}>
<div className={[styles.scrollableCard, 'overflow-auto'].join(' ')}>
<CDataTable
items={logs ?? []}
fields={columns}
loading={loading}
className="text-white"
className={styles.whiteIcon}
sorterValue={{ column: 'recorded', desc: 'true' }}
scopedSlots={{
recorded: (item) => <td>{prettyDate(item.recorded)}</td>,
@@ -237,8 +240,8 @@ const DeviceLogs = () => {
),
}}
/>
<CRow className={styles.loadMoreRow}>
{showLoadingMore && (
<div className="mb-3">
<LoadingButton
label={t('common.view_more')}
isLoadingLabel={t('common.loading_more_ellipsis')}
@@ -246,15 +249,15 @@ const DeviceLogs = () => {
action={showMoreLogs}
variant="outline"
/>
</div>
)}
</CRow>
</div>
</CCard>
</CCollapse>
<CButton show={collapse ? 'true' : 'false'} color="transparent" onClick={toggle} block>
<CIcon
name={collapse ? 'cilChevronTop' : 'cilChevronBottom'}
className="text-white"
className={styles.whiteIcon}
size="lg"
/>
</CButton>

View File

@@ -0,0 +1,23 @@
.whiteIcon {
color: white;
}
.footer {
padding: 20px;
}
.datepickerRow {
margin-bottom: 10px;
}
.scrollableCard {
height: 250px;
}
.loadMoreRow {
margin-bottom: 1%;
}
.alignRight {
float: right;
}

View File

@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CDataTable, CRow, CCol, CLabel, CInput } from '@coreui/react';
import PropTypes from 'prop-types';
import axiosInstance from 'utils/axiosInstance';
import { useAuth } from 'contexts/AuthProvider';
import { prettyDate } from 'utils/helper';
import LoadingButton from 'components/LoadingButton';
import styles from './index.module.scss';
const DeviceNotes = ({ serialNumber, notes, refreshNotes }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [currentNote, setCurrentNote] = useState('');
const [loading, setLoading] = useState(false);
const saveNote = () => {
setLoading(true);
const parameters = {
serialNumber,
notes: [{ note: currentNote }],
};
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.put(
`${endpoints.ucentralgw}/api/v1/device/${encodeURIComponent(serialNumber)}`,
parameters,
{ headers },
)
.then(() => {
setCurrentNote('');
refreshNotes();
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
const columns = [
{ key: 'created', label: t('common.date'), _style: { width: '30%' } },
{ key: 'createdBy', label: t('common.created_by'), _style: { width: '20%' } },
{ key: 'note', label: t('configuration.note'), _style: { width: '50%' } },
];
return (
<div>
<CRow className={styles.spacedRow}>
<CCol md="3">
<CLabel>{t('configuration.notes')} :</CLabel>
</CCol>
<CCol xs="9" md="7">
<CInput
id="notes-input"
name="text-input"
value={currentNote}
onChange={(e) => setCurrentNote(e.target.value)}
/>
</CCol>
<CCol>
<LoadingButton
label={t('common.add')}
isLoadingLabel={t('common.adding_ellipsis')}
isLoading={loading}
action={saveNote}
disabled={loading || currentNote === ''}
/>
</CCol>
</CRow>
<CRow>
<CCol md="3" />
<CCol xs="12" md="9">
<div className={['overflow-auto', styles.scrollableBox].join(' ')}>
<CDataTable
striped
responsive
border
loading={loading}
fields={columns}
className={styles.table}
items={notes || []}
noItemsView={{ noItems: t('common.no_items') }}
sorterValue={{ column: 'created', desc: 'true' }}
scopedSlots={{
created: (item) => (
<td>
{item.created && item.created !== 0 ? prettyDate(item.created) : t('common.na')}
</td>
),
}}
/>
</div>
</CCol>
</CRow>
</div>
);
};
DeviceNotes.propTypes = {
serialNumber: PropTypes.string.isRequired,
notes: PropTypes.arrayOf(PropTypes.instanceOf(Object)).isRequired,
refreshNotes: PropTypes.func.isRequired,
};
export default DeviceNotes;

View File

@@ -0,0 +1,15 @@
.scrollableBox {
height: 200px;
border-style: solid;
border-color: #ced2d8;
margin-bottom: 25px;
}
.table {
color: white;
}
.spacedRow {
margin-top: 5px;
margin-bottom: 20px;
}

View File

@@ -1,71 +0,0 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { useAuth, DeviceSearchBar as SearchBar } from 'ucentral-libs';
import { checkIfJson } from 'utils/helper';
const DeviceSearchBar = () => {
const { t } = useTranslation();
const history = useHistory();
const { currentToken, endpoints } = useAuth();
const [socket, setSocket] = useState(null);
const [results, setResults] = useState([]);
const [waitingSearch, setWaitingSearch] = useState('');
const search = (value) => {
if (socket.readyState === WebSocket.OPEN) {
if (value.length > 0 && value.match('^[a-fA-F0-9]+$')) {
setWaitingSearch('');
socket.send(
JSON.stringify({ command: 'serial_number_search', serial_prefix: value.toLowerCase() }),
);
} else {
setResults([]);
}
} else if (socket.readyState !== WebSocket.CONNECTING) {
setWaitingSearch(value);
setSocket(new WebSocket(`${endpoints.ucentralgw.replace('https', 'wss')}/api/v1/ws`));
} else {
setWaitingSearch(value);
}
};
const closeSocket = () => {
if (socket !== null) {
socket.close();
}
};
useEffect(() => {
if (socket !== null) {
socket.onopen = () => {
socket.send(`token:${currentToken}`);
};
socket.onmessage = (event) => {
if (checkIfJson(event.data)) {
const result = JSON.parse(event.data);
if (result.command === 'serial_number_search' && result.serialNumbers) {
setResults(result.serialNumbers);
}
}
};
if (waitingSearch.length > 0) {
search(waitingSearch);
}
}
return () => closeSocket();
}, [socket]);
useEffect(() => {
if (socket === null) {
setSocket(new WebSocket(`${endpoints.ucentralgw.replace('https', 'wss')}/api/v1/ws`));
}
}, []);
return <SearchBar t={t} search={search} results={results} history={history} />;
};
export default DeviceSearchBar;

View File

@@ -0,0 +1,36 @@
import React from 'react';
import { CPopover, CProgress, CProgressBar } from '@coreui/react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import { cleanBytesString } from 'utils/helper';
const MemoryBar = ({ usedBytes, totalBytes }) => {
const { t } = useTranslation();
const used = cleanBytesString(usedBytes);
const total = cleanBytesString(totalBytes);
const percentage = Math.floor((usedBytes / totalBytes) * 100);
return (
<CPopover content={t('status.used_total_memory', { used, total })}>
<CProgress>
<CProgressBar value={percentage}>
{percentage >= 25 ? t('status.percentage_used', { percentage, total }) : ''}
</CProgressBar>
<CProgressBar value={100 - percentage} color="transparent">
<div style={{ color: 'black' }}>
{percentage < 25
? t('status.percentage_free', { percentage: 100 - percentage, total })
: ''}
</div>
</CProgressBar>
</CProgress>
</CPopover>
);
};
MemoryBar.propTypes = {
usedBytes: PropTypes.number.isRequired,
totalBytes: PropTypes.number.isRequired,
};
export default React.memo(MemoryBar);

View File

@@ -1,7 +1,27 @@
import React, { useState, useEffect } from 'react';
import {
CCard,
CCardHeader,
CRow,
CCol,
CCardBody,
CBadge,
CModalBody,
CAlert,
CPopover,
CButton,
CSpinner,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import { cilSync } from '@coreui/icons';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { DeviceStatusCard as Card, useDevice, useAuth } from 'ucentral-libs';
import { prettyDate, secondsToDetailed } from 'utils/helper';
import MemoryBar from './MemoryBar';
import styles from './index.module.scss';
const DeviceStatusCard = () => {
const { t } = useTranslation();
@@ -12,6 +32,11 @@ const DeviceStatusCard = () => {
const [error, setError] = useState(false);
const [loading, setLoading] = useState(false);
const transformLoad = (load) => {
if (load === undefined) return t('common.na');
return `${((load / 65536) * 100).toFixed(2)}%`;
};
const getData = () => {
setLoading(true);
const options = {
@@ -50,17 +75,123 @@ const DeviceStatusCard = () => {
if (deviceSerialNumber) getData();
}, [deviceSerialNumber]);
if (!error) {
return (
<Card
t={t}
loading={loading}
error={error}
deviceSerialNumber={deviceSerialNumber}
getData={getData}
status={status}
lastStats={lastStats}
<CCard>
<CCardHeader>
<CRow>
<CCol>
<div className="text-value-lg">
{t('status.title', { serialNumber: deviceSerialNumber })}
</div>
</CCol>
<CCol>
<div className={styles.alignRight}>
<CPopover content={t('common.refresh')}>
<CButton color="secondary" onClick={getData} size="sm">
<CIcon content={cilSync} />
</CButton>
</CPopover>
</div>
</CCol>
</CRow>
</CCardHeader>
<CCardBody>
{(!lastStats || !status) && loading ? (
<div className={styles.centerContainer}>
<CSpinner className={styles.spinner} />
</div>
) : (
<div style={{ position: 'relative' }}>
<div className={styles.overlayContainer} hidden={!loading}>
<CSpinner className={styles.spinner} />
</div>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.connection_status')} :</CCol>
<CCol xs="10" md="7">
{status?.connected ? (
<CBadge color="success">{t('common.connected')}</CBadge>
) : (
<CBadge color="danger">{t('common.not_connected')}</CBadge>
)}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.uptime')} :</CCol>
<CCol xs="10" md="7">
{secondsToDetailed(
lastStats?.unit?.uptime,
t('common.day'),
t('common.days'),
t('common.hour'),
t('common.hours'),
t('common.minute'),
t('common.minutes'),
t('common.second'),
t('common.seconds'),
)}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.last_contact')} :</CCol>
<CCol xs="10" md="7">
{prettyDate(status?.lastContact)}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.localtime')} :</CCol>
<CCol xs="10" md="7">
{prettyDate(lastStats?.unit?.localtime)}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.load_averages')} :</CCol>
<CCol xs="10" md="7">
{transformLoad(lastStats?.unit?.load[0])}
{' / '}
{transformLoad(lastStats?.unit?.load[1])}
{' / '}
{transformLoad(lastStats?.unit?.load[2])}
</CCol>
</CRow>
<CRow className={styles.spacedRow}>
<CCol md="5">{t('status.memory')} :</CCol>
<CCol xs="9" md="6" style={{ paddingTop: '5px' }}>
<MemoryBar
usedBytes={
lastStats?.unit?.memory?.total && lastStats?.unit?.memory?.free
? lastStats?.unit?.memory?.total - lastStats?.unit?.memory?.free
: 0
}
totalBytes={lastStats?.unit?.memory?.total ?? 0}
/>
</CCol>
</CRow>
</div>
)}
</CCardBody>
</CCard>
);
}
return (
<CCard>
<CCardHeader>
<CRow>
<CCol>
<div className="text-value-lg">
{t('status.title', { serialNumber: deviceSerialNumber })}
</div>
</CCol>
</CRow>
</CCardHeader>
<CModalBody>
<CAlert hidden={!error} color="danger" className={styles.centerContainer}>
{t('status.error')}
</CAlert>
</CModalBody>
</CCard>
);
};
export default DeviceStatusCard;
export default React.memo(DeviceStatusCard);

View File

@@ -0,0 +1,29 @@
.centerContainer {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.overlayContainer {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
height: 100%;
}
.spacedRow {
margin-top: 5px;
margin-bottom: 5px;
}
.alignRight {
float: right;
}
.spinner {
height: 50px;
width: 50px;
}

View File

@@ -1,230 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { useUser, EditUserModal as Modal, useAuth, useToast } from 'ucentral-libs';
const initialState = {
Id: {
value: '',
error: false,
editable: false,
},
changePassword: {
value: false,
error: false,
editable: false,
},
currentPassword: {
value: '',
error: false,
editable: true,
},
email: {
value: '',
error: false,
editable: false,
},
description: {
value: '',
error: false,
editable: true,
},
name: {
value: '',
error: false,
editable: true,
},
userRole: {
value: '',
error: false,
editable: true,
},
notes: {
value: [],
editable: false,
},
};
const EditUserModal = ({ show, toggle, userId, getUsers }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [initialUser, setInitialUser] = useState({});
const [user, updateWithId, updateWithKey, setUser] = useUser(initialState);
const [policies, setPolicies] = useState({
passwordPolicy: '',
passwordPattern: '',
accessPolicy: '',
});
const getPasswordPolicy = () => {
axiosInstance
.post(`${endpoints.ucentralsec}/api/v1/oauth2?requirements=true`, {})
.then((response) => {
const newPolicies = response.data;
newPolicies.accessPolicy = `${endpoints.ucentralsec}${newPolicies.accessPolicy}`;
newPolicies.passwordPolicy = `${endpoints.ucentralsec}${newPolicies.passwordPolicy}`;
setPolicies(response.data);
})
.catch(() => {});
};
const getUser = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.ucentralsec}/api/v1/user/${userId}`, options)
.then((response) => {
const newUser = {};
for (const key of Object.keys(response.data)) {
if (key in initialState && key !== 'currentPassword') {
newUser[key] = {
...initialState[key],
value: response.data[key],
};
}
}
setInitialUser({ ...initialState, ...newUser });
setUser({ ...initialState, ...newUser });
})
.catch(() => {});
};
const updateUser = () => {
setLoading(true);
const parameters = {
id: userId,
};
let newData = false;
for (const key of Object.keys(user)) {
if (user[key].editable && user[key].value !== initialUser[key].value) {
if (key === 'currentPassword' && user[key].length < 8) {
updateWithKey('currentPassword', {
error: true,
});
newData = false;
break;
} else if (key === 'changePassword') {
parameters[key] = user[key].value === 'on';
newData = true;
} else {
parameters[key] = user[key].value;
newData = true;
}
}
}
if (newData) {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralsec}/api/v1/user/${userId}`, parameters, options)
.then(() => {
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
getUsers();
toggle();
})
.catch(() => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
color: 'danger',
autohide: true,
});
getUser();
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
getUsers();
toggle();
}
};
const addNote = (currentNote) => {
setLoading(true);
const parameters = {
id: userId,
notes: [{ note: currentNote }],
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralsec}/api/v1/user/${userId}`, parameters, options)
.then(() => {
getUser();
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (userId) {
getUser();
}
if (policies.passwordPattern.length === 0) {
getPasswordPolicy();
}
}, [userId]);
return (
<Modal
t={t}
user={user}
updateUserWithId={updateWithId}
saveUser={updateUser}
loading={loading}
policies={policies}
show={show}
toggle={toggle}
addNote={addNote}
/>
);
};
EditUserModal.propTypes = {
userId: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
getUsers: PropTypes.func.isRequired,
};
export default React.memo(EditUserModal);

View File

@@ -1,64 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { EventQueueModal as Modal, useAuth, useDevice, useToast } from 'ucentral-libs';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
const EventQueueModal = ({ show, toggle }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [result, setResult] = useState({});
const getQueue = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const parameters = {
serialNumber: deviceSerialNumber,
types: ['dhcp', 'wifi'],
};
axiosInstance
.post(
`${endpoints.ucentralgw}/api/v1/device/${deviceSerialNumber}/eventqueue`,
parameters,
options,
)
.then((response) => {
setResult(response.data);
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('commands.unable_queue'),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (show) getQueue();
}, [show]);
return <Modal t={t} show={show} toggle={toggle} loading={loading} result={result} />;
};
EventQueueModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
};
export default EventQueueModal;

View File

@@ -15,9 +15,11 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import styles from './index.module.scss';
const ConfigureModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
@@ -93,9 +95,9 @@ const ConfigureModal = ({ show, toggleModal }) => {
<div>
<CModalBody>
<CAlert color="danger">{t('factory_reset.warning')}</CAlert>
<CRow className="mt-3">
<p className="pl-4">{t('factory_reset.redirector')}</p>
<CForm className="pl-4">
<CRow className={styles.spacedRow}>
<p className={styles.spacedForm}>{t('factory_reset.redirector')}</p>
<CForm className={styles.spacedForm}>
<CSwitch
color="primary"
defaultChecked={keepRedirector}

View File

@@ -0,0 +1,7 @@
.spacedRow {
margin-top: 20px;
}
.spacedForm {
padding-left: 5%;
}

View File

@@ -1,71 +0,0 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import {
CButton,
CModal,
CModalBody,
CModalHeader,
CModalFooter,
CModalTitle,
} from '@coreui/react';
import { FirmwareHistoryTable, useAuth } from 'ucentral-libs';
const FirmwareHistoryModal = ({ serialNumber, show, toggle }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const getHistory = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.ucentralfms}/api/v1/revisionHistory/${serialNumber}`, options)
.then((response) => setData(response.data.history ?? []))
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => {
if (show) {
getHistory();
} else {
setData([]);
}
}, [show]);
return (
<CModal size="xl" show={show} onClose={toggle} scrollable>
<CModalHeader closeButton>
<CModalTitle>
#{serialNumber} {t('firmware.history_title')}
</CModalTitle>
</CModalHeader>
<CModalBody>
<FirmwareHistoryTable t={t} loading={loading} data={data} />
</CModalBody>
<CModalFooter>
<CButton color="secondary" onClick={toggle}>
{t('common.close')}
</CButton>
</CModalFooter>
</CModal>
);
};
FirmwareHistoryModal.propTypes = {
serialNumber: PropTypes.string.isRequired,
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
};
export default FirmwareHistoryModal;

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { CModalBody } from '@coreui/react';
import { v4 as createUuid } from 'uuid';
import { useAuth } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import axiosInstance from 'utils/axiosInstance';
const UpgradeWaitingBody = ({ serialNumber }) => {

View File

@@ -17,16 +17,19 @@ import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { dateToUnix } from 'utils/helper';
import 'react-widgets/styles.css';
import { useDevice, useAuth } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import getDeviceConnection from 'utils/deviceHelper';
import ButtonFooter from './UpgradeFooter';
import styles from './index.module.scss';
import UpgradeWaitingBody from './UpgradeWaitingBody';
const FirmwareUpgradeModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber, getDeviceConnection } = useDevice();
const { deviceSerialNumber } = useDevice();
const [isNow, setIsNow] = useState(true);
const [waitForUpgrade, setWaitForUpgrade] = useState(false);
const [date, setDate] = useState(new Date().toString());
@@ -153,8 +156,8 @@ const FirmwareUpgradeModal = ({ show, toggleModal }) => {
</CModalHeader>
<CModalBody>
<h6>{t('upgrade.directions')}</h6>
<CRow className="mt-3">
<CCol md="4" className="mt-2">
<CRow className={styles.spacedRow}>
<CCol md="4" className={styles.spacedColumn}>
<p>{t('upgrade.firmware_uri')}</p>
</CCol>
<CCol md="8">
@@ -171,9 +174,9 @@ const FirmwareUpgradeModal = ({ show, toggleModal }) => {
<CInvalidFeedback>{t('upgrade.need_uri')}</CInvalidFeedback>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol md="8">
<p>{t('common.execute_now')}</p>
<p className={styles.spacedText}>{t('common.execute_now')}</p>
</CCol>
<CCol>
<CSwitch
@@ -186,8 +189,8 @@ const FirmwareUpgradeModal = ({ show, toggleModal }) => {
/>
</CCol>
</CRow>
<CRow className="mt-3" hidden={isNow}>
<CCol md="4" className="mt-2">
<CRow className={styles.spacedRow} hidden={isNow}>
<CCol md="4" className={styles.spacedColumn}>
<p>{t('upgrade.time')}</p>
</CCol>
<CCol xs="12" md="8">
@@ -202,9 +205,12 @@ const FirmwareUpgradeModal = ({ show, toggleModal }) => {
<CInvalidFeedback>{t('common.need_date')}</CInvalidFeedback>
</CCol>
</CRow>
<CRow className="mt-3" hidden={true || !isNow || disabledWaiting || !deviceConnected}>
<CRow
className={styles.spacedRow}
hidden={true || !isNow || disabledWaiting || !deviceConnected}
>
<CCol md="8">
<p>
<p className={styles.spacedText}>
{t('upgrade.wait_for_upgrade')}
<b hidden={!disabledWaiting}> {t('upgrade.offline_device')}</b>
</p>

View File

@@ -0,0 +1,7 @@
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 7px;
}

View File

@@ -10,7 +10,9 @@ import {
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import styles from './index.module.scss';
const LatestStatisticsModal = ({ show, toggle }) => {
const { t } = useTranslation();
@@ -46,7 +48,7 @@ const LatestStatisticsModal = ({ show, toggle }) => {
return (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle className="text-dark">{t('statistics.latest_statistics')}</CModalTitle>
<CModalTitle className={styles.modalTitle}>{t('statistics.latest_statistics')}</CModalTitle>
</CModalHeader>
<CModalBody>
<pre className="ignore">{JSON.stringify(latestStats, null, 4)}</pre>

View File

@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { v4 as createUuid } from 'uuid';
import axiosInstance from 'utils/axiosInstance';
import { useAuth, useDevice } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import { unixToTime, capitalizeFirstLetter } from 'utils/helper';
import eventBus from 'utils/eventBus';
import DeviceStatisticsChart from './DeviceStatisticsChart';

View File

@@ -1,33 +1,31 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { CCard, CCardHeader, CCardBody, CRow, CCol, CPopover, CButton } from '@coreui/react';
import { cilSync } from '@coreui/icons';
import {
CDropdown,
CDropdownToggle,
CDropdownMenu,
CDropdownItem,
CCard,
CCardHeader,
CCardBody,
CRow,
CCol,
} from '@coreui/react';
import { cilOptions } from '@coreui/icons';
import CIcon from '@coreui/icons-react';
import eventBus from 'utils/eventBus';
import LifetimeStatsmodal from 'components/LifetimeStatsModal';
import StatisticsChartList from './StatisticsChartList';
import LatestStatisticsmodal from './LatestStatisticsModal';
import LatestStatisticsModal from './LatestStatisticsModal';
import styles from './index.module.scss';
const DeviceStatisticsCard = () => {
const history = useHistory();
const { deviceId } = useParams();
const { t } = useTranslation();
const [showLatestModal, setShowLatestModal] = useState(false);
const [showLifetimeModal, setShowLifetimeModal] = useState(false);
const toggleLatestModal = () => {
setShowLatestModal(!showLatestModal);
};
const toggleLifetimeModal = () => {
setShowLifetimeModal(!showLifetimeModal);
};
const goToAnalysis = () => {
history.push(`/devices/${deviceId}/wifianalysis`);
};
const refresh = () => {
eventBus.dispatch('refreshInterfaceStatistics', { message: 'Refresh interface statistics' });
};
@@ -38,43 +36,30 @@ const DeviceStatisticsCard = () => {
<CCardHeader>
<CRow>
<CCol>
<div className="text-value-xxl pt-2">{t('statistics.title')}</div>
<div className={['text-value-lg', styles.cardTitle].join(' ')}>
{t('statistics.title')}
</div>
</CCol>
<CCol sm="6" xxl="6">
<CRow>
<CCol sm="1" xxl="5" />
<CCol sm="4" xxl="2" className="text-right">
<CButton color="secondary" onClick={goToAnalysis}>
{t('wifi_analysis.title')}
</CButton>
</CCol>
<CCol sm="3" xxl="2" className="text-right">
<CButton color="secondary" onClick={toggleLatestModal}>
<CCol className={styles.cardOptions}>
<CDropdown className="m-1 btn-group">
<CDropdownToggle>
<CIcon name="cil-options" content={cilOptions} size="lg" color="primary" />
</CDropdownToggle>
<CDropdownMenu>
<CDropdownItem onClick={refresh}>{t('common.refresh')}</CDropdownItem>
<CDropdownItem onClick={toggleLatestModal}>
{t('statistics.show_latest')}
</CButton>
</CCol>
<CCol sm="3" xxl="2" className="text-right">
<CButton color="secondary" onClick={toggleLifetimeModal}>
Lifetime Statistics
</CButton>
</CCol>
<CCol sm="1" xxl="1" className="text-center">
<CPopover content={t('common.refresh')}>
<CButton color="secondary" onClick={refresh} size="sm">
<CIcon content={cilSync} />
</CButton>
</CPopover>
</CCol>
</CRow>
</CDropdownItem>
</CDropdownMenu>
</CDropdown>
</CCol>
</CRow>
</CCardHeader>
<CCardBody className="p-5">
<CCardBody className={styles.statsBody}>
<StatisticsChartList />
</CCardBody>
</CCard>
<LatestStatisticsmodal show={showLatestModal} toggle={toggleLatestModal} />
<LifetimeStatsmodal show={showLifetimeModal} toggle={toggleLifetimeModal} />
<LatestStatisticsModal show={showLatestModal} toggle={toggleLatestModal} />
</div>
);
};

View File

@@ -0,0 +1,15 @@
.cardOptions {
text-align: right;
}
.cardTitle {
padding-top: 10px;
}
.statsBody {
padding: 5%;
}
.modalTitle {
color: black;
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { CSelect } from '@coreui/react';
const LanguageSwitcher = () => {
const { i18n } = useTranslation();
return (
<CSelect
custom
defaultValue={i18n.language.split('-')[0]}
onChange={(e) => i18n.changeLanguage(e.target.value)}
>
<option value="de">Deutsche</option>
<option value="es">Español</option>
<option value="en">English</option>
<option value="fr">Français</option>
<option value="pt">Portugues</option>
</CSelect>
);
};
export default LanguageSwitcher;

View File

@@ -1,48 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import axiosInstance from 'utils/axiosInstance';
import { useTranslation } from 'react-i18next';
import { LifetimeStatsModal as Modal, useAuth, useDevice } from 'ucentral-libs';
const LifetimeStatsModal = ({ show, toggle }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber } = useDevice();
const [loading, setLoading] = useState(false);
const [data, setData] = useState({});
const getData = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.ucentralgw}/api/v1/device/${deviceSerialNumber}/statistics?lifetime=true`,
options,
)
.then((response) => {
setData(response.data);
})
.catch(() => {})
.finally(() => setLoading(false));
};
useEffect(() => {
if (show) getData();
}, [show]);
return <Modal t={t} loading={loading} show={show} toggle={toggle} data={data} />;
};
LifetimeStatsModal.propTypes = {
show: PropTypes.bool.isRequired,
toggle: PropTypes.func.isRequired,
};
export default LifetimeStatsModal;

View File

@@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CButton, CSpinner } from '@coreui/react';
const LoadingButton = ({
isLoading,
label,
isLoadingLabel,
action,
color,
variant,
block,
disabled,
}) => (
<CButton
variant={variant}
color={color}
onClick={action}
block={block}
disabled={isLoading || disabled}
>
{isLoading ? isLoadingLabel : label}
<CSpinner hidden={!isLoading} color="light" component="span" size="sm" />
</CButton>
);
LoadingButton.propTypes = {
isLoading: PropTypes.bool.isRequired,
block: PropTypes.bool,
disabled: PropTypes.bool,
label: PropTypes.string.isRequired,
isLoadingLabel: PropTypes.string.isRequired,
action: PropTypes.func.isRequired,
color: PropTypes.string,
variant: PropTypes.string,
};
LoadingButton.defaultProps = {
color: 'primary',
variant: '',
block: true,
disabled: false,
};
export default LoadingButton;

View File

@@ -1,34 +0,0 @@
import dagre from 'dagre';
import { isNode } from 'react-flow-renderer';
const setupDag = (elements, nodeWidth, nodeHeight) => {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: 'TB' });
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
} else {
dagreGraph.setEdge(el.source, el.target);
}
});
dagre.layout(dagreGraph);
return elements.map((el) => {
const newElement = el;
if (isNode(newElement)) {
const nodeWithPosition = dagreGraph.node(newElement.id);
newElement.targetPosition = 'top';
newElement.sourcePosition = 'bottom';
newElement.position = {
x: nodeWithPosition.x - nodeWidth / 2 + Math.random() / 1000,
y: nodeWithPosition.y - nodeHeight / 2,
};
}
return newElement;
});
};
export default setupDag;

View File

@@ -1,162 +0,0 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CRow, CCol } from '@coreui/react';
import { NetworkDiagram as Graph } from 'ucentral-libs';
import { useTranslation } from 'react-i18next';
import createLayoutedElements from './dagreAdapter';
const associationStyle = {
background: '#3399ff',
color: 'white',
border: '1px solid #777',
width: 220,
padding: 10,
};
const recognizedRadioStyle = {
background: '#2eb85c',
color: 'white',
width: 220,
padding: 15,
};
const unrecognizedRadioStyle = {
background: '#e55353',
color: 'white',
width: 220,
padding: 15,
};
const recognizedRadioNode = (radio) => (
<div className="align-middle">
<h6 className="align-middle mb-0">
Radio #{radio.radio} ({radio.channel < 16 ? '2G' : '5G'})
</h6>
</div>
);
const unrecognizedRadioNode = (t, radio) => (
<div className="align-middle">
<h6 className="align-middle mb-0">
Radio #{radio.radioIndex} ({t('common.unrecognized')})
</h6>
</div>
);
const associationNode = (associationInfo) => (
<div>
<CRow>
<CCol className="text-center">
<h6>{associationInfo.bssid}</h6>
</CCol>
</CRow>
<CRow>
<CCol className="text-left pl-4">Rx Rate : {associationInfo.rxRate}</CCol>
</CRow>
<CRow>
<CCol className="text-left pl-4">Tx Rate : {associationInfo.txRate}</CCol>
</CRow>
</div>
);
const NetworkDiagram = ({ show, radios, associations }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [elements, setElements] = useState([]);
const getX = (associationsAdded) => {
if (associationsAdded === 0) return 0;
if ((associationsAdded + 1) % 2 === 0) return -140 * (associationsAdded + 1);
return 140 * associationsAdded;
};
const parseData = () => {
setLoading(true);
const newElements = [];
const radiosAdded = {};
// Creating the radio nodes
for (const radio of radios) {
if (radiosAdded[radio.radio] === undefined) {
newElements.push({
id: `r-${radio.radio}`,
data: { label: recognizedRadioNode(radio) },
position: { x: 0, y: 200 * radio.radio },
type: 'input',
style: recognizedRadioStyle,
});
radiosAdded[radio.radio] = 0;
}
}
// Creating the association nodes and their edges
for (let i = 0; i < associations.length; i += 1) {
const assoc = associations[i];
// If the radio has not been added, we create a new unknown radio based on its index
if (radiosAdded[assoc.radio.radioIndex] === undefined) {
newElements.push({
id: `r-${assoc.radio.radioIndex}`,
data: { label: unrecognizedRadioNode(t, assoc.radio) },
position: { x: 0, y: 200 * assoc.radio.radioIndex },
type: 'input',
style: unrecognizedRadioStyle,
});
radiosAdded[assoc.radio.radioIndex] = 0;
}
// Adding the association
newElements.push({
id: `a-${assoc.bssid}`,
data: { label: associationNode(assoc) },
position: {
x: getX(radiosAdded[assoc.radio.radioIndex]),
y: 80 + 240 * assoc.radio.radioIndex,
},
style: associationStyle,
type: 'output',
});
radiosAdded[assoc.radio.radioIndex] += 1;
// Creating the edge
newElements.push({
id: `e-${assoc.radio.radioIndex}-${assoc.bssid}`,
source: `r-${assoc.radio.radioIndex}`,
target: `a-${assoc.bssid}`,
arrowHeadType: 'arrowclosed',
});
}
setElements(newElements);
setLoading(false);
};
useEffect(() => {
if (radios !== null && associations !== null) {
parseData();
}
}, [radios, associations]);
return (
<Graph
show={show}
loading={loading}
elements={createLayoutedElements(elements, 220, 80)}
setElements={setElements}
/>
);
};
NetworkDiagram.propTypes = {
show: PropTypes.bool,
radios: PropTypes.instanceOf(Array),
associations: PropTypes.instanceOf(Array),
};
NetworkDiagram.defaultProps = {
show: true,
radios: null,
associations: null,
};
export default NetworkDiagram;

View File

@@ -15,10 +15,13 @@ import DatePicker from 'react-widgets/DatePicker';
import PropTypes from 'prop-types';
import { dateToUnix } from 'utils/helper';
import 'react-widgets/styles.css';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import styles from './index.module.scss';
const ActionModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
@@ -105,8 +108,8 @@ const ActionModal = ({ show, toggleModal }) => {
/>
</CCol>
</CRow>
<CRow hidden={isNow} className="mt-2">
<CCol md="4" className="pt-2">
<CRow hidden={isNow} className={styles.spacedRow}>
<CCol md="4" className={styles.spacedDate}>
<p>{t('common.custom_date')}:</p>
</CCol>
<CCol xs="12" md="8">

View File

@@ -0,0 +1,11 @@
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 7px;
}
.spacedDate {
padding-top: 5px;
}

View File

@@ -1,16 +1,17 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { CAlert, CModalBody, CButton, CSpinner, CModalFooter } from '@coreui/react';
import { useAuth } from 'ucentral-libs';
import { CModalBody, CButton, CSpinner, CModalFooter } from '@coreui/react';
import { useAuth } from 'contexts/AuthProvider';
import axiosInstance from 'utils/axiosInstance';
import styles from './index.module.scss';
const WaitingForTraceBody = ({ serialNumber, commandUuid, toggle }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [secondsElapsed, setSecondsElapsed] = useState(0);
const [waitingForFile, setWaitingForFile] = useState(true);
const [error, setError] = useState(null);
const getTraceResult = () => {
const options = {
@@ -26,10 +27,6 @@ const WaitingForTraceBody = ({ serialNumber, commandUuid, toggle }) => {
if (response.data.waitingForFile === 0) {
setWaitingForFile(false);
}
if (response.data.errorCode !== 0) {
setWaitingForFile(false);
setError(response.data.errorText);
}
})
.catch(() => {});
};
@@ -86,19 +83,16 @@ const WaitingForTraceBody = ({ serialNumber, commandUuid, toggle }) => {
<CModalBody>
<h6>{t('trace.waiting_seconds', { seconds: secondsElapsed })}</h6>
<p>{t('trace.waiting_directions')}</p>
<div className="d-flex align-middle justify-content-center">
<div className={styles.centerDiv}>
<CSpinner hidden={!waitingForFile} />
<CButton
hidden={waitingForFile || error}
hidden={waitingForFile}
onClick={downloadTrace}
disabled={waitingForFile || error}
disabled={waitingForFile}
color="primary"
>
{t('trace.download_trace')}
</CButton>
<CAlert hidden={waitingForFile || !error} className="my-3" color="danger">
{t('trace.trace_not_successful', { error })}
</CAlert>
</div>
</CModalBody>
<CModalFooter>

View File

@@ -18,16 +18,20 @@ import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import getDeviceConnection from 'utils/deviceHelper';
import LoadingButton from 'components/LoadingButton';
import SuccessfulActionModalBody from 'components/SuccessfulActionModalBody';
import WaitingForTraceBody from './WaitingForTraceBody';
import styles from './index.module.scss';
const TraceModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { deviceSerialNumber, getDeviceConnection } = useDevice();
const { deviceSerialNumber } = useDevice();
const [hadSuccess, setHadSuccess] = useState(false);
const [hadFailure, setHadFailure] = useState(false);
const [blockFields, setBlockFields] = useState(false);
@@ -133,7 +137,7 @@ const TraceModal = ({ show, toggleModal }) => {
<div>
<CModalBody>
<h6>{t('trace.directions')}</h6>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol>
<CButton
disabled={blockFields}
@@ -155,8 +159,8 @@ const TraceModal = ({ show, toggleModal }) => {
</CButton>
</CCol>
</CRow>
<CRow className="mt-3">
<CCol md="4" className="pt-2">
<CRow className={styles.spacedRow}>
<CCol md="4" className={styles.spacedColumn}>
{usingDuration ? 'Duration: ' : 'Packets: '}
</CCol>
<CCol xs="12" md="8">
@@ -187,7 +191,7 @@ const TraceModal = ({ show, toggleModal }) => {
)}
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol md="7">{t('trace.choose_network')}:</CCol>
<CCol>
<CForm>
@@ -216,9 +220,9 @@ const TraceModal = ({ show, toggleModal }) => {
</CForm>
</CCol>
</CRow>
<CRow className="mt-3" hidden={!isDeviceConnected}>
<CRow className={styles.spacedRow} hidden={!isDeviceConnected}>
<CCol md="8">
<p>{t('trace.wait_for_file')}</p>
<p className={styles.spacedText}>{t('trace.wait_for_file')}</p>
</CCol>
<CCol>
<CSwitch

View File

@@ -0,0 +1,14 @@
.spacedRow {
margin-top: 20px;
}
.spacedColumn {
margin-top: 7px;
}
.centerDiv {
display: flex;
justify-content: center;
align-items: center;
height: 20px;
}

View File

@@ -14,11 +14,14 @@ import {
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import { useAuth } from 'contexts/AuthProvider';
import { useDevice } from 'contexts/DeviceProvider';
import axiosInstance from 'utils/axiosInstance';
import eventBus from 'utils/eventBus';
import { LoadingButton, useAuth, useDevice } from 'ucentral-libs';
import LoadingButton from 'components/LoadingButton';
import WifiChannelTable from 'components/WifiScanResultModal/WifiChannelTable';
import 'react-widgets/styles.css';
import styles from './index.module.scss';
const WifiScanModal = ({ show, toggleModal }) => {
const { t } = useTranslation();
@@ -130,12 +133,12 @@ const WifiScanModal = ({ show, toggleModal }) => {
<CModalBody>
<div hidden={hideOptions || waiting}>
<h6>{t('scan.directions')}</h6>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol md="3">
<p className="pl-2">Verbose:</p>
<p className={styles.spacedText}>Verbose:</p>
</CCol>
<CCol>
<CForm className="pl-4">
<CForm className={styles.spacedSwitch}>
<CSwitch
color="primary"
defaultChecked={choseVerbose}
@@ -146,12 +149,12 @@ const WifiScanModal = ({ show, toggleModal }) => {
</CForm>
</CCol>
</CRow>
<CRow className="mt-3">
<CRow className={styles.spacedRow}>
<CCol md="3">
<p className="pl-2">{t('scan.active')}:</p>
<p className={styles.spacedText}>{t('scan.active')}:</p>
</CCol>
<CCol>
<CForm className="pl-4">
<CForm className={styles.spacedSwitch}>
<CSwitch
color="primary"
defaultChecked={activeScan}
@@ -170,13 +173,13 @@ const WifiScanModal = ({ show, toggleModal }) => {
</CCol>
</CRow>
<CRow>
<CCol className="d-flex align-middle justify-content-center">
<CCol className={styles.centerDiv}>
<CSpinner />
</CCol>
</CRow>
</div>
<div hidden={!hadSuccess && !hadFailure}>
<CRow className="mb-2">
<CRow className={styles.bottomSpace}>
<CCol>
<h6>{t('scan.result_directions')}</h6>
</CCol>

View File

@@ -0,0 +1,22 @@
.spacedRow {
margin-top: 20px;
}
.spacedText {
padding-left: 2%;
}
.spacedSwitch {
padding-left: 5%;
}
.bottomSpace {
margin-bottom: 20px;
}
.centerDiv {
display: flex;
justify-content: center;
align-items: center;
height: 20px;
}

View File

@@ -3,6 +3,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import 'react-widgets/styles.css';
import styles from './index.module.scss';
const WifiChannelCard = ({ channel }) => {
const { t } = useTranslation();
@@ -11,13 +12,13 @@ const WifiChannelCard = ({ channel }) => {
return (
<CCard>
<CCardHeader>
<CCardTitle className="text-dark">
<CCardTitle className={styles.cardTitle}>
{t('scan.channel')} #{channel.channel}
</CCardTitle>
</CCardHeader>
<CCardBody>
<div className="overflow-auto" style={{ height: '250px' }}>
<CDataTable items={channel.devices} fields={columns} className="text-white" />
<div className={[styles.scrollable, 'overflow-auto'].join(' ')}>
<CDataTable items={channel.devices} fields={columns} className={styles.datatable} />
</div>
</CCardBody>
</CCard>

View File

@@ -12,6 +12,7 @@ import {
import PropTypes from 'prop-types';
import { prettyDate } from 'utils/helper';
import WifiChannelTable from './WifiChannelTable';
import styles from './index.module.scss';
const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
const { t } = useTranslation();
@@ -49,7 +50,7 @@ const WifiScanResultModal = ({ show, toggle, scanResults, date }) => {
return (
<CModal size="lg" show={show} onClose={toggle}>
<CModalHeader closeButton>
<CModalTitle className="text-dark">
<CModalTitle className={styles.modalTitle}>
{date !== '' ? prettyDate(date) : ''} {t('scan.results')}
</CModalTitle>
</CModalHeader>

View File

@@ -0,0 +1,15 @@
.modalTitle {
color: black;
}
.cardTitle {
color: black;
}
.scrollable {
height: 250px;
}
.datatable {
color: white;
}

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
const AuthContext = React.createContext();
export const AuthProvider = ({ token, apiEndpoints, children }) => {
const [currentToken, setCurrentToken] = useState(token);
const [endpoints, setEndpoints] = useState(apiEndpoints);
return (
<AuthContext.Provider value={{ currentToken, setCurrentToken, endpoints, setEndpoints }}>
{children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
token: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
apiEndpoints: PropTypes.instanceOf(Object),
};
AuthProvider.defaultProps = {
apiEndpoints: {},
};
export const useAuth = () => React.useContext(AuthContext);

View File

@@ -0,0 +1,21 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
const DeviceContext = React.createContext();
export const DeviceProvider = ({ serialNumber, children }) => {
const [deviceSerialNumber, setDeviceSerialNumber] = useState(serialNumber);
return (
<DeviceContext.Provider value={{ deviceSerialNumber, setDeviceSerialNumber }}>
{children}
</DeviceContext.Provider>
);
};
DeviceProvider.propTypes = {
serialNumber: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export const useDevice = () => React.useContext(DeviceContext);

View File

@@ -8,7 +8,6 @@ i18next
.use(HttpApi)
.use(LanguageDetector)
.init({
load: 'languageOnly',
supportedLngs: ['de', 'en', 'es', 'fr', 'pt'],
fallbackLng: 'en',
nonExplicitSupportedLngs: true,

View File

@@ -0,0 +1,47 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { Suspense } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { v4 as createUuid } from 'uuid';
import { CContainer, CFade } from '@coreui/react';
import routes from 'routes';
import { Translation } from 'react-i18next';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse" />
</div>
);
const TheContent = () => (
<main className="c-main">
<CContainer fluid>
<Suspense fallback={loading}>
<Translation>
{(t) => (
<Switch>
{routes.map(
(route) =>
route.component && (
<Route
key={createUuid()}
path={route.path}
exact={route.exact}
name={t(route.name)}
render={(props) => (
<CFade>
<route.component {...props} />
</CFade>
)}
/>
),
)}
<Redirect from="/" to="/devices" />
</Switch>
)}
</Translation>
</Suspense>
</CContainer>
</main>
);
export default React.memo(TheContent);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { CFooter } from '@coreui/react';
import { Translation } from 'react-i18next';
const TheFooter = () => (
<Translation>
{(t) => (
<CFooter fixed={false}>
<div>{t('footer.version')} 0.9.13</div>
<div className="mfs-auto">
<span className="mr-1">{t('footer.powered_by')}</span>
<a href="https://coreui.io/react" target="_blank" rel="noopener noreferrer">
{t('footer.coreui_for_react')}
</a>
</div>
</CFooter>
)}
</Translation>
);
export default React.memo(TheFooter);

View File

@@ -0,0 +1,82 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import {
CHeader,
CToggler,
CHeaderBrand,
CHeaderNav,
CSubheader,
CBreadcrumbRouter,
CLink,
CPopover,
} from '@coreui/react';
import PropTypes from 'prop-types';
import CIcon from '@coreui/icons-react';
import { cilAccountLogout } from '@coreui/icons';
import { logout } from 'utils/authHelper';
import routes from 'routes';
import { LanguageSwitcher } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
const TheHeader = ({ showSidebar, setShowSidebar }) => {
const { t, i18n } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [translatedRoutes, setTranslatedRoutes] = useState(routes);
const toggleSidebar = () => {
const val = [true, 'responsive'].includes(showSidebar) ? false : 'responsive';
setShowSidebar(val);
};
const toggleSidebarMobile = () => {
const val = [false, 'responsive'].includes(showSidebar) ? true : 'responsive';
setShowSidebar(val);
};
useEffect(() => {
setTranslatedRoutes(routes.map(({ name, ...rest }) => ({ ...rest, name: t(name) })));
}, [i18n.language]);
return (
<CHeader withSubheader>
<CToggler inHeader className="ml-md-3 d-lg-none" onClick={toggleSidebarMobile} />
<CToggler inHeader className="ml-3 d-md-down-none" onClick={toggleSidebar} />
<CHeaderBrand className="mx-auto d-lg-none" to="/">
<CIcon name="logo" height="48" alt="Logo" />
</CHeaderBrand>
<CHeaderNav className="d-md-down-none mr-auto" />
<CHeaderNav className="px-3">
<LanguageSwitcher i18n={i18n} />
</CHeaderNav>
<CHeaderNav className="px-3">
<CPopover content={t('common.logout')}>
<CLink className="c-subheader-nav-link">
<CIcon
name="cilAccountLogout"
content={cilAccountLogout}
size="2xl"
onClick={() => logout(currentToken, endpoints.ucentralsec)}
/>
</CLink>
</CPopover>
</CHeaderNav>
<CSubheader className="px-3 justify-content-between">
<CBreadcrumbRouter
className="border-0 c-subheader-nav m-0 px-0 px-md-3"
routes={translatedRoutes}
/>
</CSubheader>
</CHeader>
);
};
TheHeader.propTypes = {
showSidebar: PropTypes.string.isRequired,
setShowSidebar: PropTypes.func.isRequired,
};
export default TheHeader;

View File

@@ -0,0 +1,64 @@
import React from 'react';
import {
CCreateElement,
CSidebar,
CSidebarBrand,
CSidebarNav,
CSidebarNavDivider,
CSidebarNavTitle,
CSidebarMinimizer,
CSidebarNavDropdown,
CSidebarNavItem,
} from '@coreui/react';
import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next';
import styles from './index.module.scss';
const TheSidebar = ({ showSidebar, setShowSidebar }) => {
const { t } = useTranslation();
const navigation = [
{
_tag: 'CSidebarNavItem',
name: t('common.device_list'),
to: '/devices',
icon: 'cilNotes',
},
];
return (
<CSidebar show={showSidebar} onShowChange={(val) => setShowSidebar(val)}>
<CSidebarBrand className="d-md-down-none" to="/devices">
<img
className={[styles.sidebarImgFull, 'c-sidebar-brand-full'].join(' ')}
src="assets/OpenWiFi_LogoLockup_WhiteColour.svg"
alt="OpenWifi"
/>
<img
className={[styles.sidebarImgMinimized, 'c-sidebar-brand-minimized'].join(' ')}
src="assets/OpenWiFi_LogoLockup_WhiteColour.svg"
alt="OpenWifi"
/>
</CSidebarBrand>
<CSidebarNav>
<CCreateElement
items={navigation}
components={{
CSidebarNavDivider,
CSidebarNavDropdown,
CSidebarNavItem,
CSidebarNavTitle,
}}
/>
</CSidebarNav>
<CSidebarMinimizer className="c-d-md-down-none" />
</CSidebar>
);
};
TheSidebar.propTypes = {
showSidebar: PropTypes.string.isRequired,
setShowSidebar: PropTypes.func.isRequired,
};
export default React.memo(TheSidebar);

View File

@@ -0,0 +1,9 @@
.sidebarImgFull {
height: 100px;
width: 230px;
}
.sidebarImgMinimized {
height: 75px;
width: 75px;
}

View File

@@ -1,74 +1,25 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { logout } from 'utils/authHelper';
import routes from 'routes';
import { Header, Sidebar, Footer, PageContainer, ToastProvider, useAuth } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { Header } from 'ucentral-libs';
import Sidebar from './Sidebar';
import TheContent from './Content';
import TheFooter from './Footer';
const TheLayout = () => {
const [showSidebar, setShowSidebar] = useState('responsive');
const { endpoints, currentToken, user, avatar, logout } = useAuth();
const { endpoints, currentToken } = useAuth();
const { t, i18n } = useTranslation();
const navigation = [
{
_tag: 'CSidebarNavDropdown',
name: t('common.devices'),
icon: 'cilRouter',
_children: [
{
addLinkClass: 'c-sidebar-nav-link ml-2',
_tag: 'CSidebarNavItem',
name: t('common.dashboard'),
to: '/devicedashboard',
},
{
addLinkClass: 'c-sidebar-nav-link ml-2',
_tag: 'CSidebarNavItem',
name: t('common.table'),
to: '/devices',
},
],
},
{
_tag: 'CSidebarNavDropdown',
name: t('firmware.title'),
icon: 'cilSave',
_children: [
{
addLinkClass: 'c-sidebar-nav-link ml-2',
_tag: 'CSidebarNavItem',
name: t('common.dashboard'),
to: '/firmwaredashboard',
},
{
addLinkClass: 'c-sidebar-nav-link ml-2',
_tag: 'CSidebarNavItem',
name: t('common.table'),
to: '/firmware',
},
],
},
{
_tag: 'CSidebarNavItem',
name: t('user.users'),
to: '/users',
icon: 'cilPeople',
},
{
_tag: 'CSidebarNavItem',
name: t('common.system'),
to: '/system',
icon: 'cilSettings',
},
];
return (
<div className="c-app c-default-layout">
<Sidebar
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
logo="assets/OpenWiFi_LogoLockup_WhiteColour.svg"
options={navigation}
redirectTo="/devices"
t={t}
logo="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
/>
<div className="c-wrapper">
<Header
@@ -78,18 +29,13 @@ const TheLayout = () => {
t={t}
i18n={i18n}
logout={logout}
logo="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
authToken={currentToken}
endpoints={endpoints}
user={user}
avatar={avatar}
/>
<div className="c-body">
<ToastProvider>
<PageContainer t={t} routes={routes} redirectTo="/devices" />
</ToastProvider>
<TheContent />
</div>
<Footer t={t} version="2.1.0" />
<TheFooter />
</div>
</div>
);

View File

@@ -1,360 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { DeviceDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
const DeviceDashboard = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [data, setData] = useState({
status: {
datasets: [],
labels: [],
},
healths: {
datasets: [],
labels: [],
},
associations: {
datasets: [],
labels: [],
},
upTimes: {
datasets: [],
labels: [],
},
deviceType: {
datasets: [],
labels: [],
},
vendors: {
datasets: [],
labels: [],
},
certificates: {
datasets: [],
labels: [],
},
commands: {
datasets: [],
labels: [],
},
memoryUsed: {
datasets: [],
labels: [],
},
});
const parseData = (newData) => {
const parsedData = newData;
// Status pie chart
const statusDs = [];
const statusColors = [];
const statusLabels = [];
let totalDevices = parsedData.status.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.status) {
statusDs.push(Math.round((point.value / totalDevices) * 100));
statusLabels.push(point.tag);
let color = '';
switch (point.tag) {
case 'connected':
color = '#41B883';
break;
case 'not connected':
color = '#39f';
break;
case 'disconnected':
color = '#e55353';
break;
default:
break;
}
statusColors.push(color);
}
parsedData.status = {
datasets: [
{
data: statusDs,
backgroundColor: statusColors,
},
],
labels: statusLabels,
};
// General Health
let devicesAt100 = 0;
let devicesUp90 = 0;
let devicesUp60 = 0;
let devicesDown60 = 0;
// Health pie chart
const healthDs = [];
const healthColors = [];
const healthLabels = [];
totalDevices = parsedData.healths.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.healths) {
healthDs.push(Math.round((point.value / totalDevices) * 100));
healthLabels.push(point.tag);
let color = '';
switch (point.tag) {
case '100%':
color = '#41B883';
devicesAt100 += point.value;
break;
case '>90%':
color = '#ffff5c';
devicesUp90 += point.value;
break;
case '>60%':
color = '#f9b115';
devicesUp60 += point.value;
break;
case '<60%':
color = '#e55353';
devicesDown60 += point.value;
break;
default:
color = '#39f';
break;
}
healthColors.push(color);
}
parsedData.healths = {
datasets: [
{
data: healthDs,
backgroundColor: healthColors,
},
],
labels: healthLabels,
};
parsedData.overallHealth =
totalDevices === 0
? '-'
: `${Math.round(
(devicesAt100 * 100 + devicesUp90 * 95 + devicesUp60 * 75 + devicesDown60 * 35) /
totalDevices,
)}%`;
// Associations pie chart
const associationsDs = [];
const associationsColors = [];
const associationsLabels = [];
const totalAssociations = parsedData.associations.reduce((acc, point) => acc + point.value, 0);
for (let i = 0; i < parsedData.associations.length; i += 1) {
const point = parsedData.associations[i];
associationsDs.push(Math.round((point.value / totalAssociations) * 100));
associationsLabels.push(point.tag);
switch (parsedData.associations[i].tag) {
case '2G':
associationsColors.push('#41B883');
break;
case '5G':
associationsColors.push('#3399ff');
break;
default:
associationsColors.push('#636f83');
break;
}
}
parsedData.totalAssociations = totalAssociations;
parsedData.associations = {
datasets: [
{
data: associationsDs,
backgroundColor: associationsColors,
},
],
labels: associationsLabels,
};
// Uptime bar chart
const uptimeDs = [0, 0, 0, 0];
const uptimeLabels = ['now', '>day', '>week', '>month'];
const uptimeColors = ['#321fdb', '#321fdb', '#321fdb', '#321fdb'];
for (const point of parsedData.upTimes) {
switch (point.tag) {
case 'now':
uptimeDs[0] = point.value;
break;
case '>day':
uptimeDs[1] = point.value;
break;
case '>week':
uptimeDs[2] = point.value;
break;
case '>month':
uptimeDs[3] = point.value;
break;
default:
uptimeDs.push(point.value);
uptimeLabels.push(point.tag);
uptimeColors.push('#321fdb');
}
}
parsedData.upTimes = {
datasets: [
{
label: 'Devices',
data: uptimeDs,
backgroundColor: uptimeColors,
},
],
labels: uptimeLabels,
};
// Vendors bar chart
const vendorsTypeDs = [];
const vendorsColors = [];
const vendorsLabels = [];
const sortedVendors = parsedData.vendors.sort((a, b) => (a.value < b.value ? 1 : -1));
for (const point of sortedVendors) {
vendorsTypeDs.push(point.value);
vendorsLabels.push(point.tag === '' ? 'Unknown' : point.tag);
vendorsColors.push('#eb7474');
}
const otherVendors = vendorsTypeDs.slice(5).reduce((acc, vendor) => acc + vendor, 0);
parsedData.vendors = {
datasets: [
{
label: 'Devices',
data: vendorsTypeDs.slice(0, 5).concat([otherVendors]),
backgroundColor: vendorsColors,
},
],
labels: vendorsLabels.slice(0, 5).concat(['Others']),
};
// Device Type pie chart
const deviceTypeDs = [];
const deviceTypeColors = [];
const deviceTypeLabels = [];
const sortedTypes = parsedData.deviceType.sort((a, b) => (a.value < b.value ? 1 : -1));
for (let i = 0; i < sortedTypes.length; i += 1) {
const point = sortedTypes[i];
deviceTypeDs.push(point.value);
deviceTypeLabels.push(point.tag);
deviceTypeColors.push(COLOR_LIST[i]);
}
const otherTypes = deviceTypeDs.slice(5).reduce((acc, type) => acc + type, 0);
parsedData.deviceType = {
datasets: [
{
data: deviceTypeDs.slice(0, 5).concat([otherTypes]),
backgroundColor: deviceTypeColors,
},
],
labels: deviceTypeLabels.slice(0, 5).concat(['Others']),
};
// Certificates pie chart
const certificatesDs = [];
const certificatesColors = [];
const certificatesLabels = [];
const totalCerts = parsedData.certificates.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.certificates) {
certificatesDs.push(Math.round((point.value / totalCerts) * 100));
certificatesLabels.push(point.tag);
let color = '';
switch (point.tag) {
case 'verified':
color = '#41B883';
break;
case 'serial mismatch':
color = '#f9b115';
break;
case 'no certificate':
color = '#e55353';
break;
default:
color = '#39f';
break;
}
certificatesColors.push(color);
}
parsedData.certificates = {
datasets: [
{
data: certificatesDs,
backgroundColor: certificatesColors,
},
],
labels: certificatesLabels,
};
// Commands bar chart
const commandsDs = [];
const commandsColors = [];
const commandsLabels = [];
for (const point of parsedData.commands) {
commandsDs.push(point.value);
commandsLabels.push(point.tag);
commandsColors.push('#39f');
}
parsedData.commands = {
datasets: [
{
label: t('common.commands_executed'),
data: commandsDs,
backgroundColor: commandsColors,
},
],
labels: commandsLabels,
};
// Memory Used bar chart
const memoryDs = [];
const memoryColors = [];
const memoryLabels = [];
for (const point of parsedData.memoryUsed) {
memoryDs.push(point.value);
memoryLabels.push(point.tag);
memoryColors.push('#636f83');
}
parsedData.memoryUsed = {
datasets: [
{
label: 'Devices',
data: memoryDs,
backgroundColor: memoryColors,
},
],
labels: memoryLabels,
};
setData(parsedData);
};
const getDashboard = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.ucentralgw}/api/v1/deviceDashboard`, {
headers,
})
.then((response) => {
parseData(response.data);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
getDashboard();
}, []);
return <Dashboard loading={loading} t={t} data={data} />;
};
export default DeviceDashboard;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import DeviceList from 'components/DeviceListTable';
import DeviceList from '../../components/DeviceListTable';
const DeviceListPage = () => (
<div className="App">

View File

@@ -3,26 +3,25 @@ import { useParams } from 'react-router-dom';
import { CRow, CCol } from '@coreui/react';
import DeviceHealth from 'components/DeviceHealth';
import DeviceConfiguration from 'components/DeviceConfiguration';
import DeviceStatusCard from 'components/DeviceStatusCard';
import CommandHistory from 'components/CommandHistory';
import DeviceLogs from 'components/DeviceLogs';
import DeviceStatisticsCard from 'components/InterfaceStatistics';
import DeviceActionCard from 'components/DeviceActionCard';
import axiosInstance from 'utils/axiosInstance';
import { DeviceProvider } from 'ucentral-libs';
import DeviceStatusCard from 'components/DeviceStatusCard';
import { DeviceProvider } from 'contexts/DeviceProvider';
const DevicePage = () => {
const { deviceId } = useParams();
return (
<div className="App">
<DeviceProvider axiosInstance={axiosInstance} serialNumber={deviceId}>
<DeviceProvider serialNumber={deviceId}>
<CRow>
<CCol xs="12" lg="6">
<CCol xs="12" sm="6">
<DeviceStatusCard />
<DeviceConfiguration />
</CCol>
<CCol xs="12" lg="6">
<CCol xs="12" sm="6">
<DeviceLogs />
<DeviceHealth />
<DeviceActionCard />

View File

@@ -0,0 +1,3 @@
.spacedRow {
margin-top: 10px;
}

View File

@@ -1,320 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FirmwareDashboard as Dashboard, useAuth, COLOR_LIST } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
const FirmwareDashboard = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [data, setData] = useState({
status: {
datasets: [],
labels: [],
},
deviceType: {
datasets: [],
labels: [],
},
firmwareDistribution: {
datasets: [],
labels: [],
},
latest: {
datasets: [],
labels: [],
},
unknownFirmwares: {
datasets: [],
labels: [],
},
ouis: {
datasets: [],
labels: [],
},
endpoints: [],
latestSoftwareRate: '-',
});
const getOuiInfo = async (oui) => {
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
return axiosInstance
.get(`${endpoints.ucentralgw}/api/v1/ouis?macList=${oui.join(',')}`, {
headers,
})
.then((response) => {
const matchedObject = {};
for (let i = 0; i < response.data.tagList.length; i += 1) {
matchedObject[oui[i]] = response.data.tagList[i];
}
return matchedObject;
})
.catch(() => {});
};
const parseData = async (newData) => {
const parsedData = newData;
// Status pie chart
const statusDs = [];
const statusColors = [];
const statusLabels = [];
const totalDevices = parsedData.status.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.status) {
statusDs.push(Math.round((point.value / totalDevices) * 100));
statusLabels.push(point.tag);
let color = '';
switch (point.tag) {
case 'connected':
color = '#41B883';
break;
case 'not connected':
color = '#e55353';
break;
default:
break;
}
statusColors.push(color);
}
parsedData.status = {
datasets: [
{
data: statusDs,
backgroundColor: statusColors,
},
],
labels: statusLabels,
};
// Device Type pie chart
const deviceTypeDs = [];
const deviceTypeColors = [];
const deviceTypeLabels = [];
const sortedTypes = parsedData.deviceTypes.sort((a, b) => (a.value < b.value ? 1 : -1));
for (let i = 0; i < sortedTypes.length; i += 1) {
const point = sortedTypes[i];
deviceTypeDs.push(point.value);
deviceTypeLabels.push(point.tag);
deviceTypeColors.push(COLOR_LIST[i]);
}
const otherTypes = deviceTypeDs.slice(5).reduce((acc, type) => acc + type, 0);
parsedData.deviceType = {
datasets: [
{
data: deviceTypeDs.slice(0, 5).concat([otherTypes]),
backgroundColor: deviceTypeColors,
},
],
labels: deviceTypeLabels.slice(0, 5).concat(['Others']),
};
// Latest/unknown distribution
const usingLatestFirmware =
parsedData.usingLatest.length > 0
? parsedData.usingLatest.reduce((acc, firmware) => acc + firmware.value, 0)
: 0;
const unknownFirmware = parsedData.numberOfDevices - usingLatestFirmware;
parsedData.usingLatestFirmware = usingLatestFirmware;
parsedData.firmwareDistribution = {
datasets: [
{
label: t('common.devices'),
data: [unknownFirmware, usingLatestFirmware],
backgroundColor: ['#e55353', '#41B883'],
},
],
labels: [t('common.unknown'), t('common.latest')],
};
if (
parsedData.numberOfDevices === undefined ||
Number.isNaN(parsedData.numberOfDevices) ||
parsedData === 0
) {
parsedData.latestSoftwareRate = '-';
} else {
parsedData.latestSoftwareRate = `${(
(parsedData.usingLatestFirmware / parsedData.numberOfDevices) *
100
).toFixed(1)}%`;
}
// Average firmware age calculation
const usingUnknownFirmwareFromArray =
parsedData.unknownFirmwares.length > 0
? parsedData.unknownFirmwares.reduce((acc, firmware) => acc + firmware.value, 0)
: 0;
const devicesForAverage = parsedData.numberOfDevices - usingUnknownFirmwareFromArray;
parsedData.averageFirmwareAge =
parsedData.totalSecondsOld[0].value /
(devicesForAverage > 0 ? devicesForAverage : 1) /
(24 * 60 * 60);
// Latest firmware distribution
const latestDs = [];
const latestColors = [];
const latestLabels = [];
const usingLatest = parsedData.usingLatest.sort((a, b) => (a.value < b.value ? 1 : -1));
for (const point of usingLatest) {
latestDs.push(point.value);
if (point.tag === '') {
latestLabels.push('Unknown');
} else if (point.tag.split(' / ').length > 1) {
latestLabels.push(point.tag.split(' / ')[1]);
} else {
latestLabels.push(point.tag);
}
latestColors.push('#39f');
}
parsedData.latest = {
datasets: [
{
label: t('common.firmware'),
data: latestDs.slice(0, 5),
backgroundColor: latestColors,
},
],
labels: latestLabels.slice(0, 5),
};
// Unknown firmware distribution
const unknownDs = [];
const unknownColors = [];
const unknownLabels = [];
const unknownFirmwares = parsedData.unknownFirmwares.sort((a, b) =>
a.value < b.value ? 1 : -1,
);
for (const point of unknownFirmwares) {
unknownDs.push(point.value);
if (point.tag === '') {
unknownLabels.push('Unknown');
} else if (point.tag.split(' / ').length > 1) {
unknownLabels.push(point.tag.split(' / ')[1]);
} else {
unknownLabels.push(point.tag);
}
unknownColors.push('#39f');
}
parsedData.unknownFirmwares = {
datasets: [
{
label: t('common.firmware'),
data: unknownDs.slice(0, 5),
backgroundColor: unknownColors,
},
],
labels: unknownLabels.slice(0, 5),
};
// OUIs bar graph
const ouiCompleteInfo = [];
const ouisLabels = [];
const sortedOuis = parsedData.ouis.sort((a, b) => (a.value < b.value ? 1 : -1));
for (const point of sortedOuis) {
ouiCompleteInfo.push({
value: point.value,
tag: point.tag,
});
ouisLabels.push(point.tag === '' ? 'Unknown' : point.tag);
}
const ouiDetails = await getOuiInfo(ouisLabels);
// Merging 'Good' labels with ouiCompleteInfo
if (ouiDetails !== null) {
for (let i = 0; i < ouiCompleteInfo.length; i += 1) {
ouiCompleteInfo[i].label =
ouiDetails[ouiCompleteInfo[i].tag].value !== undefined &&
ouiDetails[ouiCompleteInfo[i].tag].value !== ''
? ouiDetails[ouiCompleteInfo[i].tag].value
: 'Unknown';
}
}
// Merging OUIs that have the same label that we got from getOuiInfo
const finalOuis = {};
for (const oui of ouiCompleteInfo) {
if (finalOuis[oui.label] === undefined) {
finalOuis[oui.label] = {
label: oui.label,
value: oui.value,
};
} else {
finalOuis[oui.label] = {
label: oui.label,
value: finalOuis[oui.label].value + oui.value,
};
}
}
// Flattening finalOuis into an array so we can create the arrays necessary for the chart
const finalOuisArr = Object.entries(finalOuis);
const finalOuiDs = [];
const finalOuiLabels = [];
const finalOuiColors = [];
for (const oui of finalOuisArr) {
finalOuiDs.push(oui[1].value);
finalOuiLabels.push(oui[1].label);
finalOuiColors.push('#39f');
}
const totalOthers = finalOuiDs.slice(5).reduce((acc, oui) => acc + oui, 0);
parsedData.ouis = {
datasets: [
{
label: 'OUIs',
data: finalOuiDs.slice(0, 5).concat(totalOthers),
backgroundColor: finalOuiColors.concat('#39f'),
},
],
labels: finalOuiLabels.slice(0, 5).concat('Others'),
};
// Endpoints table
const endpointsDs = [];
const endpointsTotal = parsedData.endPoints.reduce((acc, point) => acc + point.value, 0);
for (const point of parsedData.endPoints) {
endpointsDs.push({
endpoint: point.tag,
devices: point.value,
percent: `${Math.round((point.value / endpointsTotal) * 100)}%`,
});
}
parsedData.endpoints = endpointsDs;
setData(parsedData);
};
const getDashboard = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.ucentralfms}/api/v1/deviceReport`, {
headers,
})
.then((response) => {
parseData(response.data);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
useEffect(() => {
getDashboard();
}, []);
return <Dashboard loading={loading} t={t} data={data} />;
};
export default FirmwareDashboard;

View File

@@ -1,226 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import axiosInstance from 'utils/axiosInstance';
import { FirmwareList, useAuth, useToast } from 'ucentral-libs';
const FirmwareListPage = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [page, setPage] = useState({ selected: 0 });
const [pageCount, setPageCount] = useState(0);
const [firmwarePerPage, setFirmwarePerPage] = useState('10');
const [selectedDeviceType, setSelectedDeviceType] = useState('');
const [deviceTypes, setDeviceTypes] = useState([]);
const [firmware, setFirmware] = useState([]);
const [filteredFirmware, setFilteredFirmware] = useState([]);
const [displayedFirmware, setDisplayedFirmware] = useState([]);
const [displayDev, setDisplayDev] = useState(false);
const [addNoteLoading, setAddNoteLoading] = useState(false);
const [updateDescriptionLoading, setUpdateDescriptionLoading] = useState(false);
const [loading, setLoading] = useState(false);
const displayFirmware = (currentPage, perPage, firmwareToDisplay) => {
setLoading(true);
const startIndex = currentPage.selected * perPage;
const endIndex = parseInt(startIndex, 10) + parseInt(perPage, 10);
setDisplayedFirmware(firmwareToDisplay.slice(startIndex, endIndex));
setLoading(false);
};
const filterFirmware = (newFirmware, displayDevDevices) => {
let firmwareToDisplay = newFirmware;
if (!displayDevDevices) {
firmwareToDisplay = firmwareToDisplay.filter((i) => !i.revision.includes('devel'));
}
const count = Math.ceil(firmwareToDisplay.length / firmwarePerPage);
setPageCount(count);
setPage({ selected: 0 });
setFilteredFirmware(firmwareToDisplay);
displayFirmware({ selected: 0 }, firmwarePerPage, firmwareToDisplay);
};
const toggleDevDisplay = () => {
setDisplayDev(!displayDev);
filterFirmware(firmware, !displayDev);
};
const getFirmware = (deviceType) => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(
`${endpoints.ucentralfms}/api/v1/firmwares?deviceType=${deviceType ?? selectedDeviceType}`,
{
headers,
},
)
.then((response) => {
const sortedFirmware = response.data.firmwares.sort((a, b) => {
const firstDate = a.imageDate;
const secondDate = b.imageDate;
if (firstDate < secondDate) return 1;
return firstDate > secondDate ? -1 : 0;
});
setFirmware(sortedFirmware);
filterFirmware(sortedFirmware, displayDev);
})
.catch(() => {
setLoading(false);
addToast({
title: t('common.error'),
body: t('common.general_error'),
color: 'danger',
autohide: true,
});
});
};
const getDeviceTypes = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.ucentralfms}/api/v1/firmwares?deviceSet=true`, {
headers,
})
.then((response) => {
const newDeviceTypes = response.data.deviceTypes;
setDeviceTypes(newDeviceTypes);
setSelectedDeviceType(newDeviceTypes[0]);
getFirmware(newDeviceTypes[0]);
})
.catch(() => {
setLoading(false);
addToast({
title: t('common.error'),
body: t('common.general_error'),
color: 'danger',
autohide: true,
});
});
};
const updateFirmwarePerPage = (value) => {
const count = Math.ceil(filteredFirmware.length / value);
setPageCount(count);
setPage({ selected: 0 });
setFirmwarePerPage(value);
displayFirmware({ selected: 0 }, value, filteredFirmware);
};
const updatePage = (value) => {
setPage(value);
displayFirmware(value, firmwarePerPage, filteredFirmware);
};
const updateSelectedType = (value) => {
setSelectedDeviceType(value);
getFirmware(value);
};
const addNote = (value, id) => {
setAddNoteLoading(true);
const parameters = {
id,
notes: [{ note: value }],
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralfms}/api/v1/firmware/${id}`, parameters, options)
.then(() => {
getFirmware();
setAddNoteLoading(false);
})
.catch(() => {
setAddNoteLoading(false);
addToast({
title: t('common.error'),
body: t('common.general_error'),
color: 'danger',
autohide: true,
});
});
};
const updateDescription = (value, id) => {
setUpdateDescriptionLoading(true);
const parameters = {
id,
description: value,
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralfms}/api/v1/firmware/${id}`, parameters, options)
.then(() => {
getFirmware();
setUpdateDescriptionLoading(false);
})
.catch(() => {
setUpdateDescriptionLoading(false);
addToast({
title: t('common.error'),
body: t('common.general_error'),
color: 'danger',
autohide: true,
});
});
};
useEffect(() => {
if (selectedDeviceType === '' && !loading) getDeviceTypes();
}, []);
return (
<FirmwareList
t={t}
loading={loading}
page={page}
pageCount={pageCount}
setPage={updatePage}
data={displayedFirmware}
firmwarePerPage={firmwarePerPage}
setFirmwarePerPage={updateFirmwarePerPage}
selectedDeviceType={selectedDeviceType}
deviceTypes={deviceTypes}
setSelectedDeviceType={updateSelectedType}
addNote={addNote}
addNoteLoading={addNoteLoading}
updateDescription={updateDescription}
updateDescriptionLoading={updateDescriptionLoading}
displayDev={displayDev}
toggleDevDisplay={toggleDevDisplay}
/>
);
};
export default FirmwareListPage;

View File

@@ -1,141 +1,46 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import * as axios from 'axios';
import { LoginPage, useFormFields, useAuth } from 'ucentral-libs';
const initialFormState = {
username: {
value: '',
error: false,
placeholder: 'login.username',
},
password: {
value: '',
error: false,
placeholder: 'login.password',
},
ucentralsecurl: {
value: '',
error: false,
hidden: true,
placeholder: 'login.url',
},
forgotusername: {
value: '',
error: false,
placeholder: 'login.username',
},
newpassword: {
value: '',
error: false,
placeholder: 'login.new_password',
},
confirmpassword: {
value: '',
error: false,
placeholder: 'login.confirm_new_password',
},
};
const initialResponseState = {
text: '',
error: false,
tried: false,
};
import {
CButton,
CCard,
CCardBody,
CCardGroup,
CCol,
CContainer,
CForm,
CInput,
CInputGroup,
CInputGroupPrepend,
CInputGroupText,
CRow,
CPopover,
CAlert,
CInvalidFeedback,
} from '@coreui/react';
import CIcon from '@coreui/icons-react';
import { useAuth } from 'contexts/AuthProvider';
import { cilUser, cilLockLocked, cilLink } from '@coreui/icons';
import axiosInstance from 'utils/axiosInstance';
import { LanguageSwitcher } from 'ucentral-libs';
import styles from './index.module.scss';
const Login = () => {
const { t, i18n } = useTranslation();
const { setCurrentToken, setEndpoints } = useAuth();
const [userId, setUsername] = useState('');
const [password, setPassword] = useState('');
const [uCentralSecUrl, setUCentralSecUrl] = useState('');
const [hadError, setHadError] = useState(false);
const [emptyUsername, setEmptyUsername] = useState(false);
const [emptyPassword, setEmptyPassword] = useState(false);
const [emptyGateway, setEmptyGateway] = useState(false);
const [defaultConfig, setDefaultConfig] = useState({
value: '',
error: false,
hidden: true,
placeholder: 'login.url',
DEFAULT_UCENTRALSEC_URL: '',
ALLOW_UCENTRALSEC_CHANGE: true,
});
const [loading, setLoading] = useState(false);
const [loginResponse, setLoginResponse] = useState(initialResponseState);
const [forgotResponse, setForgotResponse] = useState(initialResponseState);
const [changePasswordResponse, setChangeResponse] = useState(initialResponseState);
const [policies, setPolicies] = useState({
passwordPolicy: '',
passwordPattern: '',
accessPolicy: '',
});
const [isLogin, setIsLogin] = useState(true);
const [isPasswordChange, setIsChangePassword] = useState(false);
const [fields, updateFieldWithId, updateField, setFormFields] = useFormFields(initialFormState);
const axiosInstance = axios.create();
axiosInstance.defaults.timeout = 5000;
const toggleForgotPassword = () => {
setFormFields({
...initialFormState,
...{
ucentralsecurl: defaultConfig,
},
});
setLoginResponse(initialResponseState);
setForgotResponse(initialResponseState);
setIsLogin(!isLogin);
};
const cancelPasswordChange = () => {
setFormFields({
...initialFormState,
...{
ucentralsecurl: defaultConfig,
},
});
setLoginResponse(initialResponseState);
setForgotResponse(initialResponseState);
setIsLogin(true);
setIsChangePassword(false);
};
const signInValidation = () => {
let valid = true;
if (fields.ucentralsecurl.value === '') {
updateField('ucentralsecurl', { error: true });
valid = false;
}
if (fields.password.value === '') {
updateField('password', { error: true });
valid = false;
}
if (fields.username.value === '') {
updateField('username', { error: true });
valid = false;
}
if (isPasswordChange && fields.newpassword.value !== fields.confirmpassword.value) {
updateField('confirmpassword', { error: true });
valid = false;
}
return valid;
};
const forgotValidation = () => {
let valid = true;
if (fields.ucentralsecurl.value === '') {
updateField('ucentralsecurl', { error: true });
valid = false;
}
if (fields.forgotusername.value === '') {
updateField('forgotusername', { error: true });
valid = false;
}
return valid;
};
const onKeyDown = (event, action) => {
if (event.code === 'Enter') {
action();
}
};
const placeholderUrl = 'Gateway URL (ex: https://your-url:port)';
const getDefaultConfig = async () => {
let uCentralSecUrl = '';
fetch('./config.json', {
headers: {
'Content-Type': 'application/json',
@@ -144,59 +49,45 @@ const Login = () => {
})
.then((response) => response.json())
.then((json) => {
const newUcentralSecConfig = {
value: json.DEFAULT_UCENTRALSEC_URL,
error: false,
hidden: !json.ALLOW_UCENTRALSEC_CHANGE,
placeholder: json.DEFAULT_UCENTRALSEC_URL,
};
uCentralSecUrl = newUcentralSecConfig.value;
setDefaultConfig(newUcentralSecConfig);
setFormFields({
...fields,
...{
ucentralsecurl: newUcentralSecConfig,
},
});
return axiosInstance.post(
`${newUcentralSecConfig.value}/api/v1/oauth2?requirements=true`,
{},
);
})
.then((response) => {
const newPolicies = response.data;
newPolicies.accessPolicy = `${uCentralSecUrl}${newPolicies.accessPolicy}`;
newPolicies.passwordPolicy = `${uCentralSecUrl}${newPolicies.passwordPolicy}`;
setPolicies(newPolicies);
setDefaultConfig(json);
})
.catch();
};
const SignIn = () => {
setLoginResponse(initialResponseState);
if (signInValidation()) {
setLoading(true);
let token = '';
const formValidation = () => {
setHadError(false);
const parameters = {
userId: fields.username.value,
password: fields.password.value,
let isSuccessful = true;
if (userId.trim() === '') {
setEmptyUsername(true);
isSuccessful = false;
}
if (password.trim() === '') {
setEmptyPassword(true);
isSuccessful = false;
}
if (uCentralSecUrl.trim() === '') {
setEmptyGateway(true);
isSuccessful = false;
}
return isSuccessful;
};
if (isPasswordChange) {
parameters.newPassword = fields.newpassword.value;
}
const SignIn = (credentials) => {
let token = '';
const finalUCentralSecUrl = defaultConfig.ALLOW_UCENTRALSEC_CHANGE
? uCentralSecUrl
: defaultConfig.DEFAULT_UCENTRALSEC_URL;
axiosInstance
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2`, parameters)
.post(`${finalUCentralSecUrl}/api/v1/oauth2`, credentials)
.then((response) => {
if (response.data.userMustChangePassword) {
setIsChangePassword(true);
return null;
}
sessionStorage.setItem('access_token', response.data.access_token);
token = response.data.access_token;
return axiosInstance.get(`${fields.ucentralsecurl.value}/api/v1/systemEndpoints`, {
return axiosInstance.get(`${finalUCentralSecUrl}/api/v1/systemEndpoints`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${response.data.access_token}`,
@@ -204,9 +95,8 @@ const Login = () => {
});
})
.then((response) => {
if (response) {
const endpoints = {
ucentralsec: fields.ucentralsecurl.value,
ucentralsec: finalUCentralSecUrl,
};
for (const endpoint of response.data.endpoints) {
endpoints[endpoint.type] = endpoint.uri;
@@ -214,95 +104,143 @@ const Login = () => {
sessionStorage.setItem('gateway_endpoints', JSON.stringify(endpoints));
setEndpoints(endpoints);
setCurrentToken(token);
}
})
.catch((error) => {
if (!isPasswordChange) {
if (
error.response.status === 403 &&
error.response?.data?.ErrorDescription === 'Password change expected.'
) {
setIsChangePassword(true);
}
setLoginResponse({
text: t('login.login_error'),
error: true,
tried: true,
});
} else {
setChangeResponse({
text:
fields.newpassword.value === fields.password.value
? t('login.previously_used')
: t('login.change_password_error'),
error: true,
tried: true,
});
}
})
.finally(() => {
setLoading(false);
});
}
};
const sendForgotPasswordEmail = () => {
setForgotResponse(initialResponseState);
if (forgotValidation()) {
setLoading(true);
axiosInstance
.post(`${fields.ucentralsecurl.value}/api/v1/oauth2?forgotPassword=true`, {
userId: fields.forgotusername.value,
})
.then(() => {
updateField('forgotusername', {
value: '',
});
setForgotResponse({
text: t('login.forgot_password_success'),
error: false,
tried: true,
});
})
.catch(() => {
setForgotResponse({
text: t('login.forgot_password_error'),
error: true,
tried: true,
});
})
.finally(() => {
setLoading(false);
setHadError(true);
});
};
const onKeyDown = (event) => {
if (event.code === 'Enter' && formValidation()) {
SignIn({ userId, password });
}
};
useEffect(() => {
if (emptyUsername) setEmptyUsername(false);
}, [userId]);
useEffect(() => {
if (emptyPassword) setEmptyPassword(false);
}, [password]);
useEffect(() => {
if (emptyGateway) setEmptyGateway(false);
}, [uCentralSecUrl]);
useEffect(() => {
getDefaultConfig();
}, []);
useEffect(() => {
setUCentralSecUrl(defaultConfig.DEFAULT_UCENTRALSEC_URL);
}, [defaultConfig]);
return (
<LoginPage
t={t}
i18n={i18n}
logo="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
signIn={SignIn}
loading={loading}
loginResponse={loginResponse}
forgotResponse={forgotResponse}
fields={fields}
updateField={updateFieldWithId}
toggleForgotPassword={toggleForgotPassword}
isLogin={isLogin}
isPasswordChange={isPasswordChange}
onKeyDown={onKeyDown}
sendForgotPasswordEmail={sendForgotPasswordEmail}
changePasswordResponse={changePasswordResponse}
cancelPasswordChange={cancelPasswordChange}
policies={policies}
<div className="c-app c-default-layout flex-row align-items-center">
<CContainer>
<CRow className="justify-content-center">
<CCol md="8">
<img
className={[styles.logo, 'c-sidebar-brand-full'].join(' ')}
src="assets/OpenWiFi_LogoLockup_DarkGreyColour.svg"
alt="OpenWifi"
/>
<CCardGroup>
<CCard className="p-4">
<CCardBody>
<CForm onKeyDown={onKeyDown}>
<h1>{t('login.login')}</h1>
<p className="text-muted">{t('login.sign_in_to_account')}</p>
<CInputGroup className="mb-3">
<CPopover content={t('login.username')}>
<CInputGroupPrepend>
<CInputGroupText>
<CIcon name="cilUser" content={cilUser} />
</CInputGroupText>
</CInputGroupPrepend>
</CPopover>
<CInput
invalid={emptyUsername}
autoFocus
required
type="text"
placeholder={t('login.username')}
autoComplete="username"
onChange={(event) => setUsername(event.target.value)}
/>
<CInvalidFeedback className="help-block">
{t('login.please_enter_username')}
</CInvalidFeedback>
</CInputGroup>
<CInputGroup className="mb-4">
<CPopover content={t('login.password')}>
<CInputGroupPrepend>
<CInputGroupText>
<CIcon content={cilLockLocked} />
</CInputGroupText>
</CInputGroupPrepend>
</CPopover>
<CInput
invalid={emptyPassword}
required
type="password"
placeholder={t('login.password')}
autoComplete="current-password"
onChange={(event) => setPassword(event.target.value)}
/>
<CInvalidFeedback className="help-block">
{t('login.please_enter_password')}
</CInvalidFeedback>
</CInputGroup>
<CInputGroup className="mb-4" hidden={!defaultConfig.ALLOW_UCENTRALSEC_CHANGE}>
<CPopover content={t('login.url')}>
<CInputGroupPrepend>
<CInputGroupText>
<CIcon name="cilLink" content={cilLink} />
</CInputGroupText>
</CInputGroupPrepend>
</CPopover>
<CInput
invalid={emptyGateway}
type="text"
required
placeholder={placeholderUrl}
value={uCentralSecUrl}
autoComplete="gateway-url"
onChange={(event) => setUCentralSecUrl(event.target.value)}
/>
<CInvalidFeedback className="help-block">
{t('login.please_enter_gateway')}
</CInvalidFeedback>
</CInputGroup>
<CRow>
<CCol>
<CAlert show={hadError} color="danger">
{t('login.login_error')}
</CAlert>
</CCol>
</CRow>
<CRow>
<CCol xs="6">
<CButton
color="primary"
className="px-4"
onClick={() => (formValidation() ? SignIn({ userId, password }) : null)}
>
{t('login.login')}
</CButton>
</CCol>
<CCol xs="6">
<div className={styles.languageSwitcher}>
<LanguageSwitcher i18n={i18n} />
</div>
</CCol>
</CRow>
</CForm>
</CCardBody>
</CCard>
</CCardGroup>
</CCol>
</CRow>
</CContainer>
</div>
);
};

View File

@@ -1,283 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CCard, CCardBody } from '@coreui/react';
import axiosInstance from 'utils/axiosInstance';
import { testRegex } from 'utils/helper';
import { useUser, EditMyProfile, useAuth, useToast } from 'ucentral-libs';
const initialState = {
Id: {
value: '',
error: false,
editable: false,
},
currentPassword: {
value: '',
error: false,
editable: true,
},
email: {
value: '',
error: false,
editable: false,
},
description: {
value: '',
error: false,
editable: true,
},
name: {
value: '',
error: false,
editable: true,
},
userRole: {
value: '',
error: false,
editable: true,
},
notes: {
value: [],
editable: false,
},
};
const ProfilePage = () => {
const { t } = useTranslation();
const { currentToken, endpoints, user, getAvatar, avatar } = useAuth();
const { addToast } = useToast();
const [loading, setLoading] = useState(false);
const [initialUser, setInitialUser] = useState({});
const [userForm, updateWithId, updateWithKey, setUser] = useUser(initialState);
const [newAvatar, setNewAvatar] = useState('');
const [newAvatarFile, setNewAvatarFile] = useState(null);
const [fileInputKey, setFileInputKey] = useState(0);
const [policies, setPolicies] = useState({
passwordPolicy: '',
passwordPattern: '',
accessPolicy: '',
});
const getPasswordPolicy = () => {
axiosInstance
.post(`${endpoints.ucentralsec}/api/v1/oauth2?requirements=true`, {})
.then((response) => {
const newPolicies = response.data;
newPolicies.accessPolicy = `${endpoints.ucentralsec}${newPolicies.accessPolicy}`;
newPolicies.passwordPolicy = `${endpoints.ucentralsec}${newPolicies.passwordPolicy}`;
setPolicies(response.data);
})
.catch(() => {});
};
const getUser = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(`${endpoints.ucentralsec}/api/v1/user/${user.Id}`, options)
.then((response) => {
const newUser = {};
for (const key of Object.keys(response.data)) {
if (key in initialState && key !== 'currentPassword') {
newUser[key] = {
...initialState[key],
value: response.data[key],
};
}
}
setInitialUser({ ...initialState, ...newUser });
setUser({ ...initialState, ...newUser });
})
.catch(() => {});
};
const uploadAvatar = () => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const data = new FormData();
data.append('file', newAvatarFile);
axiosInstance
.post(`${endpoints.ucentralsec}/api/v1/avatar/${user.Id}`, data, options)
.then(() => {
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
getAvatar();
setNewAvatar('');
setNewAvatarFile(null);
setFileInputKey(fileInputKey + 1);
})
.catch(() => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
color: 'danger',
autohide: true,
});
});
};
const updateUser = () => {
setLoading(true);
const parameters = {
id: user.Id,
};
let newData = true;
for (const key of Object.keys(userForm)) {
if (userForm[key].editable && userForm[key].value !== initialUser[key].value) {
if (
key === 'currentPassword' &&
!testRegex(userForm[key].value, policies.passwordPattern)
) {
updateWithKey('currentPassword', {
error: true,
});
newData = false;
break;
} else {
parameters[key] = userForm[key].value;
}
}
}
if (newAvatarFile !== null) {
uploadAvatar();
}
if (newData) {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralsec}/api/v1/user/${user.Id}`, parameters, options)
.then(() => {
addToast({
title: t('user.update_success_title'),
body: t('user.update_success'),
color: 'success',
autohide: true,
});
})
.catch(() => {
addToast({
title: t('user.update_failure_title'),
body: t('user.update_failure'),
color: 'danger',
autohide: true,
});
})
.finally(() => {
getUser();
setLoading(false);
});
} else {
setLoading(false);
}
};
const addNote = (currentNote) => {
setLoading(true);
const parameters = {
id: user.Id,
notes: [{ note: currentNote }],
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.put(`${endpoints.ucentralsec}/api/v1/user/${user.Id}`, parameters, options)
.then(() => {
getUser();
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
const showPreview = (e) => {
const imageFile = e.target.files[0];
setNewAvatar(URL.createObjectURL(imageFile));
setNewAvatarFile(imageFile);
};
const deleteAvatar = () => {
setLoading(true);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
return axiosInstance
.delete(`${endpoints.ucentralsec}/api/v1/avatar/${user.Id}`, options)
.then(() => {
getAvatar();
})
.catch(() => {})
.finally(() => {
setLoading(false);
});
};
useEffect(() => {
if (user.Id) {
getAvatar();
getUser();
}
if (policies.passwordPattern.length === 0) {
getPasswordPolicy();
}
}, [user.Id]);
return (
<CCard>
<CCardBody>
<EditMyProfile
t={t}
user={userForm}
updateUserWithId={updateWithId}
saveUser={updateUser}
loading={loading}
policies={policies}
addNote={addNote}
avatar={avatar}
newAvatar={newAvatar}
showPreview={showPreview}
deleteAvatar={deleteAvatar}
fileInputKey={fileInputKey}
/>
</CCardBody>
</CCard>
);
};
export default ProfilePage;

View File

@@ -1,99 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ApiStatusCard, useAuth, useToast } from 'ucentral-libs';
import { v4 as createUuid } from 'uuid';
import axiosInstance from 'utils/axiosInstance';
import { CRow, CCol } from '@coreui/react';
import { prettyDate, secondsToDetailed } from 'utils/helper';
const SystemPage = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [endpointsInfo, setEndpointsInfo] = useState([]);
const getSystemInfo = async (key, endpoint) => {
const systemInfo = {
title: key,
endpoint,
uptime: t('common.unknown'),
version: t('common.unknown'),
start: t('common.unknown'),
};
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
const getUptime = axiosInstance.get(`${endpoint}/api/v1/system?command=times`, options);
const getVersion = axiosInstance.get(`${endpoint}/api/v1/system?command=version`, options);
return Promise.all([getUptime, getVersion])
.then(([newUptime, newVersion]) => {
const uptimeObj = newUptime.data.times.find((obj) => obj.tag === 'uptime');
const startObj = newUptime.data.times.find((obj) => obj.tag === 'start');
return {
title: key,
endpoint,
uptime: uptimeObj?.value
? secondsToDetailed(
uptimeObj.value,
t('common.day'),
t('common.days'),
t('common.hour'),
t('common.hours'),
t('common.minute'),
t('common.minutes'),
t('common.second'),
t('common.seconds'),
)
: t('common.unknown'),
version: newVersion.data.value,
start: prettyDate(startObj.value),
};
})
.catch(() => {
throw new Error('Error while fetching');
})
.finally(() => systemInfo);
};
const getAllInfo = async () => {
const promises = [];
for (const [key, value] of Object.entries(endpoints)) {
promises.push(getSystemInfo(key, value));
}
try {
const results = await Promise.all(promises);
setEndpointsInfo(results);
} catch {
addToast({
title: t('common.error'),
body: t('system.error_fetching'),
color: 'danger',
autohide: true,
});
}
};
useEffect(() => {
getAllInfo();
}, []);
return (
<CRow>
{endpointsInfo.map((info) => (
<CCol key={createUuid()} md="4">
<ApiStatusCard t={t} info={info} />
</CCol>
))}
</CRow>
);
};
export default SystemPage;

View File

@@ -1,207 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { UserListTable, useAuth, useToast } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import { getItem, setItem } from 'utils/localStorageHelper';
import CreateUserModal from 'components/CreateUserModal';
import EditUserModal from 'components/EditUserModal';
const UserListPage = () => {
const { t } = useTranslation();
const { currentToken, endpoints } = useAuth();
const { addToast } = useToast();
const [page, setPage] = useState({ selected: 0 });
const [users, setUsers] = useState([]);
const [usersToDisplay, setUsersToDisplay] = useState([]);
const [userToEdit, setUserToEdit] = useState('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [pageCount, setPageCount] = useState(0);
const [loading, setLoading] = useState(true);
const [deleteLoading, setDeleteLoading] = useState(false);
const [usersPerPage, setUsersPerPage] = useState(getItem('devicesPerPage') || '10');
const toggleCreateModal = () => {
setShowCreateModal(!showCreateModal);
};
const toggleEditModal = (userId) => {
if (userId) setUserToEdit(userId);
setShowEditModal(!showEditModal);
};
const getUsers = () => {
setLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.get(`${endpoints.ucentralsec}/api/v1/users?idOnly=true`, {
headers,
})
.then((response) => {
setUsers(response.data.users);
})
.catch(() => {
setLoading(false);
});
};
const getAvatarPromises = (userIds) => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
responseType: 'arraybuffer',
};
const promises = userIds.map(async (id) =>
axiosInstance.get(
`${endpoints.ucentralsec}/api/v1/avatar/${id}?timestamp=${new Date().toString()}`,
options,
),
);
return promises;
};
const displayUsers = async () => {
setLoading(true);
const startIndex = page.selected * usersPerPage;
const endIndex = parseInt(startIndex, 10) + parseInt(usersPerPage, 10);
const idsToGet = users
.slice(startIndex, endIndex)
.map((x) => encodeURIComponent(x))
.join(',');
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
const avatarRequests = getAvatarPromises(users.slice(startIndex, endIndex));
const avatars = await Promise.all(avatarRequests).then((results) =>
results.map((response) => {
const base64 = btoa(
new Uint8Array(response.data).reduce(
(data, byte) => data + String.fromCharCode(byte),
'',
),
);
return `data:;base64,${base64}`;
}),
);
axiosInstance
.get(`${endpoints.ucentralsec}/api/v1/users?select=${idsToGet}`, {
headers,
})
.then((response) => {
const newUsers = response.data.users.map((user, index) => {
const newUser = {
...user,
avatar: avatars[index],
};
return newUser;
});
setUsersToDisplay(newUsers);
setLoading(false);
})
.catch(() => {
setLoading(false);
});
};
const deleteUser = (userId) => {
setDeleteLoading(true);
const headers = {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
};
axiosInstance
.delete(`${endpoints.ucentralsec}/api/v1/user/${userId}`, {
headers,
})
.then(() => {
addToast({
title: t('common.success'),
body: t('user.delete_success'),
color: 'success',
autohide: true,
});
getUsers();
})
.catch(() => {
addToast({
title: t('common.error'),
body: t('user.delete_failure'),
color: 'danger',
autohide: true,
});
})
.finally(() => {
setDeleteLoading(false);
});
};
const updateUsersPerPage = (value) => {
setItem('usersPerPage', value);
setUsersPerPage(value);
};
useEffect(() => {
if (users.length > 0) {
displayUsers();
} else {
setUsersToDisplay([]);
setLoading(false);
}
}, [users, usersPerPage, page]);
useEffect(() => {
getUsers();
}, []);
useEffect(() => {
if (users !== []) {
const count = Math.ceil(users.length / usersPerPage);
setPageCount(count);
}
}, [usersPerPage, users]);
return (
<div>
<UserListTable
t={t}
users={usersToDisplay}
loading={loading}
usersPerPage={usersPerPage}
setUsersPerPage={updateUsersPerPage}
pageCount={pageCount}
setPage={setPage}
deleteUser={deleteUser}
deleteLoading={deleteLoading}
toggleCreate={toggleCreateModal}
toggleEdit={toggleEditModal}
refreshUsers={getUsers}
/>
<CreateUserModal show={showCreateModal} toggle={toggleCreateModal} getUsers={getUsers} />
<EditUserModal
show={showEditModal}
toggle={toggleEditModal}
userId={userToEdit}
getUsers={getUsers}
/>
</div>
);
};
export default UserListPage;

View File

@@ -1,258 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { WifiAnalysisTable, RadioAnalysisTable, useAuth } from 'ucentral-libs';
import axiosInstance from 'utils/axiosInstance';
import NetworkDiagram from 'components/NetworkDiagram';
import { cleanBytesString, prettyDate, compactSecondsToDetailed } from 'utils/helper';
import {
CButton,
CCard,
CCardBody,
CCardHeader,
CCol,
CModal,
CModalHeader,
CModalBody,
CRow,
} from '@coreui/react';
const WifiAnalysisPage = () => {
const { t } = useTranslation();
const { deviceId } = useParams();
const { currentToken, endpoints } = useAuth();
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [tableTime, setTableTime] = useState('');
const [parsedAssociationStats, setParsedAssociationStats] = useState([]);
const [selectedAssociationStats, setSelectedAssociationStats] = useState(null);
const [parsedRadioStats, setParsedRadioStats] = useState([]);
const [selectedRadioStats, setSelectedRadioStats] = useState(null);
const [range, setRange] = useState(19);
const toggleModal = () => {
setShowModal(!showModal);
};
const secondsToLabel = (seconds) =>
compactSecondsToDetailed(seconds, t('common.day'), t('common.days'), t('common.seconds'));
const extractIp = (json, bssid) => {
const ips = {
ipV4: [],
ipV6: [],
};
for (const obj of json.interfaces) {
if ('clients' in obj) {
for (const client of obj.clients) {
if (client.mac === bssid) {
ips.ipV4 = ips.ipV4.concat(client.ipv4_addresses ?? []);
ips.ipV6 = ips.ipV6.concat(client.ipv6_addresses ?? []);
}
}
}
}
return ips;
};
const parseAssociationStats = (json) => {
const dbmNumber = 4294967295;
const newParsedAssociationStats = [];
const newParsedRadioStats = [];
for (const stat of json.data) {
const associations = [];
const radios = [];
const timeStamp = prettyDate(stat.recorded);
if (stat.data.radios !== undefined) {
for (let i = 0; i < stat.data.radios.length; i += 1) {
const radio = stat.data.radios[i];
radios.push({
timeStamp,
radio: i,
channel: radio.channel,
channelWidth: radio.channel_width,
noise: radio.noise ? (dbmNumber - radio.noise) * -1 : '-',
txPower: radio.tx_power,
activeMs: secondsToLabel(radio?.active_ms ? Math.floor(radio.active_ms / 1000) : 0),
busyMs: secondsToLabel(radio?.busy_ms ? Math.floor(radio.busy_ms / 1000) : 0),
receiveMs: secondsToLabel(radio?.receive_ms ? Math.floor(radio.receive_ms / 1000) : 0),
});
}
newParsedRadioStats.push(radios);
}
// Looping through the interfaces
for (const deviceInterface of stat.data.interfaces) {
if ('counters' in deviceInterface && 'ssids' in deviceInterface) {
for (const ssid of deviceInterface.ssids) {
// Information common between all associations
const radioInfo = {
found: false,
};
if (ssid.phy !== undefined) {
radioInfo.radio = stat.data.radios.findIndex((element) => element.phy === ssid.phy);
radioInfo.found = radioInfo.radio !== undefined;
radioInfo.radioIndex = radioInfo.radio;
}
if (!radioInfo.found && ssid.radio !== undefined) {
const radioArray = ssid.radio.$ref.split('/');
const radioIndex = radioArray !== undefined ? radioArray[radioArray.length - 1] : '-';
radioInfo.found = stat.data.radios[radioIndex] !== undefined;
radioInfo.radio = radioIndex;
radioInfo.radioIndex = radioIndex;
}
if (!radioInfo.found) {
radioInfo.radio = '-';
}
if ('associations' in ssid) {
for (const association of ssid.associations) {
const data = {
radio: radioInfo,
...extractIp(stat.data, association.bssid),
bssid: association.bssid,
ssid: ssid.ssid,
rssi: association.rssi ? (dbmNumber - association.rssi) * -1 : '-',
mode: ssid.mode,
rxBytes: cleanBytesString(association.rx_bytes, 0),
rxRate: association.rx_rate.bitrate,
rxMcs: association.rx_rate.mcs ?? '-',
rxNss: association.rx_rate.nss ?? '-',
txBytes: cleanBytesString(association.tx_bytes, 0),
txMcs: association.tx_rate.mcs,
txNss: association.tx_rate.nss,
txRate: association.tx_rate.bitrate,
timeStamp,
};
associations.push(data);
}
}
}
}
}
newParsedAssociationStats.push(associations);
}
// Radio Stats
const ascOrderedRadioStats = newParsedRadioStats.reverse();
setParsedRadioStats(ascOrderedRadioStats);
setSelectedRadioStats(ascOrderedRadioStats[ascOrderedRadioStats.length - 1]);
const ascOrderedAssociationStats = newParsedAssociationStats.reverse();
setParsedAssociationStats(ascOrderedAssociationStats);
setSelectedAssociationStats(ascOrderedAssociationStats[ascOrderedAssociationStats.length - 1]);
setRange(ascOrderedRadioStats.length > 0 ? ascOrderedRadioStats.length - 1 : 0);
setTableTime(
ascOrderedRadioStats.length > 0
? ascOrderedRadioStats[ascOrderedRadioStats.length - 1][0]?.timeStamp
: '',
);
setLoading(false);
};
const getLatestAssociationStats = () => {
setLoading(true);
setRange(19);
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${currentToken}`,
},
};
axiosInstance
.get(
`${endpoints.ucentralgw}/api/v1/device/${deviceId}/statistics?newest=true&limit=20`,
options,
)
.then((response) => {
parseAssociationStats(response.data);
})
.catch(() => {
setLoading(false);
});
};
const updateSelectedStats = (index) => {
setTableTime(parsedRadioStats[index][0].timeStamp);
setSelectedAssociationStats(parsedAssociationStats[index]);
setSelectedRadioStats(parsedRadioStats[index]);
};
useEffect(() => {
if (deviceId && deviceId.length > 0) {
getLatestAssociationStats();
}
}, [deviceId]);
return (
<div>
<CCard>
<CCardHeader>
<CRow>
<CCol>
<h5 className="mb-0">{t('common.device', { serialNumber: deviceId })}</h5>
</CCol>
<CCol className="text-right">
<CButton color="secondary" onClick={toggleModal}>
{t('wifi_analysis.network_diagram')}
</CButton>
</CCol>
</CRow>
</CCardHeader>
<CCardBody className="overflow-auto" style={{ height: 'calc(100vh - 300px)' }}>
<CRow className="mb-4">
<CCol className="text-center">
<input
type="range"
style={{ width: '80%' }}
className="form-range"
min="0"
max={range}
step="1"
onChange={(e) => updateSelectedStats(e.target.value)}
defaultValue={range}
disabled={!selectedRadioStats}
/>
<h5>
{t('common.timestamp')}: {tableTime}
</h5>
</CCol>
</CRow>
<h5 className="pb-3 text-center">{t('wifi_analysis.radios')}</h5>
<RadioAnalysisTable data={selectedRadioStats ?? []} loading={loading} range={range} />
<h5 className="pt-5 pb-3 text-center">{t('wifi_analysis.associations')}</h5>
<WifiAnalysisTable
t={t}
data={selectedAssociationStats ?? []}
loading={loading}
range={range}
/>
</CCardBody>
</CCard>
<CModal size="xl" show={showModal} onClose={toggleModal}>
<CModalHeader closeButton>{t('wifi_analysis.network_diagram')}</CModalHeader>
<CModalBody>
{showModal ? (
<NetworkDiagram
show={showModal}
radios={selectedRadioStats}
associations={selectedAssociationStats}
/>
) : (
<div />
)}
</CModalBody>
</CModal>
</div>
);
};
export default WifiAnalysisPage;

View File

@@ -1,4 +1,4 @@
import { useAuth } from 'ucentral-libs';
import { useAuth } from 'contexts/AuthProvider';
import { Route } from 'react-router-dom';
import React from 'react';

View File

@@ -1,37 +1,9 @@
import React from 'react';
const DevicePage = React.lazy(() => import('pages/DevicePage'));
const DeviceDashboard = React.lazy(() => import('pages/DeviceDashboard'));
const DeviceListPage = React.lazy(() => import('pages/DeviceListPage'));
const UserListPage = React.lazy(() => import('pages/UserListPage'));
const ProfilePage = React.lazy(() => import('pages/ProfilePage'));
const WifiAnalysisPage = React.lazy(() => import('pages/WifiAnalysisPage'));
const SystemPage = React.lazy(() => import('pages/SystemPage'));
const FirmwareListPage = React.lazy(() => import('pages/FirmwareListPage'));
const FirmwareDashboard = React.lazy(() => import('pages/FirmwareDashboard'));
export default [
{
path: '/devicedashboard',
exact: true,
name: 'common.device_dashboard',
component: DeviceDashboard,
},
{ path: '/devices', exact: true, name: 'common.devices', component: DeviceListPage },
{
path: '/devices/:deviceId/wifianalysis',
name: 'wifi_analysis.title',
component: WifiAnalysisPage,
},
{ path: '/devices/:deviceId', name: 'common.device_page', component: DevicePage },
{ path: '/firmware', name: 'firmware.title', component: FirmwareListPage },
{
path: '/firmwaredashboard',
exact: true,
name: 'common.firmware_dashboard',
component: FirmwareDashboard,
},
{ path: '/users', exact: true, name: 'user.users', component: UserListPage },
{ path: '/myprofile', exact: true, name: 'user.my_profile', component: ProfilePage },
{ path: '/system', exact: true, name: 'common.system', component: SystemPage },
];

View File

@@ -5,28 +5,5 @@ pre.ignore {
.custom-select {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
-webkit-appearance: none;
}
.avatar {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
border-radius: 50em;
transition: margin .15s;
width: 3rem;
height: 3rem;
font-size: .8rem;
}
.ignore-overflow {
overflow: unset !important;
}
.tooltipLeft { &::after { left: 25% !important; } }
.tooltipLeft { &::before { left: 25% !important; } }
.tooltipRight { &::after { left: 75% !important; } }
.tooltipRight { &::before { left: 75% !important; } }

View File

@@ -1,12 +1 @@
// Variable overrides
$navbar-brand-width: 125px !default;
$sidebar-width: 175px !default;
$grid-breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1800px
);

26
src/utils/authHelper.js Normal file
View File

@@ -0,0 +1,26 @@
import axiosInstance from './axiosInstance';
export const logout = (token, endpoint) => {
axiosInstance
.delete(`${endpoint}/api/v1/oauth2/${token}`, {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
})
.then(() => {})
.catch(() => {})
.finally(() => {
sessionStorage.clear();
window.location.replace('/');
});
};
export const getToken = () => {
const token = sessionStorage.getItem('access_token');
if (token === undefined || token === null) {
logout();
return null;
}
return token;
};

15
src/utils/deviceHelper.js Normal file
View File

@@ -0,0 +1,15 @@
import axiosInstance from 'utils/axiosInstance';
export default async (deviceId, token, endpoint) => {
const options = {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
};
return axiosInstance
.get(`${endpoint}/api/v1/device/${encodeURIComponent(deviceId)}/status`, options)
.then((response) => response.data.connected)
.catch(() => false);
};

Some files were not shown because too many files have changed in this diff Show More