mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-10-30 10:22:24 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9010737a | ||
|
|
b2d9a829e9 | ||
|
|
c6a85d76cc | ||
|
|
4a74bfebc4 | ||
|
|
44c29c7912 | ||
|
|
653cd758f4 | ||
|
|
e65f577202 | ||
|
|
3f9478de30 | ||
|
|
070a03c73e | ||
|
|
244692e766 | ||
|
|
a154fffcce | ||
|
|
ae0c529fca | ||
|
|
edcca87acf | ||
|
|
356188a350 | ||
|
|
cafb950aa7 | ||
|
|
549627a355 | ||
|
|
e6307648da | ||
|
|
fab4467bfd | ||
|
|
37666c5075 | ||
|
|
871efc88b5 | ||
|
|
db5611233b | ||
|
|
caa1fd4d9b | ||
|
|
be3f5548f4 | ||
|
|
a33740c372 | ||
|
|
130d71d5a0 | ||
|
|
bcd9c692e6 | ||
|
|
5947f3362d | ||
|
|
4bbfbb82bc | ||
|
|
6f7876d3e7 | ||
|
|
d4aff8067e | ||
|
|
eaca70d29b | ||
|
|
a1889c88d3 | ||
|
|
53b3926e29 | ||
|
|
745e76db79 | ||
|
|
82e153c277 | ||
|
|
b080b73b97 | ||
|
|
1c05d8df28 | ||
|
|
efc80a183b | ||
|
|
8a92912035 | ||
|
|
b870cf828a | ||
|
|
4cb4fe53a5 | ||
|
|
f70992e9a1 | ||
|
|
eb48d77636 | ||
|
|
df1686a2ae | ||
|
|
8781c78c15 | ||
|
|
ad5b0ce2a0 | ||
|
|
039e641046 | ||
|
|
f1f62efe6f | ||
|
|
b3053f32b2 | ||
|
|
09184b0402 | ||
|
|
98562fd967 | ||
|
|
65e9e64cb4 | ||
|
|
573ecbd58d | ||
|
|
a801fcca49 | ||
|
|
e9d16ee172 | ||
|
|
db4dfc93e8 | ||
|
|
cf17f03ae0 | ||
|
|
64f3ee797e | ||
|
|
e287705e88 | ||
|
|
9583b2bae0 | ||
|
|
2698993a6d | ||
|
|
a14b595e8c | ||
|
|
d7957b85ae | ||
|
|
227a51423d | ||
|
|
ea0e7340cc | ||
|
|
999680e94b | ||
|
|
566dbbb157 | ||
|
|
75d995d54e | ||
|
|
908faa491b | ||
|
|
7a254e343e | ||
|
|
016ac336b9 | ||
|
|
1cfd3a10ad | ||
|
|
1838029d22 | ||
|
|
7767043a5a | ||
|
|
b1cfa6db19 | ||
|
|
623d5a5546 | ||
|
|
8c676eb965 | ||
|
|
1e4ccce36c | ||
|
|
1808206e74 | ||
|
|
0fbc2b92aa |
@@ -3,3 +3,4 @@ build
|
|||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
.github
|
.github
|
||||||
|
/helm
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ fullnameOverride: ""
|
|||||||
images:
|
images:
|
||||||
owgwui:
|
owgwui:
|
||||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||||
tag: main
|
tag: v2.10.0
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|||||||
9302
package-lock.json
generated
9302
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
96
package.json
96
package.json
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "ucentral-client",
|
"name": "ucentral-client",
|
||||||
"version": "2.8.0(44)",
|
"version": "2.10.0(49)",
|
||||||
"description": "",
|
"description": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "index.tsx",
|
"main": "index.tsx",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"format": "prettier --write \"src/**/*.js\"",
|
"format": "prettier --write \"src/**/*x.{ts,tsx,js,jsx}\"",
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
"lint": "TIMING=1 eslint \"src/**/*.{ts,tsx,js,jsx}\" --fix",
|
||||||
"clean": "rm -rf node_modules && rm -rf build"
|
"clean": "rm -rf node_modules && rm -rf build"
|
||||||
@@ -15,78 +15,84 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.0.11",
|
"@chakra-ui/icons": "^2.0.18",
|
||||||
"@chakra-ui/react": "^2.3.6",
|
"@chakra-ui/react": "^2.3.6",
|
||||||
"@chakra-ui/theme-tools": "^2.0.12",
|
"@chakra-ui/theme-tools": "^2.0.12",
|
||||||
"@chakra-ui/utils": "^2.0.11",
|
"@chakra-ui/utils": "^2.0.14",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.6",
|
||||||
"@emotion/styled": "^11.10.4",
|
"@emotion/styled": "^11.10.6",
|
||||||
"@fontsource/inter": "^4.5.14",
|
"@fontsource/inter": "^4.5.15",
|
||||||
"@react-spring/web": "^9.5.5",
|
"@googlemaps/react-wrapper": "^1.1.35",
|
||||||
"axios": "^1.1.3",
|
"@googlemaps/typescript-guards": "^2.0.3",
|
||||||
|
"@hello-pangea/dnd": "^16.2.0",
|
||||||
|
"@phosphor-icons/react": "^2.0.8",
|
||||||
|
"@react-spring/web": "^9.7.2",
|
||||||
|
"@tanstack/react-query": "^4.29.3",
|
||||||
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
"@textea/json-viewer": "^2.16.2",
|
||||||
|
"axios": "^1.3.5",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"chakra-react-select": "^4.3.0",
|
"chakra-react-select": "^4.6.0",
|
||||||
|
"chart.js": "^3.9.1",
|
||||||
"dagre": "^0.8.5",
|
"dagre": "^0.8.5",
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"framer-motion": "^7.6.1",
|
"framer-motion": "^10.12.2",
|
||||||
"i18next": "^22.0.0",
|
"i18next": "^22.4.14",
|
||||||
"i18next-browser-languagedetector": "^6.1.8",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
"i18next-http-backend": "^1.4.4",
|
"i18next-http-backend": "^2.2.0",
|
||||||
"libphonenumber-js": "^1.10.14",
|
"libphonenumber-js": "^1.10.26",
|
||||||
"phosphor-react": "^1.4.1",
|
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
"react-app-polyfill": "^3.0.0",
|
||||||
"react-chartjs-2": "^4.3.1",
|
"react-chartjs-2": "^4.3.1",
|
||||||
"chart.js": "^3.9.1",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-country-flag": "^3.0.2",
|
|
||||||
"react-csv": "^2.2.2",
|
"react-csv": "^2.2.2",
|
||||||
"react-datepicker": "^4.8.0",
|
"react-datepicker": "^4.11.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"@textea/json-viewer": "^2.10.0",
|
"react-fast-compare": "^3.2.1",
|
||||||
"react-fast-compare": "^3.2.0",
|
"react-i18next": "^12.2.0",
|
||||||
"react-i18next": "^11.18.6",
|
|
||||||
"react-masonry-css": "^1.0.16",
|
"react-masonry-css": "^1.0.16",
|
||||||
"@tanstack/react-query": "^4.12.0",
|
"react-router-dom": "^6.10.0",
|
||||||
"react-router-dom": "^6.4.2",
|
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
"react-virtualized-auto-sizer": "^1.0.7",
|
"react-virtualized-auto-sizer": "^1.0.15",
|
||||||
"react-window": "^1.8.8",
|
"react-window": "^1.8.9",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^3.1.8",
|
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
"vite": "^4.2.1",
|
||||||
"yup": "^0.32.11",
|
"yup": "^0.32.11",
|
||||||
"zustand": "^4.1.2"
|
"zustand": "^4.3.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.11.2",
|
"@types/google.maps": "^3.52.5",
|
||||||
"@types/react": "^18.0.21",
|
"@types/node": "^18.15.11",
|
||||||
|
"@types/react": "^18.0.37",
|
||||||
"@types/react-csv": "^1.1.3",
|
"@types/react-csv": "^1.1.3",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-datepicker": "4.10.0",
|
||||||
"@types/react-table": "^7.7.12",
|
"@types/react-dom": "^18.0.11",
|
||||||
"@types/react-datepicker": "4.8.0",
|
"@types/react-table": "^7.7.14",
|
||||||
"@types/uuid": "^8.3.4",
|
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"eslint": "8.25.0",
|
"@types/uuid": "^9.0.1",
|
||||||
"vite-tsconfig-paths": "^3.5.1",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"lint-staged": "^13.0.3",
|
"eslint": "8.38.0",
|
||||||
"@vitejs/plugin-react": "^2.1.0",
|
|
||||||
"vite-plugin-pwa": "^0.13.1",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
|
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-no-inline-styles": "^1.0.5",
|
"eslint-plugin-no-inline-styles": "^1.0.5",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0"
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"lint-staged": "^13.2.1",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"vite-plugin-pwa": "^0.14.7",
|
||||||
|
"vite-tsconfig-paths": "^4.2.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
BIN
public/devices/cig_wf196.png
Normal file
BIN
public/devices/cig_wf196.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
public/devices/generic_ap.png
Normal file
BIN
public/devices/generic_ap.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -64,6 +64,8 @@
|
|||||||
"health": "Gesundheit",
|
"health": "Gesundheit",
|
||||||
"inactive": "Inaktiv",
|
"inactive": "Inaktiv",
|
||||||
"interval": "Intervall",
|
"interval": "Intervall",
|
||||||
|
"last_connected": "Zuletzt verbunden",
|
||||||
|
"last_connected_tooltip": "Das letzte Mal, wann dieses Gerät mit dem Controller verbunden war. Dies kann verwendet werden, um abzuschätzen, wann ein Gerät getrennt wurde",
|
||||||
"last_connection": "Letzte Verbindung",
|
"last_connection": "Letzte Verbindung",
|
||||||
"last_contact": "Letzter Kontakt",
|
"last_contact": "Letzter Kontakt",
|
||||||
"last_disconnection": "Letzte Trennung",
|
"last_disconnection": "Letzte Trennung",
|
||||||
@@ -79,8 +81,11 @@
|
|||||||
"live_view_help": "Hilfe zur Live-Ansicht",
|
"live_view_help": "Hilfe zur Live-Ansicht",
|
||||||
"memory": "Erinnerung",
|
"memory": "Erinnerung",
|
||||||
"memory_used": "Verwendeter Speicher",
|
"memory_used": "Verwendeter Speicher",
|
||||||
"missing_board": "Die Analytics-Überwachung an diesem Veranstaltungsort ist nicht mehr aktiv. Bitte starten Sie die Überwachung über das obere Menü neu",
|
"missing_board": "Analytics-Überwachung an diesem Ort ist nicht mehr aktiv. Klicken Sie hier, um die Überwachung neu zu starten",
|
||||||
"mode": "Modus",
|
"mode": "Modus",
|
||||||
|
"monitoring": "Überwachung",
|
||||||
|
"no_board": "Keine Überwachung",
|
||||||
|
"no_board_description": "Sie überwachen diesen Veranstaltungsort derzeit nicht, klicken Sie hier, um zu beginnen",
|
||||||
"noise": "Lärm",
|
"noise": "Lärm",
|
||||||
"packets": "Pakete",
|
"packets": "Pakete",
|
||||||
"radio": "RADIO",
|
"radio": "RADIO",
|
||||||
@@ -91,6 +96,8 @@
|
|||||||
"retries": "Wiederholungen",
|
"retries": "Wiederholungen",
|
||||||
"search_serials": "Zeitschriften suchen",
|
"search_serials": "Zeitschriften suchen",
|
||||||
"stop_monitoring": "Beenden Sie die Überwachung",
|
"stop_monitoring": "Beenden Sie die Überwachung",
|
||||||
|
"stop_monitoring_success": "Überwachungsort gestoppt!",
|
||||||
|
"stop_monitoring_warning": "Bist du sicher? Dadurch werden alle aufgezeichneten Überwachungsdaten für diesen Veranstaltungsort gelöscht",
|
||||||
"temperature": "Temperatur",
|
"temperature": "Temperatur",
|
||||||
"title": "ANALYTICS",
|
"title": "ANALYTICS",
|
||||||
"total_data": "Gesamtdaten",
|
"total_data": "Gesamtdaten",
|
||||||
@@ -175,6 +182,7 @@
|
|||||||
"other": "Befehle",
|
"other": "Befehle",
|
||||||
"override_dfs": "DFS überschreiben",
|
"override_dfs": "DFS überschreiben",
|
||||||
"reboot": "Starten Sie neu",
|
"reboot": "Starten Sie neu",
|
||||||
|
"reboot_description": "Möchten Sie dieses Gerät neu starten?",
|
||||||
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
|
"reboot_error": "Fehler beim Senden des Neustartbefehls: {{e}}",
|
||||||
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
|
"reboot_success": "Neustartbefehl erfolgreich gesendet!",
|
||||||
"revision": "Revision",
|
"revision": "Revision",
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
"error_download": "Fehler beim Downloadversuch: {{e}}",
|
"error_download": "Fehler beim Downloadversuch: {{e}}",
|
||||||
"errors": "Fehler",
|
"errors": "Fehler",
|
||||||
"exit_fullscreen": "Ausgang",
|
"exit_fullscreen": "Ausgang",
|
||||||
|
"export": "Export",
|
||||||
"finished": "Fertig",
|
"finished": "Fertig",
|
||||||
"fullscreen": "Vollbildschirm",
|
"fullscreen": "Vollbildschirm",
|
||||||
"general_error": "Fehler beim Verbinden mit dem Server. Bitte wenden Sie sich an Ihren Administrator.",
|
"general_error": "Fehler beim Verbinden mit dem Server. Bitte wenden Sie sich an Ihren Administrator.",
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
"error_pushes_one": "Fehler (könnte an einer fehlerhaften Konfiguration liegen): {{count}}",
|
"error_pushes_one": "Fehler (könnte an einer fehlerhaften Konfiguration liegen): {{count}}",
|
||||||
"error_pushes_other": "Fehler (können auf eine fehlerhafte Konfiguration zurückzuführen sein): {{count}}",
|
"error_pushes_other": "Fehler (können auf eine fehlerhafte Konfiguration zurückzuführen sein): {{count}}",
|
||||||
"expert_name": "Expertenmodus",
|
"expert_name": "Expertenmodus",
|
||||||
|
"expert_name_explanation": "Sie können den Expertenmodus verwenden, um Ihre Konfiguration direkt zu ändern, einschließlich des Hinzufügens von Werten, die derzeit nicht von der Benutzeroberfläche unterstützt werden. Bitte verwenden Sie dieses Format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||||
"explanation": "Erläuterung",
|
"explanation": "Erläuterung",
|
||||||
"firewall": "Firewall",
|
"firewall": "Firewall",
|
||||||
"firmware_upgrade": "Firmware-Aktualisierung",
|
"firmware_upgrade": "Firmware-Aktualisierung",
|
||||||
@@ -391,6 +401,7 @@
|
|||||||
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
|
"warning_pushes_one": "Warten auf Geräteverbindung: {{count}}",
|
||||||
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
|
"warning_pushes_other": "Warten auf Geräteverbindung: {{count}}",
|
||||||
"weight": "Gewicht",
|
"weight": "Gewicht",
|
||||||
|
"wifi_bands_max": "Es können nicht mehr als 8 SSIDs dieses WLAN-Band verwenden",
|
||||||
"wifi_frames": "WiFi-Frames"
|
"wifi_frames": "WiFi-Frames"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -515,6 +526,7 @@
|
|||||||
"ouis_explanation": "OUIs von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
"ouis_explanation": "OUIs von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||||
"outdated_one": "Firmware {{count}} Tag alt",
|
"outdated_one": "Firmware {{count}} Tag alt",
|
||||||
"outdated_other": "Firmware {{count}} Tage alt",
|
"outdated_other": "Firmware {{count}} Tage alt",
|
||||||
|
"outdated_unknown": "Firmware unbekannten Alters",
|
||||||
"release": "Veröffentlichung",
|
"release": "Veröffentlichung",
|
||||||
"show_dev_releases": "Entwicklerversionen",
|
"show_dev_releases": "Entwicklerversionen",
|
||||||
"status_explanation": "Verbindungsstatus von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
"status_explanation": "Verbindungsstatus von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||||
@@ -530,6 +542,16 @@
|
|||||||
"queue": {
|
"queue": {
|
||||||
"title": "Ereigniswarteschlange"
|
"title": "Ereigniswarteschlange"
|
||||||
},
|
},
|
||||||
|
"radius": {
|
||||||
|
"calling_station_id": "Stations",
|
||||||
|
"disconnect": "Trennen",
|
||||||
|
"disconnect_success": "Radius-Sitzung getrennt!",
|
||||||
|
"input_octets": "Eingang",
|
||||||
|
"output_octets": "Ausgabe",
|
||||||
|
"radius_clients": "Radius-Kunden",
|
||||||
|
"session_time": "Sitzungszeit",
|
||||||
|
"username": "Nutzername"
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"load": "Belastung (1 | 5 | 15 m.)",
|
"load": "Belastung (1 | 5 | 15 m.)",
|
||||||
"seconds_ago": " Vor {{s}} Sekunden",
|
"seconds_ago": " Vor {{s}} Sekunden",
|
||||||
@@ -600,6 +622,7 @@
|
|||||||
"certificate_expires_in": "Zertifikat läuft ab in",
|
"certificate_expires_in": "Zertifikat läuft ab in",
|
||||||
"certificate_expiry": "Zert. Läuft ab in",
|
"certificate_expiry": "Zert. Läuft ab in",
|
||||||
"connected": "In Verbindung gebracht",
|
"connected": "In Verbindung gebracht",
|
||||||
|
"crash_logs": "Absturzprotokolle",
|
||||||
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
"create_errors": "Fehler beim Versuch, Geräte zu erstellen",
|
||||||
"create_success": " Geräte erfolgreich erstellt",
|
"create_success": " Geräte erfolgreich erstellt",
|
||||||
"current_firmware": "Aktuelle Firmware",
|
"current_firmware": "Aktuelle Firmware",
|
||||||
@@ -613,6 +636,7 @@
|
|||||||
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
"import_device_warning": "Bitte stellen Sie sicher, dass am Anfang oder Ende von Werten keine zusätzlichen Leerzeichen stehen, es sei denn, es handelt sich um einen Teil des gewünschten Werts",
|
||||||
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Für den Massenimport von Geräten müssen Sie eine CSV-Datei mit den folgenden Spalten verwenden: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
"invalid_serial_number": "Ungültige Seriennummer (muss 12 HEX-Zeichen lang sein)",
|
||||||
|
"logs_one": "Log",
|
||||||
"new_devices": "Neue Geräte",
|
"new_devices": "Neue Geräte",
|
||||||
"no_model_image": "Kein Modellbild gefunden",
|
"no_model_image": "Kein Modellbild gefunden",
|
||||||
"not_connected": "Nicht verbunden",
|
"not_connected": "Nicht verbunden",
|
||||||
@@ -620,7 +644,10 @@
|
|||||||
"notifications": "Gerätebenachrichtigungen",
|
"notifications": "Gerätebenachrichtigungen",
|
||||||
"one": "Gerät",
|
"one": "Gerät",
|
||||||
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
||||||
|
"reboot_logs": "Neustartprotokolle",
|
||||||
"restricted": "Beschränkt",
|
"restricted": "Beschränkt",
|
||||||
|
"restricted_overriden": "Dies ist ein eingeschränktes Gerät, aber es befindet sich im Entwicklungsmodus. Alle Einschränkungen werden derzeit ignoriert",
|
||||||
|
"restrictions_overriden_title": "Dev-Modus",
|
||||||
"sanity": "Gesundheit",
|
"sanity": "Gesundheit",
|
||||||
"start_import": "Geräteimport starten",
|
"start_import": "Geräteimport starten",
|
||||||
"test_batch": "Testen Sie Importdaten",
|
"test_batch": "Testen Sie Importdaten",
|
||||||
@@ -671,7 +698,15 @@
|
|||||||
"test_digicert_creds": "Anmeldeinformationen testen",
|
"test_digicert_creds": "Anmeldeinformationen testen",
|
||||||
"title": "Entitäten",
|
"title": "Entitäten",
|
||||||
"tree": "Entitätsbaum",
|
"tree": "Entitätsbaum",
|
||||||
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden. Bitte erstellen Sie neue Entitäten und erstellen Sie Veranstaltungsorte unter diesen."
|
"update_success": "Entität aktualisiert!",
|
||||||
|
"venues_under_root": "Veranstaltungsorte können nicht direkt unter der Root-Entität erstellt werden"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Dieser Vorgang wird täglich automatisch durchgeführt, ohne dass dieses manuelle Update verwendet werden muss. Die Aktualisierung dieser Datenbank kann bis zu 25 Minuten dauern",
|
||||||
|
"last_db_update_modal": "Firmware-Datenbank",
|
||||||
|
"last_db_update_title": "Datenbank",
|
||||||
|
"start_db_update": "Datenbankaktualisierung starten",
|
||||||
|
"started_db_update": "Datenbankaktualisierung gestartet, dieser Vorgang sollte bis zu 25 Minuten dauern"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Unterstützt von",
|
"powered_by": "Unterstützt von",
|
||||||
@@ -693,7 +728,7 @@
|
|||||||
"invalid_ipv6": "Ungültige IPv6-Adresse (Bsp.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
"invalid_ipv6": "Ungültige IPv6-Adresse (Bsp.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||||
"invalid_json": "Ungültige JSON-Zeichenfolge",
|
"invalid_json": "Ungültige JSON-Zeichenfolge",
|
||||||
"invalid_lease_time": "Ungültiger Lease-Time-Wert! Sie müssen im digitUnit-Format vorliegen. Zum Beispiel: 6d2h5m, was 6 Tage, 2 Stunden und 5 Minuten bedeutet. Hier sind die akzeptierten Einheiten: m, h, d. Wenn Sie eine Einheit nicht verwenden möchten, lassen Sie sie vollständig weg. Anstatt also 0d2h0m zu sagen, verwenden Sie 2h",
|
"invalid_lease_time": "Ungültiger Lease-Time-Wert! Sie müssen im digitUnit-Format vorliegen. Zum Beispiel: 6d2h5m, was 6 Tage, 2 Stunden und 5 Minuten bedeutet. Hier sind die akzeptierten Einheiten: m, h, d. Wenn Sie eine Einheit nicht verwenden möchten, lassen Sie sie vollständig weg. Anstatt also 0d2h0m zu sagen, verwenden Sie 2h",
|
||||||
"invalid_mac_uc": "Ungültiger UC-MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
"invalid_mac_uc": "Ungültiger MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
||||||
"invalid_password": "Ungültiges Passwort, bitte sehen Sie sich die Passwortrichtlinie an",
|
"invalid_password": "Ungültiges Passwort, bitte sehen Sie sich die Passwortrichtlinie an",
|
||||||
"invalid_phone_number": "Ungültige Telefonnummer",
|
"invalid_phone_number": "Ungültige Telefonnummer",
|
||||||
"invalid_phone_numbers": "Mindestens eine der Telefonnummern ist ungültig. Bitte geben Sie sie ohne Symbole und Leerzeichen oder in diesem Format an: +1(123)123-1234",
|
"invalid_phone_numbers": "Mindestens eine der Telefonnummern ist ungültig. Bitte geben Sie sie ohne Symbole und Leerzeichen oder in diesem Format an: +1(123)123-1234",
|
||||||
@@ -765,13 +800,17 @@
|
|||||||
"city": "Stadt",
|
"city": "Stadt",
|
||||||
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
|
"claim_explanation": "Um Standorte zu beanspruchen, können Sie die folgende Tabelle verwenden",
|
||||||
"country": "Land",
|
"country": "Land",
|
||||||
|
"elevation": "Elevation",
|
||||||
"geocode": "Geo-Code",
|
"geocode": "Geo-Code",
|
||||||
|
"lat": "Breite",
|
||||||
|
"longitude": "Längengrad",
|
||||||
"one": "Ort",
|
"one": "Ort",
|
||||||
"other": "Standorte",
|
"other": "Standorte",
|
||||||
"postal": "Postleitzahl",
|
"postal": "Postleitzahl",
|
||||||
"state": "Bundesstaat / Provinz",
|
"state": "Bundesstaat / Provinz",
|
||||||
"title": "Standorte",
|
"title": "Standorte",
|
||||||
"to_claim": "Standorte zu beanspruchen"
|
"to_claim": "Standorte zu beanspruchen",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Zugangsrichtlinien",
|
"access_policy": "Zugangsrichtlinien",
|
||||||
@@ -797,6 +836,7 @@
|
|||||||
"reset_password": "Passwort zurücksetzen",
|
"reset_password": "Passwort zurücksetzen",
|
||||||
"sign_in": "Einloggen",
|
"sign_in": "Einloggen",
|
||||||
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
"sms_instructions": "Sie sollten bald einen 6-stelligen Code auf Ihrem Telefon erhalten. Bitte geben Sie es unten ein, um sich anzumelden",
|
||||||
|
"suspended_error": "Konto gesperrt, wenden Sie sich bitte an Ihren Administrator",
|
||||||
"verification": "Bestätigen Sie Ihre Anmeldung",
|
"verification": "Bestätigen Sie Ihre Anmeldung",
|
||||||
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
"waiting_for_email_verification": "Konto noch nicht per E-Mail validiert. Bitte sehen Sie in Ihrem Posteingang nach oder bitten Sie Ihren Administrator, eine Bestätigung erneut zu senden",
|
||||||
"welcome_back": "Willkommen zurück!",
|
"welcome_back": "Willkommen zurück!",
|
||||||
@@ -902,7 +942,7 @@
|
|||||||
"dfs": "DFS-Überschreibung",
|
"dfs": "DFS-Überschreibung",
|
||||||
"gw_commands": "Gateway-Befehle",
|
"gw_commands": "Gateway-Befehle",
|
||||||
"identifier": "Identifikator",
|
"identifier": "Identifikator",
|
||||||
"key_verification": "Überprüfung des Signaturschlüssels",
|
"key_verification": "Signieren von Schlüsselinformationen",
|
||||||
"restricted": "Beschränkt",
|
"restricted": "Beschränkt",
|
||||||
"signed_upgrade": "Nur signiertes Upgrade",
|
"signed_upgrade": "Nur signiertes Upgrade",
|
||||||
"title": "Beschränkungen",
|
"title": "Beschränkungen",
|
||||||
@@ -966,6 +1006,8 @@
|
|||||||
"concurrent_devices": "Gleichzeitige Geräte",
|
"concurrent_devices": "Gleichzeitige Geräte",
|
||||||
"controller": "Regler",
|
"controller": "Regler",
|
||||||
"current_live_devices": "Aktuelle Live-Geräte",
|
"current_live_devices": "Aktuelle Live-Geräte",
|
||||||
|
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
|
||||||
|
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
|
||||||
"delete_success": "Gelöschte Simulation!",
|
"delete_success": "Gelöschte Simulation!",
|
||||||
"duration": "Dauer",
|
"duration": "Dauer",
|
||||||
"error_devices": "Fehler Geräte",
|
"error_devices": "Fehler Geräte",
|
||||||
@@ -973,6 +1015,7 @@
|
|||||||
"infinite": "Unendlich",
|
"infinite": "Unendlich",
|
||||||
"keep_alive": "Bleib am Leben",
|
"keep_alive": "Bleib am Leben",
|
||||||
"mac_prefix": "MAC-Präfix",
|
"mac_prefix": "MAC-Präfix",
|
||||||
|
"mac_prefix_length": "Ihr MAC-Präfix muss gültige 6 HEX-Ziffern haben (z. B.: 00112233)",
|
||||||
"max_associations": "max. Verbände",
|
"max_associations": "max. Verbände",
|
||||||
"max_clients": "Max. Kunden",
|
"max_clients": "Max. Kunden",
|
||||||
"min_associations": "Mindest. Verbände",
|
"min_associations": "Mindest. Verbände",
|
||||||
@@ -989,6 +1032,7 @@
|
|||||||
"rx_messages": "Rx-Meldungen",
|
"rx_messages": "Rx-Meldungen",
|
||||||
"sim_currently_running": "Simulation \"{{sim}}\" läuft",
|
"sim_currently_running": "Simulation \"{{sim}}\" läuft",
|
||||||
"sim_history": "{{sim}} Vorherige Läufe",
|
"sim_history": "{{sim}} Vorherige Läufe",
|
||||||
|
"simulated": "Simuliert",
|
||||||
"start": "Simulation starten",
|
"start": "Simulation starten",
|
||||||
"start_success": "Simulationslauf gestartet!",
|
"start_success": "Simulationslauf gestartet!",
|
||||||
"state_interval": "Zustandsintervall",
|
"state_interval": "Zustandsintervall",
|
||||||
@@ -1002,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"last_stats": "Letzte Statistik",
|
"last_stats": "Letzte Statistik",
|
||||||
|
"latest": "Neueste Statistiken",
|
||||||
"memory": "Erinnerung"
|
"memory": "Erinnerung"
|
||||||
},
|
},
|
||||||
"subscribers": {
|
"subscribers": {
|
||||||
@@ -1022,6 +1067,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Back-End-Protokolle",
|
"backend_logs": "Back-End-Protokolle",
|
||||||
|
"configuration": "Aufbau",
|
||||||
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
"could_not_retrieve": "Fehler: {{name}} Systeminformationen konnten nicht abgerufen werden",
|
||||||
"endpoint": "Endpunkt",
|
"endpoint": "Endpunkt",
|
||||||
"hostname": "Hostname",
|
"hostname": "Hostname",
|
||||||
@@ -1032,6 +1078,10 @@
|
|||||||
"os": "Betriebssystem",
|
"os": "Betriebssystem",
|
||||||
"processors": "Prozessoren",
|
"processors": "Prozessoren",
|
||||||
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
|
"reload_chosen_subsystems": "Ausgewählte Subsysteme neu laden",
|
||||||
|
"secrets": "Geheimnisse",
|
||||||
|
"secrets_create": "Geheimnis erstellen",
|
||||||
|
"secrets_one": "Geheimnis",
|
||||||
|
"services": "dienstleistungen",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"subsystems": "Subsysteme",
|
"subsystems": "Subsysteme",
|
||||||
"success_reload": "Reload-Befehl erfolgreich gesendet!",
|
"success_reload": "Reload-Befehl erfolgreich gesendet!",
|
||||||
@@ -1043,19 +1093,31 @@
|
|||||||
"version": "Ausführung"
|
"version": "Ausführung"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"columns": "Säulen",
|
||||||
"columns_hidden_one": "{{count}} Spalte ausgeblendet",
|
"columns_hidden_one": "{{count}} Spalte ausgeblendet",
|
||||||
"columns_hidden_other": "{{count}} Spalten ausgeblendet",
|
"columns_hidden_other": "{{count}} Spalten ausgeblendet",
|
||||||
|
"display_column": "Anzeige",
|
||||||
|
"drag_always_show": "Sie können diese Spalte nicht ausblenden, aber ihre Position ändern",
|
||||||
|
"drag_explanation": "Ziehen und Ablegen zum Neuordnen und Ändern der Spaltensichtbarkeit",
|
||||||
|
"drag_locked": "Diese Säule ist in ihrer Position arretiert",
|
||||||
|
"export_current_page": "Nur aktuelle Seite",
|
||||||
"first_page": "Erste Seite",
|
"first_page": "Erste Seite",
|
||||||
"go_to_page": "Zur Seite gehen",
|
"go_to_page": "Zur Seite gehen",
|
||||||
|
"hide_column": "verbergen",
|
||||||
"last_page": "Letzte Seite",
|
"last_page": "Letzte Seite",
|
||||||
"next_page": "Nächste Seite",
|
"next_page": "Nächste Seite",
|
||||||
"page": "Seite",
|
"page": "Seite",
|
||||||
"previous_page": "Vorherige Seite"
|
"preferences": "Tabelleneinstellungen",
|
||||||
|
"previous_page": "Vorherige Seite",
|
||||||
|
"reset": "Einstellungen zurücksetzen",
|
||||||
|
"settings": "die Einstellungen"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "E-Mail nicht validiert",
|
||||||
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
"error_fetching": "Fehler beim Abrufen der Benutzerinformationen: {{e}}",
|
||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"role": "Rolle",
|
"role": "Rolle",
|
||||||
|
"suspended": "Suspendiert",
|
||||||
"title": "Nutzer"
|
"title": "Nutzer"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1162,11 @@
|
|||||||
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
"successfully_update_devices": " {{num}} Geräte werden aktualisiert!",
|
||||||
"title": "Veranstaltungsorte",
|
"title": "Veranstaltungsorte",
|
||||||
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
"update_all_devices": "Alle Gerätekonfigurationen aktualisieren",
|
||||||
"upgrade_all_devices": "Aktualisieren Sie alle Geräte auf die neueste Firmware",
|
"update_success": "Veranstaltungsort aktualisiert!",
|
||||||
|
"upgrade_all_devices": "Aktualisieren Sie die Firmware aller Geräte",
|
||||||
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
|
"upgrade_all_devices_error": "Fehler beim Aktualisieren von Geräten: {{e}}",
|
||||||
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
|
"upgrade_all_devices_success": "Upgrade von Geräten erfolgreich gestartet!",
|
||||||
|
"upgrade_options_available": "Hier sind alle verfügbaren Revisionen, bitte wählen Sie diejenige aus, auf die ALLE Geräte dieses Veranstaltungsortes aktualisiert werden sollen",
|
||||||
"use_existing": "Benutze existierendes",
|
"use_existing": "Benutze existierendes",
|
||||||
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
|
"use_existing_contacts": "Verwenden Sie vorhandene Kontakte",
|
||||||
"use_this_contact": "Verwenden Sie diesen Kontakt"
|
"use_this_contact": "Verwenden Sie diesen Kontakt"
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"health": "Health",
|
"health": "Health",
|
||||||
"inactive": "Inactive",
|
"inactive": "Inactive",
|
||||||
"interval": "Interval",
|
"interval": "Interval",
|
||||||
|
"last_connected": "Last Connected",
|
||||||
|
"last_connected_tooltip": "Last time this device was connected to the controller. This can be used to estimate when a device disconnected",
|
||||||
"last_connection": "Last Connection",
|
"last_connection": "Last Connection",
|
||||||
"last_contact": "Last Contact",
|
"last_contact": "Last Contact",
|
||||||
"last_disconnection": "Last Disconnection",
|
"last_disconnection": "Last Disconnection",
|
||||||
@@ -79,8 +81,11 @@
|
|||||||
"live_view_help": "Live View Help",
|
"live_view_help": "Live View Help",
|
||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"memory_used": "Memory Used",
|
"memory_used": "Memory Used",
|
||||||
"missing_board": "Analytics monitoring on this venue is no longer active, please restart monitoring using the top menu",
|
"missing_board": "Analytics monitoring on this venue is no longer active. Click here to restart monitoring",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"monitoring": "Monitoring",
|
||||||
|
"no_board": "No Monitoring",
|
||||||
|
"no_board_description": "You are not monitoring this Venue at the moment, click here to start",
|
||||||
"noise": "Noise",
|
"noise": "Noise",
|
||||||
"packets": "Packets",
|
"packets": "Packets",
|
||||||
"radio": "Radio",
|
"radio": "Radio",
|
||||||
@@ -91,6 +96,8 @@
|
|||||||
"retries": "Retries",
|
"retries": "Retries",
|
||||||
"search_serials": "Search Serials",
|
"search_serials": "Search Serials",
|
||||||
"stop_monitoring": "Stop Monitoring",
|
"stop_monitoring": "Stop Monitoring",
|
||||||
|
"stop_monitoring_success": "Stopped Monitoring Venue!",
|
||||||
|
"stop_monitoring_warning": "Are you sure? This will erase all recorded monitoring data for this venue",
|
||||||
"temperature": "Temperature",
|
"temperature": "Temperature",
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"total_data": "Total Data",
|
"total_data": "Total Data",
|
||||||
@@ -175,6 +182,7 @@
|
|||||||
"other": "Commands",
|
"other": "Commands",
|
||||||
"override_dfs": "Override DFS",
|
"override_dfs": "Override DFS",
|
||||||
"reboot": "Reboot",
|
"reboot": "Reboot",
|
||||||
|
"reboot_description": "Do you want to reboot this device?",
|
||||||
"reboot_error": "Error while sending reboot command: {{e}}",
|
"reboot_error": "Error while sending reboot command: {{e}}",
|
||||||
"reboot_success": "Successfully sent reboot command!",
|
"reboot_success": "Successfully sent reboot command!",
|
||||||
"revision": "Revision",
|
"revision": "Revision",
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
"error_download": "Error while trying to download: {{e}}",
|
"error_download": "Error while trying to download: {{e}}",
|
||||||
"errors": "Errors",
|
"errors": "Errors",
|
||||||
"exit_fullscreen": "Exit",
|
"exit_fullscreen": "Exit",
|
||||||
|
"export": "Export",
|
||||||
"finished": "Finished",
|
"finished": "Finished",
|
||||||
"fullscreen": "Fullscreen",
|
"fullscreen": "Fullscreen",
|
||||||
"general_error": "Error connecting to the server. Please consult your administrator.",
|
"general_error": "Error connecting to the server. Please consult your administrator.",
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
"error_pushes_one": "Error (could be because of bad configuration): {{count}}",
|
"error_pushes_one": "Error (could be because of bad configuration): {{count}}",
|
||||||
"error_pushes_other": "Errors (could be because of bad configuration): {{count}}",
|
"error_pushes_other": "Errors (could be because of bad configuration): {{count}}",
|
||||||
"expert_name": "Expert Mode",
|
"expert_name": "Expert Mode",
|
||||||
|
"expert_name_explanation": "You can use expert mode to directly modify your configuration, including adding values that are not currently supported by the UI. Please use this format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||||
"explanation": "Explanation",
|
"explanation": "Explanation",
|
||||||
"firewall": "Firewall",
|
"firewall": "Firewall",
|
||||||
"firmware_upgrade": "Firmware Upgrade",
|
"firmware_upgrade": "Firmware Upgrade",
|
||||||
@@ -391,6 +401,7 @@
|
|||||||
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
|
"warning_pushes_one": "Waiting for devices to connect: {{count}}",
|
||||||
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
|
"warning_pushes_other": "Waiting for devices to connect: {{count}}",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
|
"wifi_bands_max": "There cannot be more than 8 SSIDs using this wifi-band",
|
||||||
"wifi_frames": "WiFi Frames"
|
"wifi_frames": "WiFi Frames"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -515,6 +526,7 @@
|
|||||||
"ouis_explanation": "OUIs of devices that have connected to this firmware server",
|
"ouis_explanation": "OUIs of devices that have connected to this firmware server",
|
||||||
"outdated_one": "Firmware {{count}} day old",
|
"outdated_one": "Firmware {{count}} day old",
|
||||||
"outdated_other": "Firmware {{count}} days old",
|
"outdated_other": "Firmware {{count}} days old",
|
||||||
|
"outdated_unknown": "Firmware of unknown age",
|
||||||
"release": "Release",
|
"release": "Release",
|
||||||
"show_dev_releases": "Dev Releases",
|
"show_dev_releases": "Dev Releases",
|
||||||
"status_explanation": "Connection status of devices that have connected to this firmware server",
|
"status_explanation": "Connection status of devices that have connected to this firmware server",
|
||||||
@@ -530,6 +542,16 @@
|
|||||||
"queue": {
|
"queue": {
|
||||||
"title": "Event Queue"
|
"title": "Event Queue"
|
||||||
},
|
},
|
||||||
|
"radius": {
|
||||||
|
"calling_station_id": "Station",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"disconnect_success": "Radius session disconnected!",
|
||||||
|
"input_octets": "Input",
|
||||||
|
"output_octets": "Output",
|
||||||
|
"radius_clients": "Radius Clients",
|
||||||
|
"session_time": "Session Time",
|
||||||
|
"username": "Username"
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"load": "Load (1 | 5 | 15 m.)",
|
"load": "Load (1 | 5 | 15 m.)",
|
||||||
"seconds_ago": "{{s}} seconds ago",
|
"seconds_ago": "{{s}} seconds ago",
|
||||||
@@ -600,6 +622,7 @@
|
|||||||
"certificate_expires_in": "Certificate Expiry",
|
"certificate_expires_in": "Certificate Expiry",
|
||||||
"certificate_expiry": "Cert. Expires In",
|
"certificate_expiry": "Cert. Expires In",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"crash_logs": "Crash Logs",
|
||||||
"create_errors": "errors while trying to create devices",
|
"create_errors": "errors while trying to create devices",
|
||||||
"create_success": " devices successfully created",
|
"create_success": " devices successfully created",
|
||||||
"current_firmware": "Current Firmware",
|
"current_firmware": "Current Firmware",
|
||||||
@@ -613,6 +636,7 @@
|
|||||||
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
"import_device_warning": "Please make sure there are no extra spaces at the start or end of any values unless it is part of the value desired",
|
||||||
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "To bulk import devices, you need to use a CSV file with the following columns: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
"invalid_serial_number": "Invalid Serial Number (needs to be 12 HEX chars)",
|
||||||
|
"logs_one": "Log",
|
||||||
"new_devices": "new devices",
|
"new_devices": "new devices",
|
||||||
"no_model_image": "No Model Image Found",
|
"no_model_image": "No Model Image Found",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
@@ -620,7 +644,10 @@
|
|||||||
"notifications": "Device Notifications",
|
"notifications": "Device Notifications",
|
||||||
"one": "Device",
|
"one": "Device",
|
||||||
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
||||||
|
"reboot_logs": "Reboot Logs",
|
||||||
"restricted": "Restricted",
|
"restricted": "Restricted",
|
||||||
|
"restricted_overriden": "This is a restricted device, but it is in development mode. All restrictions are currently ignored",
|
||||||
|
"restrictions_overriden_title": "Dev Mode",
|
||||||
"sanity": "Sanity",
|
"sanity": "Sanity",
|
||||||
"start_import": "Start Device Importation",
|
"start_import": "Start Device Importation",
|
||||||
"test_batch": "Test Import Data",
|
"test_batch": "Test Import Data",
|
||||||
@@ -671,7 +698,15 @@
|
|||||||
"test_digicert_creds": "Test Credentials",
|
"test_digicert_creds": "Test Credentials",
|
||||||
"title": "Entities",
|
"title": "Entities",
|
||||||
"tree": "Entity Tree",
|
"tree": "Entity Tree",
|
||||||
"venues_under_root": "Venues cannot be created directly under the root entity. Please create new entities and create venues under these."
|
"update_success": "Entity updated!",
|
||||||
|
"venues_under_root": "Venues cannot be created directly under the root entity"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "This operation is done daily automatically without need to use this manual update. Updating this database can take up to 25 minutes",
|
||||||
|
"last_db_update_modal": "Firmware Database",
|
||||||
|
"last_db_update_title": "Database",
|
||||||
|
"start_db_update": "Start Database Update",
|
||||||
|
"started_db_update": "Started database update, this operation should take up to 25 minutes to complete"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Powered By",
|
"powered_by": "Powered By",
|
||||||
@@ -693,7 +728,7 @@
|
|||||||
"invalid_ipv6": "Invalid IPv6 address (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
"invalid_ipv6": "Invalid IPv6 address (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||||
"invalid_json": "Invalid JSON string",
|
"invalid_json": "Invalid JSON string",
|
||||||
"invalid_lease_time": "Invalid lease time value! They need to be in the digitUnit format. For example: 6d2h5m, which means 6 days, 2 hours and 5 minutes. Here are the accepted units: m, h, d. If you do not want to use a unit, omit it completely. So instead of saying 0d2h0m, use 2h",
|
"invalid_lease_time": "Invalid lease time value! They need to be in the digitUnit format. For example: 6d2h5m, which means 6 days, 2 hours and 5 minutes. Here are the accepted units: m, h, d. If you do not want to use a unit, omit it completely. So instead of saying 0d2h0m, use 2h",
|
||||||
"invalid_mac_uc": "Invalid UC-MAC value, for example: 00:00:5e:00:53:af",
|
"invalid_mac_uc": "Invalid MAC value, for example: 00:00:5e:00:53:af",
|
||||||
"invalid_password": "Invalid password, please look at the password policy",
|
"invalid_password": "Invalid password, please look at the password policy",
|
||||||
"invalid_phone_number": "Invalid Phone Number",
|
"invalid_phone_number": "Invalid Phone Number",
|
||||||
"invalid_phone_numbers": "One or more of the phone numbers are invalid. Please provide them without symbols and spaces, or in this format: +1(123)123-1234",
|
"invalid_phone_numbers": "One or more of the phone numbers are invalid. Please provide them without symbols and spaces, or in this format: +1(123)123-1234",
|
||||||
@@ -734,8 +769,8 @@
|
|||||||
"success_remove_claim": "Successfully removed claim on: {{serial}}",
|
"success_remove_claim": "Successfully removed claim on: {{serial}}",
|
||||||
"successful_reboots": "Started Rebooting: {{count}}",
|
"successful_reboots": "Started Rebooting: {{count}}",
|
||||||
"successful_upgrades": "Successful upgrades: {{count}}",
|
"successful_upgrades": "Successful upgrades: {{count}}",
|
||||||
"tag_one": "Tag",
|
"tag_one": "Device",
|
||||||
"tags": "Inventory Tags",
|
"tags": "Inventory Devices",
|
||||||
"title": "Inventory",
|
"title": "Inventory",
|
||||||
"warning_reboots": "Not connected: {{count}}",
|
"warning_reboots": "Not connected: {{count}}",
|
||||||
"warning_upgrades": "Devices not connected: {{count}}"
|
"warning_upgrades": "Devices not connected: {{count}}"
|
||||||
@@ -765,13 +800,17 @@
|
|||||||
"city": "City",
|
"city": "City",
|
||||||
"claim_explanation": "To claim locations you can use the table below",
|
"claim_explanation": "To claim locations you can use the table below",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
|
"elevation": "Elevation",
|
||||||
"geocode": "Geo Code",
|
"geocode": "Geo Code",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Location",
|
"one": "Location",
|
||||||
"other": "Locations",
|
"other": "Locations",
|
||||||
"postal": "ZIP/Postal Code",
|
"postal": "ZIP/Postal Code",
|
||||||
"state": "State/Province",
|
"state": "State/Province",
|
||||||
"title": "Locations",
|
"title": "Locations",
|
||||||
"to_claim": "Locations to claim"
|
"to_claim": "Locations to claim",
|
||||||
|
"view_gps": "View GPS Location"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Access Policy",
|
"access_policy": "Access Policy",
|
||||||
@@ -797,6 +836,7 @@
|
|||||||
"reset_password": "Reset Password",
|
"reset_password": "Reset Password",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
"sms_instructions": "You should receive a 6-digit code on your phone soon. Please enter it below to login",
|
||||||
|
"suspended_error": "Suspended account, please contact your administrator",
|
||||||
"verification": "Verify your login",
|
"verification": "Verify your login",
|
||||||
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
"waiting_for_email_verification": "Account not yet email validated. Please look at your inbox or ask your administrator to resend a validation",
|
||||||
"welcome_back": "Welcome Back!",
|
"welcome_back": "Welcome Back!",
|
||||||
@@ -902,7 +942,7 @@
|
|||||||
"dfs": "DFS Override",
|
"dfs": "DFS Override",
|
||||||
"gw_commands": "Gateway Commands",
|
"gw_commands": "Gateway Commands",
|
||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"key_verification": "Signing Key Verification",
|
"key_verification": "Signing Key Information",
|
||||||
"restricted": "Restricted",
|
"restricted": "Restricted",
|
||||||
"signed_upgrade": "Signed Upgrade Only",
|
"signed_upgrade": "Signed Upgrade Only",
|
||||||
"title": "Restrictions",
|
"title": "Restrictions",
|
||||||
@@ -966,6 +1006,8 @@
|
|||||||
"concurrent_devices": "Concurrent Devices",
|
"concurrent_devices": "Concurrent Devices",
|
||||||
"controller": "Controller",
|
"controller": "Controller",
|
||||||
"current_live_devices": "Current Live Devices",
|
"current_live_devices": "Current Live Devices",
|
||||||
|
"currently_running_one": "There is currently {{count}} simulation running",
|
||||||
|
"currently_running_other": "There are currently {{count}} simulations running",
|
||||||
"delete_success": "Deleted Simulation!",
|
"delete_success": "Deleted Simulation!",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"error_devices": "Error Devices",
|
"error_devices": "Error Devices",
|
||||||
@@ -973,6 +1015,7 @@
|
|||||||
"infinite": "Infinite",
|
"infinite": "Infinite",
|
||||||
"keep_alive": "Keep Alive",
|
"keep_alive": "Keep Alive",
|
||||||
"mac_prefix": "MAC Prefix",
|
"mac_prefix": "MAC Prefix",
|
||||||
|
"mac_prefix_length": "Your MAC prefix needs to be valid 6 HEX digits (ex.: 00112233)",
|
||||||
"max_associations": "Max. Associations",
|
"max_associations": "Max. Associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"min_associations": "Min. Associations",
|
"min_associations": "Min. Associations",
|
||||||
@@ -989,6 +1032,7 @@
|
|||||||
"rx_messages": "Rx Messages",
|
"rx_messages": "Rx Messages",
|
||||||
"sim_currently_running": "Simulation \"{{sim}}\" in progress",
|
"sim_currently_running": "Simulation \"{{sim}}\" in progress",
|
||||||
"sim_history": "{{sim}} Previous Runs",
|
"sim_history": "{{sim}} Previous Runs",
|
||||||
|
"simulated": "Simulated",
|
||||||
"start": "Start Simulation",
|
"start": "Start Simulation",
|
||||||
"start_success": "Started simulation run!",
|
"start_success": "Started simulation run!",
|
||||||
"state_interval": "State Interval",
|
"state_interval": "State Interval",
|
||||||
@@ -1002,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"last_stats": "Last Statistics",
|
"last_stats": "Last Statistics",
|
||||||
|
"latest": "Latest Statistics",
|
||||||
"memory": "Memory"
|
"memory": "Memory"
|
||||||
},
|
},
|
||||||
"subscribers": {
|
"subscribers": {
|
||||||
@@ -1022,6 +1067,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Back-End Logs",
|
"backend_logs": "Back-End Logs",
|
||||||
|
"configuration": "Configuration",
|
||||||
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
"could_not_retrieve": "Error: could not retrieve {{name}} system information",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"hostname": "Host Name",
|
"hostname": "Host Name",
|
||||||
@@ -1032,6 +1078,10 @@
|
|||||||
"os": "Operating System",
|
"os": "Operating System",
|
||||||
"processors": "Processors",
|
"processors": "Processors",
|
||||||
"reload_chosen_subsystems": "Reload Chosen Subsystems",
|
"reload_chosen_subsystems": "Reload Chosen Subsystems",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secrets_create": "Create Secret",
|
||||||
|
"secrets_one": "Secret",
|
||||||
|
"services": "Services",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"subsystems": "Subsystems",
|
"subsystems": "Subsystems",
|
||||||
"success_reload": "Successfully sent reload command!",
|
"success_reload": "Successfully sent reload command!",
|
||||||
@@ -1043,19 +1093,31 @@
|
|||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"columns": "Columns",
|
||||||
"columns_hidden_one": "{{count}} Column Hidden",
|
"columns_hidden_one": "{{count}} Column Hidden",
|
||||||
"columns_hidden_other": "{{count}} Columns Hidden",
|
"columns_hidden_other": "{{count}} Columns Hidden",
|
||||||
|
"display_column": "Display",
|
||||||
|
"drag_always_show": "You cannot hide this column but can change its position ",
|
||||||
|
"drag_explanation": "Drag and drop to reorder and change column visibility",
|
||||||
|
"drag_locked": "This column is locked in its position",
|
||||||
|
"export_current_page": "Current Page Only",
|
||||||
"first_page": "First Page",
|
"first_page": "First Page",
|
||||||
"go_to_page": "Go to page",
|
"go_to_page": "Go to page",
|
||||||
|
"hide_column": "Hide",
|
||||||
"last_page": "Last Page",
|
"last_page": "Last Page",
|
||||||
"next_page": "Next Page",
|
"next_page": "Next Page",
|
||||||
"page": "Page",
|
"page": "Page",
|
||||||
"previous_page": "Previous Page"
|
"preferences": "Table Preferences",
|
||||||
|
"previous_page": "Previous Page",
|
||||||
|
"reset": "Reset Preferences",
|
||||||
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "email not validated",
|
||||||
"error_fetching": "Error fetching user information: {{e}}",
|
"error_fetching": "Error fetching user information: {{e}}",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
|
"suspended": "suspended",
|
||||||
"title": "User"
|
"title": "User"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1162,11 @@
|
|||||||
"successfully_update_devices": "Updating {{num}} devices!",
|
"successfully_update_devices": "Updating {{num}} devices!",
|
||||||
"title": "Venues",
|
"title": "Venues",
|
||||||
"update_all_devices": "Update All Device Configurations",
|
"update_all_devices": "Update All Device Configurations",
|
||||||
"upgrade_all_devices": "Upgrade All Devices to Latest Firmware",
|
"update_success": "Venue updated!",
|
||||||
|
"upgrade_all_devices": "Upgrade All Devices Firmware",
|
||||||
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
|
"upgrade_all_devices_error": "Error upgrading devices: {{e}}",
|
||||||
"upgrade_all_devices_success": "Successfully started upgrading devices!",
|
"upgrade_all_devices_success": "Successfully started upgrading devices!",
|
||||||
|
"upgrade_options_available": "Here are all available revisions, please select the one you want ALL of this venue's devices to be upgrade to",
|
||||||
"use_existing": "Use Existing",
|
"use_existing": "Use Existing",
|
||||||
"use_existing_contacts": "Use Existing Contacts",
|
"use_existing_contacts": "Use Existing Contacts",
|
||||||
"use_this_contact": "Use this contact"
|
"use_this_contact": "Use this contact"
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"health": "salud",
|
"health": "salud",
|
||||||
"inactive": "inactivo",
|
"inactive": "inactivo",
|
||||||
"interval": "intervalo",
|
"interval": "intervalo",
|
||||||
|
"last_connected": "Última conexion",
|
||||||
|
"last_connected_tooltip": "La última vez que se conectó este dispositivo al controlador. Esto se puede usar para estimar cuándo se desconectó un dispositivo",
|
||||||
"last_connection": "Última conexion",
|
"last_connection": "Última conexion",
|
||||||
"last_contact": "Último contacto",
|
"last_contact": "Último contacto",
|
||||||
"last_disconnection": "Última desconexión",
|
"last_disconnection": "Última desconexión",
|
||||||
@@ -79,8 +81,11 @@
|
|||||||
"live_view_help": "Ayuda de visualización en vivo",
|
"live_view_help": "Ayuda de visualización en vivo",
|
||||||
"memory": "Memoria",
|
"memory": "Memoria",
|
||||||
"memory_used": "Memoria usada",
|
"memory_used": "Memoria usada",
|
||||||
"missing_board": "El monitoreo analítico en este lugar ya no está activo, reinicie el monitoreo usando el menú superior",
|
"missing_board": "El monitoreo analítico en este lugar ya no está activo. Haga clic aquí para reiniciar el monitoreo",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
|
"monitoring": "Vigilancia",
|
||||||
|
"no_board": "Sin monitoreo",
|
||||||
|
"no_board_description": "No está monitoreando este lugar en este momento, haga clic aquí para comenzar",
|
||||||
"noise": "Ruido",
|
"noise": "Ruido",
|
||||||
"packets": "Paquetes",
|
"packets": "Paquetes",
|
||||||
"radio": "RADIO",
|
"radio": "RADIO",
|
||||||
@@ -91,6 +96,8 @@
|
|||||||
"retries": "Reintentos",
|
"retries": "Reintentos",
|
||||||
"search_serials": "Buscar seriales",
|
"search_serials": "Buscar seriales",
|
||||||
"stop_monitoring": "Dejar de monitorear",
|
"stop_monitoring": "Dejar de monitorear",
|
||||||
|
"stop_monitoring_success": "¡Se detuvo el lugar de monitoreo!",
|
||||||
|
"stop_monitoring_warning": "¿Está seguro? Esto borrará todos los datos de monitoreo grabados para este lugar.",
|
||||||
"temperature": "temperatura",
|
"temperature": "temperatura",
|
||||||
"title": "ANALÍTICA",
|
"title": "ANALÍTICA",
|
||||||
"total_data": "Datos totales",
|
"total_data": "Datos totales",
|
||||||
@@ -175,6 +182,7 @@
|
|||||||
"other": "comandos",
|
"other": "comandos",
|
||||||
"override_dfs": "Anular DFS",
|
"override_dfs": "Anular DFS",
|
||||||
"reboot": "Reiniciar",
|
"reboot": "Reiniciar",
|
||||||
|
"reboot_description": "¿Quieres reiniciar este dispositivo?",
|
||||||
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
|
"reboot_error": "Error al enviar el comando de reinicio: {{e}}",
|
||||||
"reboot_success": "¡Comando de reinicio enviado con éxito!",
|
"reboot_success": "¡Comando de reinicio enviado con éxito!",
|
||||||
"revision": "revisión",
|
"revision": "revisión",
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
"error_download": "Error al intentar descargar: {{e}}",
|
"error_download": "Error al intentar descargar: {{e}}",
|
||||||
"errors": "Los errores",
|
"errors": "Los errores",
|
||||||
"exit_fullscreen": "salida",
|
"exit_fullscreen": "salida",
|
||||||
|
"export": "Exportar",
|
||||||
"finished": "terminado",
|
"finished": "terminado",
|
||||||
"fullscreen": "Pantalla Completa",
|
"fullscreen": "Pantalla Completa",
|
||||||
"general_error": "Error al conectar con el servidor. Consulte a su administrador.",
|
"general_error": "Error al conectar con el servidor. Consulte a su administrador.",
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
"error_pushes_one": "Error (podría deberse a una mala configuración): {{count}}",
|
"error_pushes_one": "Error (podría deberse a una mala configuración): {{count}}",
|
||||||
"error_pushes_other": "Errores (pueden deberse a una mala configuración): {{count}}",
|
"error_pushes_other": "Errores (pueden deberse a una mala configuración): {{count}}",
|
||||||
"expert_name": "Modo experto",
|
"expert_name": "Modo experto",
|
||||||
|
"expert_name_explanation": "Puede usar el modo experto para modificar directamente su configuración, incluida la adición de valores que actualmente no son compatibles con la interfaz de usuario. Utilice este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||||
"explanation": "Explicación",
|
"explanation": "Explicación",
|
||||||
"firewall": "cortafuegos",
|
"firewall": "cortafuegos",
|
||||||
"firmware_upgrade": "Actualización de firmware",
|
"firmware_upgrade": "Actualización de firmware",
|
||||||
@@ -391,6 +401,7 @@
|
|||||||
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
|
"warning_pushes_one": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||||
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
|
"warning_pushes_other": "Esperando a que los dispositivos se conecten: {{count}}",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
"wifi_bands_max": "No puede haber más de 8 SSID usando esta banda wifi",
|
||||||
"wifi_frames": "Marcos WiFi"
|
"wifi_frames": "Marcos WiFi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -515,6 +526,7 @@
|
|||||||
"ouis_explanation": "OUI de dispositivos que se han conectado a este servidor de firmware",
|
"ouis_explanation": "OUI de dispositivos que se han conectado a este servidor de firmware",
|
||||||
"outdated_one": "Firmware {{count}} día de antigüedad",
|
"outdated_one": "Firmware {{count}} día de antigüedad",
|
||||||
"outdated_other": "Firmware de {{count}} días de antigüedad",
|
"outdated_other": "Firmware de {{count}} días de antigüedad",
|
||||||
|
"outdated_unknown": "Firmware de antigüedad desconocida",
|
||||||
"release": "Lanzamiento",
|
"release": "Lanzamiento",
|
||||||
"show_dev_releases": "Lanzamientos de desarrollo",
|
"show_dev_releases": "Lanzamientos de desarrollo",
|
||||||
"status_explanation": "Estado de conexión de los dispositivos que se han conectado a este servidor de firmware",
|
"status_explanation": "Estado de conexión de los dispositivos que se han conectado a este servidor de firmware",
|
||||||
@@ -530,6 +542,16 @@
|
|||||||
"queue": {
|
"queue": {
|
||||||
"title": "Cola de eventos"
|
"title": "Cola de eventos"
|
||||||
},
|
},
|
||||||
|
"radius": {
|
||||||
|
"calling_station_id": "Estación",
|
||||||
|
"disconnect": "desconectar",
|
||||||
|
"disconnect_success": "¡Sesión de radio desconectada!",
|
||||||
|
"input_octets": "entrada",
|
||||||
|
"output_octets": "salida",
|
||||||
|
"radius_clients": "Clientes de radio",
|
||||||
|
"session_time": "Tiempo de sesión",
|
||||||
|
"username": "Nombre de usuario"
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"load": "Carga (1 | 5 | 15 m.)",
|
"load": "Carga (1 | 5 | 15 m.)",
|
||||||
"seconds_ago": " Hace {{s}} segundos",
|
"seconds_ago": " Hace {{s}} segundos",
|
||||||
@@ -600,6 +622,7 @@
|
|||||||
"certificate_expires_in": "El certificado caduca en",
|
"certificate_expires_in": "El certificado caduca en",
|
||||||
"certificate_expiry": "Cert. Expira en",
|
"certificate_expiry": "Cert. Expira en",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"crash_logs": "Registros de fallas",
|
||||||
"create_errors": "errores al intentar crear dispositivos",
|
"create_errors": "errores al intentar crear dispositivos",
|
||||||
"create_success": " dispositivos creados con éxito",
|
"create_success": " dispositivos creados con éxito",
|
||||||
"current_firmware": "Firmware actual",
|
"current_firmware": "Firmware actual",
|
||||||
@@ -613,6 +636,7 @@
|
|||||||
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
"import_device_warning": "Asegúrese de que no haya espacios adicionales al principio o al final de ningún valor a menos que sea parte del valor deseado",
|
||||||
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
"import_explanation": "Para importar dispositivos de forma masiva, debe usar un archivo CSV con las siguientes columnas: Número de serie, Tipo de dispositivo, Nombre, Descripción, Nota",
|
||||||
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
"invalid_serial_number": "Número de serie no válido (debe tener 12 caracteres HEX)",
|
||||||
|
"logs_one": "Iniciar sesión",
|
||||||
"new_devices": "Nuevos dispositivos",
|
"new_devices": "Nuevos dispositivos",
|
||||||
"no_model_image": "No se encontró ninguna imagen de modelo",
|
"no_model_image": "No se encontró ninguna imagen de modelo",
|
||||||
"not_connected": "No conectado",
|
"not_connected": "No conectado",
|
||||||
@@ -620,7 +644,10 @@
|
|||||||
"notifications": "notificaciones de dispositivos",
|
"notifications": "notificaciones de dispositivos",
|
||||||
"one": "Dispositivo",
|
"one": "Dispositivo",
|
||||||
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
||||||
|
"reboot_logs": "Reiniciar registros",
|
||||||
"restricted": "Restringido",
|
"restricted": "Restringido",
|
||||||
|
"restricted_overriden": "Este es un dispositivo restringido, pero está en modo de desarrollo. Actualmente se ignoran todas las restricciones.",
|
||||||
|
"restrictions_overriden_title": "MODO DE DESARROLLO",
|
||||||
"sanity": "Cordura",
|
"sanity": "Cordura",
|
||||||
"start_import": "Iniciar la importación de dispositivos",
|
"start_import": "Iniciar la importación de dispositivos",
|
||||||
"test_batch": "Datos de importación de prueba",
|
"test_batch": "Datos de importación de prueba",
|
||||||
@@ -671,7 +698,15 @@
|
|||||||
"test_digicert_creds": "Credenciales de prueba",
|
"test_digicert_creds": "Credenciales de prueba",
|
||||||
"title": "entidades",
|
"title": "entidades",
|
||||||
"tree": "Árbol de entidades",
|
"tree": "Árbol de entidades",
|
||||||
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz. Cree nuevas entidades y cree lugares bajo estas."
|
"update_success": "¡Entidad actualizada!",
|
||||||
|
"venues_under_root": "Los lugares no se pueden crear directamente bajo la entidad raíz"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Esta operación se realiza automáticamente todos los días de forma automática sin necesidad de utilizar esta actualización manual. La actualización de esta base de datos puede tardar hasta 25 minutos",
|
||||||
|
"last_db_update_modal": "Base de datos de firmware",
|
||||||
|
"last_db_update_title": "Base de datos",
|
||||||
|
"start_db_update": "Iniciar actualización de la base de datos",
|
||||||
|
"started_db_update": "Actualización de la base de datos iniciada, esta operación debería tardar hasta 25 minutos en completarse"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "energizado por",
|
"powered_by": "energizado por",
|
||||||
@@ -693,7 +728,7 @@
|
|||||||
"invalid_ipv6": "Dirección IPv6 no válida (ej.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
"invalid_ipv6": "Dirección IPv6 no válida (ej.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||||
"invalid_json": "Cadena JSON no válida",
|
"invalid_json": "Cadena JSON no válida",
|
||||||
"invalid_lease_time": "¡Valor de tiempo de arrendamiento no válido! Deben estar en el formato digitUnit. Por ejemplo: 6d2h5m, lo que significa 6 días, 2 horas y 5 minutos. Estas son las unidades aceptadas: m, h, d. Si no desea utilizar una unidad, omítala por completo. Entonces, en lugar de decir 0d2h0m, usa 2h",
|
"invalid_lease_time": "¡Valor de tiempo de arrendamiento no válido! Deben estar en el formato digitUnit. Por ejemplo: 6d2h5m, lo que significa 6 días, 2 horas y 5 minutos. Estas son las unidades aceptadas: m, h, d. Si no desea utilizar una unidad, omítala por completo. Entonces, en lugar de decir 0d2h0m, usa 2h",
|
||||||
"invalid_mac_uc": "Valor de UC-MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
"invalid_mac_uc": "Valor de MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
||||||
"invalid_password": "Contraseña no válida, consulte la política de contraseñas",
|
"invalid_password": "Contraseña no válida, consulte la política de contraseñas",
|
||||||
"invalid_phone_number": "Numero de telefono invalido",
|
"invalid_phone_number": "Numero de telefono invalido",
|
||||||
"invalid_phone_numbers": "Uno o más de los números de teléfono no son válidos. Proporciónelos sin símbolos ni espacios, o en este formato: +1(123)123-1234",
|
"invalid_phone_numbers": "Uno o más de los números de teléfono no son válidos. Proporciónelos sin símbolos ni espacios, o en este formato: +1(123)123-1234",
|
||||||
@@ -765,13 +800,17 @@
|
|||||||
"city": "ciudad",
|
"city": "ciudad",
|
||||||
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
|
"claim_explanation": "Para reclamar ubicaciones, puede usar la tabla a continuación",
|
||||||
"country": "País",
|
"country": "País",
|
||||||
|
"elevation": "Elevación",
|
||||||
"geocode": "Código geográfico",
|
"geocode": "Código geográfico",
|
||||||
|
"lat": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
"one": "Ubicación",
|
"one": "Ubicación",
|
||||||
"other": "Ubicaciones",
|
"other": "Ubicaciones",
|
||||||
"postal": "código postal",
|
"postal": "código postal",
|
||||||
"state": "Provincia del estado",
|
"state": "Provincia del estado",
|
||||||
"title": "Ubicaciones",
|
"title": "Ubicaciones",
|
||||||
"to_claim": "Ubicaciones para reclamar"
|
"to_claim": "Ubicaciones para reclamar",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Política de acceso",
|
"access_policy": "Política de acceso",
|
||||||
@@ -797,6 +836,7 @@
|
|||||||
"reset_password": "Restablecer la contraseña",
|
"reset_password": "Restablecer la contraseña",
|
||||||
"sign_in": "Registrarse",
|
"sign_in": "Registrarse",
|
||||||
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
"sms_instructions": "Debería recibir un código de 6 dígitos en su teléfono pronto. Por favor, introdúzcalo a continuación para iniciar sesión",
|
||||||
|
"suspended_error": "Cuenta suspendida, comuníquese con su administrador",
|
||||||
"verification": "Verifica tu inicio de sesión",
|
"verification": "Verifica tu inicio de sesión",
|
||||||
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
"waiting_for_email_verification": "Cuenta aún no validada por correo electrónico. Mire su bandeja de entrada o solicite a su administrador que vuelva a enviar una validación.",
|
||||||
"welcome_back": "¡Dar una buena acogida!",
|
"welcome_back": "¡Dar una buena acogida!",
|
||||||
@@ -902,7 +942,7 @@
|
|||||||
"dfs": "Anulación de DFS",
|
"dfs": "Anulación de DFS",
|
||||||
"gw_commands": "Comandos de puerta de enlace",
|
"gw_commands": "Comandos de puerta de enlace",
|
||||||
"identifier": "Identificador",
|
"identifier": "Identificador",
|
||||||
"key_verification": "Verificación de clave de firma",
|
"key_verification": "Información clave de firma",
|
||||||
"restricted": "Restringido",
|
"restricted": "Restringido",
|
||||||
"signed_upgrade": "Solo actualización firmada",
|
"signed_upgrade": "Solo actualización firmada",
|
||||||
"title": "Las restricciones",
|
"title": "Las restricciones",
|
||||||
@@ -966,6 +1006,8 @@
|
|||||||
"concurrent_devices": "Dispositivos concurrentes",
|
"concurrent_devices": "Dispositivos concurrentes",
|
||||||
"controller": "Controlador",
|
"controller": "Controlador",
|
||||||
"current_live_devices": "Dispositivos activos actuales",
|
"current_live_devices": "Dispositivos activos actuales",
|
||||||
|
"currently_running_one": "Actualmente hay {{count}} simulación en ejecución",
|
||||||
|
"currently_running_other": "Actualmente hay {{count}} simulaciones ejecutándose",
|
||||||
"delete_success": "¡Simulación eliminada!",
|
"delete_success": "¡Simulación eliminada!",
|
||||||
"duration": "Duración",
|
"duration": "Duración",
|
||||||
"error_devices": "Dispositivos de error",
|
"error_devices": "Dispositivos de error",
|
||||||
@@ -973,6 +1015,7 @@
|
|||||||
"infinite": "infinito",
|
"infinite": "infinito",
|
||||||
"keep_alive": "Mantener viva",
|
"keep_alive": "Mantener viva",
|
||||||
"mac_prefix": "Prefijo MAC",
|
"mac_prefix": "Prefijo MAC",
|
||||||
|
"mac_prefix_length": "Su prefijo MAC debe tener 6 dígitos hexadecimales válidos (p. ej.: 00112233)",
|
||||||
"max_associations": "Max. Asociaciones",
|
"max_associations": "Max. Asociaciones",
|
||||||
"max_clients": "Max. Clientela",
|
"max_clients": "Max. Clientela",
|
||||||
"min_associations": "Min. Asociaciones",
|
"min_associations": "Min. Asociaciones",
|
||||||
@@ -989,6 +1032,7 @@
|
|||||||
"rx_messages": "Mensajes prescritos",
|
"rx_messages": "Mensajes prescritos",
|
||||||
"sim_currently_running": "Simulación \"{{sim}}\" en curso",
|
"sim_currently_running": "Simulación \"{{sim}}\" en curso",
|
||||||
"sim_history": "{{sim}} ejecuciones anteriores",
|
"sim_history": "{{sim}} ejecuciones anteriores",
|
||||||
|
"simulated": "Simulado",
|
||||||
"start": "Iniciar simulación",
|
"start": "Iniciar simulación",
|
||||||
"start_success": "¡Ejecución de simulación iniciada!",
|
"start_success": "¡Ejecución de simulación iniciada!",
|
||||||
"state_interval": "Intervalo de estado",
|
"state_interval": "Intervalo de estado",
|
||||||
@@ -1002,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"last_stats": "Últimas estadísticas",
|
"last_stats": "Últimas estadísticas",
|
||||||
|
"latest": "Últimas estadísticas",
|
||||||
"memory": "Memoria"
|
"memory": "Memoria"
|
||||||
},
|
},
|
||||||
"subscribers": {
|
"subscribers": {
|
||||||
@@ -1022,6 +1067,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Registros de back-end",
|
"backend_logs": "Registros de back-end",
|
||||||
|
"configuration": "Configuración",
|
||||||
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
"could_not_retrieve": "Error: no se pudo recuperar la información del sistema {{name}} ",
|
||||||
"endpoint": "punto final",
|
"endpoint": "punto final",
|
||||||
"hostname": "Nombre de host",
|
"hostname": "Nombre de host",
|
||||||
@@ -1032,6 +1078,10 @@
|
|||||||
"os": "sistema operativo",
|
"os": "sistema operativo",
|
||||||
"processors": "Procesadores",
|
"processors": "Procesadores",
|
||||||
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
|
"reload_chosen_subsystems": "Recargar subsistemas elegidos",
|
||||||
|
"secrets": "Misterios",
|
||||||
|
"secrets_create": "Crear secreto",
|
||||||
|
"secrets_one": "secreto",
|
||||||
|
"services": "Servicios",
|
||||||
"start": "comienzo",
|
"start": "comienzo",
|
||||||
"subsystems": "Subsistemas",
|
"subsystems": "Subsistemas",
|
||||||
"success_reload": "¡Comando de recarga enviado con éxito!",
|
"success_reload": "¡Comando de recarga enviado con éxito!",
|
||||||
@@ -1043,19 +1093,31 @@
|
|||||||
"version": "Versión"
|
"version": "Versión"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"columns": "Columnas",
|
||||||
"columns_hidden_one": "{{count}} columna oculta",
|
"columns_hidden_one": "{{count}} columna oculta",
|
||||||
"columns_hidden_other": "{{count}} columnas ocultas",
|
"columns_hidden_other": "{{count}} columnas ocultas",
|
||||||
|
"display_column": "Monitor",
|
||||||
|
"drag_always_show": "No puede ocultar esta columna pero puede cambiar su posición",
|
||||||
|
"drag_explanation": "Arrastre y suelte para reordenar y cambiar la visibilidad de las columnas",
|
||||||
|
"drag_locked": "Esta columna está bloqueada en su posición.",
|
||||||
|
"export_current_page": "Solo página actual",
|
||||||
"first_page": "Primera pagina",
|
"first_page": "Primera pagina",
|
||||||
"go_to_page": "Ir a la página",
|
"go_to_page": "Ir a la página",
|
||||||
|
"hide_column": "Esconder",
|
||||||
"last_page": "Ultima pagina",
|
"last_page": "Ultima pagina",
|
||||||
"next_page": "Siguiente página",
|
"next_page": "Siguiente página",
|
||||||
"page": "Página",
|
"page": "Página",
|
||||||
"previous_page": "Página anterior"
|
"preferences": "Preferencias de mesa",
|
||||||
|
"previous_page": "Página anterior",
|
||||||
|
"reset": "Reiniciar preferencias",
|
||||||
|
"settings": "Ajustes"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "correo electrónico no validado",
|
||||||
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
"error_fetching": "Error al obtener la información del usuario: {{e}}",
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"role": "papel",
|
"role": "papel",
|
||||||
|
"suspended": "Suspendido",
|
||||||
"title": "Usuario"
|
"title": "Usuario"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1162,11 @@
|
|||||||
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
"successfully_update_devices": "¡Actualizando {{num}} dispositivos!",
|
||||||
"title": "Sedes",
|
"title": "Sedes",
|
||||||
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
"update_all_devices": "Actualizar todas las configuraciones de dispositivos",
|
||||||
"upgrade_all_devices": "Actualice todos los dispositivos al firmware más reciente",
|
"update_success": "Lugar actualizado!",
|
||||||
|
"upgrade_all_devices": "Actualizar el firmware de todos los dispositivos",
|
||||||
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
|
"upgrade_all_devices_error": "Error al actualizar dispositivos: {{e}}",
|
||||||
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
|
"upgrade_all_devices_success": "¡Comenzó con éxito la actualización de dispositivos!",
|
||||||
|
"upgrade_options_available": "Aquí están todas las revisiones disponibles, seleccione la que desea que TODOS los dispositivos de este lugar se actualicen",
|
||||||
"use_existing": "Utilizar existente",
|
"use_existing": "Utilizar existente",
|
||||||
"use_existing_contacts": "Usar contactos existentes",
|
"use_existing_contacts": "Usar contactos existentes",
|
||||||
"use_this_contact": "Usa este contacto"
|
"use_this_contact": "Usa este contacto"
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"health": "santé",
|
"health": "santé",
|
||||||
"inactive": "Inactif",
|
"inactive": "Inactif",
|
||||||
"interval": "Intervalle",
|
"interval": "Intervalle",
|
||||||
|
"last_connected": "Dernière connexion",
|
||||||
|
"last_connected_tooltip": "La dernière fois que cet appareil a été connecté au contrôleur. Cela peut être utilisé pour estimer quand un appareil s'est déconnecté",
|
||||||
"last_connection": "Dernière connexion",
|
"last_connection": "Dernière connexion",
|
||||||
"last_contact": "Dernier contact",
|
"last_contact": "Dernier contact",
|
||||||
"last_disconnection": "Dernière déconnexion",
|
"last_disconnection": "Dernière déconnexion",
|
||||||
@@ -79,8 +81,11 @@
|
|||||||
"live_view_help": "Aide sur l'affichage en direct",
|
"live_view_help": "Aide sur l'affichage en direct",
|
||||||
"memory": "mémoire",
|
"memory": "mémoire",
|
||||||
"memory_used": "Mémoire utilisée",
|
"memory_used": "Mémoire utilisée",
|
||||||
"missing_board": "La surveillance analytique sur ce lieu n'est plus active, veuillez redémarrer la surveillance en utilisant le menu du haut",
|
"missing_board": "La surveillance analytique sur ce site n'est plus active. Cliquez ici pour redémarrer la surveillance",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"monitoring": "surveillance",
|
||||||
|
"no_board": "Aucune surveillance",
|
||||||
|
"no_board_description": "Vous ne surveillez pas ce lieu pour le moment, cliquez ici pour commencer",
|
||||||
"noise": "Bruit",
|
"noise": "Bruit",
|
||||||
"packets": "Paquets",
|
"packets": "Paquets",
|
||||||
"radio": "Radio",
|
"radio": "Radio",
|
||||||
@@ -91,6 +96,8 @@
|
|||||||
"retries": "Tentatives",
|
"retries": "Tentatives",
|
||||||
"search_serials": "Rechercher des publications en série",
|
"search_serials": "Rechercher des publications en série",
|
||||||
"stop_monitoring": "Arrêter la surveillance",
|
"stop_monitoring": "Arrêter la surveillance",
|
||||||
|
"stop_monitoring_success": "Lieu de surveillance arrêté !",
|
||||||
|
"stop_monitoring_warning": "Êtes-vous sûr? Cela effacera toutes les données de surveillance enregistrées pour ce lieu",
|
||||||
"temperature": "Température",
|
"temperature": "Température",
|
||||||
"title": "ANALYTIQUE",
|
"title": "ANALYTIQUE",
|
||||||
"total_data": "Données totales",
|
"total_data": "Données totales",
|
||||||
@@ -175,6 +182,7 @@
|
|||||||
"other": "Les commandes",
|
"other": "Les commandes",
|
||||||
"override_dfs": "Remplacer DFS",
|
"override_dfs": "Remplacer DFS",
|
||||||
"reboot": "Redémarrer",
|
"reboot": "Redémarrer",
|
||||||
|
"reboot_description": "Voulez-vous redémarrer cet appareil ?",
|
||||||
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
|
"reboot_error": "Erreur lors de l'envoi de la commande de redémarrage : {{e}}",
|
||||||
"reboot_success": "Commande de redémarrage envoyée avec succès !",
|
"reboot_success": "Commande de redémarrage envoyée avec succès !",
|
||||||
"revision": "Révision",
|
"revision": "Révision",
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
"error_download": "Erreur lors de la tentative de téléchargement : {{e}}",
|
"error_download": "Erreur lors de la tentative de téléchargement : {{e}}",
|
||||||
"errors": "les erreurs",
|
"errors": "les erreurs",
|
||||||
"exit_fullscreen": "Sortie",
|
"exit_fullscreen": "Sortie",
|
||||||
|
"export": "Exportation",
|
||||||
"finished": "fini",
|
"finished": "fini",
|
||||||
"fullscreen": "Plein écran",
|
"fullscreen": "Plein écran",
|
||||||
"general_error": "Erreur de connexion au serveur. Veuillez consulter votre administrateur.",
|
"general_error": "Erreur de connexion au serveur. Veuillez consulter votre administrateur.",
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
"error_pushes_one": "Erreur (peut être due à une mauvaise configuration) : {{count}}",
|
"error_pushes_one": "Erreur (peut être due à une mauvaise configuration) : {{count}}",
|
||||||
"error_pushes_other": "Erreurs (peut-être dues à une mauvaise configuration) : {{count}}",
|
"error_pushes_other": "Erreurs (peut-être dues à une mauvaise configuration) : {{count}}",
|
||||||
"expert_name": "Mode expert",
|
"expert_name": "Mode expert",
|
||||||
|
"expert_name_explanation": "Vous pouvez utiliser le mode expert pour modifier directement votre configuration, notamment en ajoutant des valeurs qui ne sont pas actuellement prises en charge par l'interface utilisateur. Veuillez utiliser ce format : { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||||
"explanation": "Explication",
|
"explanation": "Explication",
|
||||||
"firewall": "Pare-feu",
|
"firewall": "Pare-feu",
|
||||||
"firmware_upgrade": "Mise à jour du firmware",
|
"firmware_upgrade": "Mise à jour du firmware",
|
||||||
@@ -391,6 +401,7 @@
|
|||||||
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
|
"warning_pushes_one": "En attente de connexion des appareils : {{count}}",
|
||||||
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
|
"warning_pushes_other": "En attente de connexion des appareils : {{count}}",
|
||||||
"weight": "Poids",
|
"weight": "Poids",
|
||||||
|
"wifi_bands_max": "Il ne peut y avoir plus de 8 SSID utilisant cette bande wifi",
|
||||||
"wifi_frames": "Cadres Wi-Fi"
|
"wifi_frames": "Cadres Wi-Fi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -515,6 +526,7 @@
|
|||||||
"ouis_explanation": "OUI des appareils qui se sont connectés à ce serveur de firmware",
|
"ouis_explanation": "OUI des appareils qui se sont connectés à ce serveur de firmware",
|
||||||
"outdated_one": "Micrologiciel vieux de {{count}} jours",
|
"outdated_one": "Micrologiciel vieux de {{count}} jours",
|
||||||
"outdated_other": "Micrologiciel vieux de {{count}} jours",
|
"outdated_other": "Micrologiciel vieux de {{count}} jours",
|
||||||
|
"outdated_unknown": "Firmware d'âge inconnu",
|
||||||
"release": "libération",
|
"release": "libération",
|
||||||
"show_dev_releases": "Versions de développement",
|
"show_dev_releases": "Versions de développement",
|
||||||
"status_explanation": "État de connexion des appareils qui se sont connectés à ce serveur de micrologiciel",
|
"status_explanation": "État de connexion des appareils qui se sont connectés à ce serveur de micrologiciel",
|
||||||
@@ -530,6 +542,16 @@
|
|||||||
"queue": {
|
"queue": {
|
||||||
"title": "File d'attente d'événements"
|
"title": "File d'attente d'événements"
|
||||||
},
|
},
|
||||||
|
"radius": {
|
||||||
|
"calling_station_id": "Station",
|
||||||
|
"disconnect": "déconnecter",
|
||||||
|
"disconnect_success": "Session Radius déconnectée !",
|
||||||
|
"input_octets": "Contribution",
|
||||||
|
"output_octets": "Sortie",
|
||||||
|
"radius_clients": "Clients rayon",
|
||||||
|
"session_time": "Temps de session",
|
||||||
|
"username": "Nom d'utilisateur"
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"load": "Charge (1 | 5 | 15 m.)",
|
"load": "Charge (1 | 5 | 15 m.)",
|
||||||
"seconds_ago": " Il y a {{s}} secondes",
|
"seconds_ago": " Il y a {{s}} secondes",
|
||||||
@@ -600,6 +622,7 @@
|
|||||||
"certificate_expires_in": "Le certificat expire dans",
|
"certificate_expires_in": "Le certificat expire dans",
|
||||||
"certificate_expiry": "Cert. Expire dans",
|
"certificate_expiry": "Cert. Expire dans",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
|
"crash_logs": "Journaux des plantages",
|
||||||
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
"create_errors": "erreurs lors de la tentative de création d'appareils",
|
||||||
"create_success": " appareils créés avec succès",
|
"create_success": " appareils créés avec succès",
|
||||||
"current_firmware": "Firmware actuel",
|
"current_firmware": "Firmware actuel",
|
||||||
@@ -613,6 +636,7 @@
|
|||||||
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
"import_device_warning": "Veuillez vous assurer qu'il n'y a pas d'espaces supplémentaires au début ou à la fin des valeurs, sauf si cela fait partie de la valeur souhaitée",
|
||||||
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Pour importer en masse des appareils, vous devez utiliser un fichier CSV avec les colonnes suivantes : SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
"invalid_serial_number": "Numéro de série non valide (doit être composé de 12 caractères HEX)",
|
||||||
|
"logs_one": "Bûche",
|
||||||
"new_devices": "nouveaux appareils",
|
"new_devices": "nouveaux appareils",
|
||||||
"no_model_image": "Aucune image de modèle trouvée",
|
"no_model_image": "Aucune image de modèle trouvée",
|
||||||
"not_connected": "Pas connecté",
|
"not_connected": "Pas connecté",
|
||||||
@@ -620,7 +644,10 @@
|
|||||||
"notifications": "notifications de l'appareil",
|
"notifications": "notifications de l'appareil",
|
||||||
"one": "Dispositif",
|
"one": "Dispositif",
|
||||||
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
||||||
|
"reboot_logs": "Journaux de redémarrage",
|
||||||
"restricted": "Limité",
|
"restricted": "Limité",
|
||||||
|
"restricted_overriden": "Il s'agit d'un appareil restreint, mais il est en mode développement. Toutes les restrictions sont actuellement ignorées",
|
||||||
|
"restrictions_overriden_title": "Mode développement",
|
||||||
"sanity": "Santé mentale",
|
"sanity": "Santé mentale",
|
||||||
"start_import": "Démarrer l'importation de l'appareil",
|
"start_import": "Démarrer l'importation de l'appareil",
|
||||||
"test_batch": "Tester les données d'importation",
|
"test_batch": "Tester les données d'importation",
|
||||||
@@ -671,7 +698,15 @@
|
|||||||
"test_digicert_creds": "Tester les informations d'identification",
|
"test_digicert_creds": "Tester les informations d'identification",
|
||||||
"title": "Entités",
|
"title": "Entités",
|
||||||
"tree": "Arborescence des entités",
|
"tree": "Arborescence des entités",
|
||||||
"venues_under_root": "Les sites ne peuvent pas être créés directement sous l'entité racine. Veuillez créer de nouvelles entités et créer des lieux sous celles-ci."
|
"update_success": "Entité mise à jour !",
|
||||||
|
"venues_under_root": "Les lieux ne peuvent pas être créés directement sous l'entité racine"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Cette opération se fait automatiquement quotidiennement sans avoir besoin d'utiliser cette mise à jour manuelle. La mise à jour de cette base de données peut prendre jusqu'à 25 minutes",
|
||||||
|
"last_db_update_modal": "Base de données du micrologiciel",
|
||||||
|
"last_db_update_title": "Base de données",
|
||||||
|
"start_db_update": "Démarrer la mise à jour de la base de données",
|
||||||
|
"started_db_update": "Mise à jour de la base de données démarrée, cette opération devrait prendre jusqu'à 25 minutes"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Alimenté par",
|
"powered_by": "Alimenté par",
|
||||||
@@ -693,7 +728,7 @@
|
|||||||
"invalid_ipv6": "Adresse IPv6 invalide (ex. : 2001:db8:3333:4444:5555:6666:7777:8888)",
|
"invalid_ipv6": "Adresse IPv6 invalide (ex. : 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||||
"invalid_json": "Chaîne JSON non valide",
|
"invalid_json": "Chaîne JSON non valide",
|
||||||
"invalid_lease_time": "Valeur de durée de bail non valide ! Ils doivent être au format digitUnit. Par exemple : 6d2h5m, ce qui signifie 6 jours, 2 heures et 5 minutes. Voici les unités acceptées : m, h, d. Si vous ne voulez pas utiliser une unité, omettez-la complètement. Donc au lieu de dire 0d2h0m, utilisez 2h",
|
"invalid_lease_time": "Valeur de durée de bail non valide ! Ils doivent être au format digitUnit. Par exemple : 6d2h5m, ce qui signifie 6 jours, 2 heures et 5 minutes. Voici les unités acceptées : m, h, d. Si vous ne voulez pas utiliser une unité, omettez-la complètement. Donc au lieu de dire 0d2h0m, utilisez 2h",
|
||||||
"invalid_mac_uc": "Valeur UC-MAC non valide, par exemple : 00:00:5e:00:53:af",
|
"invalid_mac_uc": "Valeur MAC non valide, par exemple : 00:00:5e:00:53:af",
|
||||||
"invalid_password": "Mot de passe invalide, veuillez consulter la politique de mot de passe",
|
"invalid_password": "Mot de passe invalide, veuillez consulter la politique de mot de passe",
|
||||||
"invalid_phone_number": "Numéro de téléphone invalide",
|
"invalid_phone_number": "Numéro de téléphone invalide",
|
||||||
"invalid_phone_numbers": "Un ou plusieurs des numéros de téléphone sont invalides. Veuillez les fournir sans symboles ni espaces, ou dans ce format : +1(123)123-1234",
|
"invalid_phone_numbers": "Un ou plusieurs des numéros de téléphone sont invalides. Veuillez les fournir sans symboles ni espaces, ou dans ce format : +1(123)123-1234",
|
||||||
@@ -765,13 +800,17 @@
|
|||||||
"city": "Ville",
|
"city": "Ville",
|
||||||
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
|
"claim_explanation": "Pour revendiquer des emplacements, vous pouvez utiliser le tableau ci-dessous",
|
||||||
"country": "Pays",
|
"country": "Pays",
|
||||||
|
"elevation": "Élévation",
|
||||||
"geocode": "Geo code",
|
"geocode": "Geo code",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Emplacement",
|
"one": "Emplacement",
|
||||||
"other": "Emplacements",
|
"other": "Emplacements",
|
||||||
"postal": "Zip / code postal",
|
"postal": "Zip / code postal",
|
||||||
"state": "Etat / Province",
|
"state": "Etat / Province",
|
||||||
"title": "Emplacements",
|
"title": "Emplacements",
|
||||||
"to_claim": "Emplacements à réclamer"
|
"to_claim": "Emplacements à réclamer",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Politique d'accès",
|
"access_policy": "Politique d'accès",
|
||||||
@@ -797,6 +836,7 @@
|
|||||||
"reset_password": "Réinitialiser le mot de passe",
|
"reset_password": "Réinitialiser le mot de passe",
|
||||||
"sign_in": "se connecter",
|
"sign_in": "se connecter",
|
||||||
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
"sms_instructions": "Vous devriez bientôt recevoir un code à 6 chiffres sur votre téléphone. Veuillez le saisir ci-dessous pour vous connecter",
|
||||||
|
"suspended_error": "Compte suspendu, veuillez contacter votre administrateur",
|
||||||
"verification": "Vérifiez votre connexion",
|
"verification": "Vérifiez votre connexion",
|
||||||
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
"waiting_for_email_verification": "Compte pas encore e-mail validé. Veuillez consulter votre boîte de réception ou demander à votre administrateur de renvoyer une validation",
|
||||||
"welcome_back": "Nous saluons le retour!",
|
"welcome_back": "Nous saluons le retour!",
|
||||||
@@ -902,7 +942,7 @@
|
|||||||
"dfs": "Remplacement DFS",
|
"dfs": "Remplacement DFS",
|
||||||
"gw_commands": "Commandes de passerelle",
|
"gw_commands": "Commandes de passerelle",
|
||||||
"identifier": "Identifiant",
|
"identifier": "Identifiant",
|
||||||
"key_verification": "Vérification de la clé de signature",
|
"key_verification": "Signature des informations clés",
|
||||||
"restricted": "Limité",
|
"restricted": "Limité",
|
||||||
"signed_upgrade": "Mise à niveau signée uniquement",
|
"signed_upgrade": "Mise à niveau signée uniquement",
|
||||||
"title": "Restrictions",
|
"title": "Restrictions",
|
||||||
@@ -966,6 +1006,8 @@
|
|||||||
"concurrent_devices": "Périphériques simultanés",
|
"concurrent_devices": "Périphériques simultanés",
|
||||||
"controller": "Manette",
|
"controller": "Manette",
|
||||||
"current_live_devices": "Appareils en direct actuels",
|
"current_live_devices": "Appareils en direct actuels",
|
||||||
|
"currently_running_one": "Il y a actuellement {{count}} simulation en cours",
|
||||||
|
"currently_running_other": "Il y a actuellement {{count}} simulations en cours d'exécution",
|
||||||
"delete_success": "Simulation supprimée !",
|
"delete_success": "Simulation supprimée !",
|
||||||
"duration": "Durée",
|
"duration": "Durée",
|
||||||
"error_devices": "Périphériques d'erreur",
|
"error_devices": "Périphériques d'erreur",
|
||||||
@@ -973,6 +1015,7 @@
|
|||||||
"infinite": "Infini",
|
"infinite": "Infini",
|
||||||
"keep_alive": "Rester en vie",
|
"keep_alive": "Rester en vie",
|
||||||
"mac_prefix": "Préfixe MAC",
|
"mac_prefix": "Préfixe MAC",
|
||||||
|
"mac_prefix_length": "Votre préfixe MAC doit être valide à 6 chiffres HEX (ex. : 00112233)",
|
||||||
"max_associations": "Max. Les associations",
|
"max_associations": "Max. Les associations",
|
||||||
"max_clients": "Max. Clients",
|
"max_clients": "Max. Clients",
|
||||||
"min_associations": "Min. Les associations",
|
"min_associations": "Min. Les associations",
|
||||||
@@ -989,6 +1032,7 @@
|
|||||||
"rx_messages": "Messages reçus",
|
"rx_messages": "Messages reçus",
|
||||||
"sim_currently_running": "Simulation \"{{sim}}\" en cours",
|
"sim_currently_running": "Simulation \"{{sim}}\" en cours",
|
||||||
"sim_history": "{{sim}} courses précédentes",
|
"sim_history": "{{sim}} courses précédentes",
|
||||||
|
"simulated": "Simulé",
|
||||||
"start": "Démarrer la simulation",
|
"start": "Démarrer la simulation",
|
||||||
"start_success": "Lancement de la simulation !",
|
"start_success": "Lancement de la simulation !",
|
||||||
"state_interval": "Intervalle d'état",
|
"state_interval": "Intervalle d'état",
|
||||||
@@ -1002,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"last_stats": "Dernières statistiques",
|
"last_stats": "Dernières statistiques",
|
||||||
|
"latest": "Dernières statistiques",
|
||||||
"memory": "mémoire"
|
"memory": "mémoire"
|
||||||
},
|
},
|
||||||
"subscribers": {
|
"subscribers": {
|
||||||
@@ -1022,6 +1067,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Journaux principaux",
|
"backend_logs": "Journaux principaux",
|
||||||
|
"configuration": "Configuration",
|
||||||
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
"could_not_retrieve": "Erreur : impossible de récupérer les informations système {{name}} ",
|
||||||
"endpoint": "Point final",
|
"endpoint": "Point final",
|
||||||
"hostname": "nom d'hôte",
|
"hostname": "nom d'hôte",
|
||||||
@@ -1032,6 +1078,10 @@
|
|||||||
"os": "Système opérateur",
|
"os": "Système opérateur",
|
||||||
"processors": "Processeurs",
|
"processors": "Processeurs",
|
||||||
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
|
"reload_chosen_subsystems": "Recharger les sous-systèmes choisis",
|
||||||
|
"secrets": "Secrets",
|
||||||
|
"secrets_create": "Créer un secret",
|
||||||
|
"secrets_one": "Secret",
|
||||||
|
"services": "Prestations de service",
|
||||||
"start": "Début",
|
"start": "Début",
|
||||||
"subsystems": "Sous-systèmes",
|
"subsystems": "Sous-systèmes",
|
||||||
"success_reload": "Commande de rechargement envoyée avec succès !",
|
"success_reload": "Commande de rechargement envoyée avec succès !",
|
||||||
@@ -1043,19 +1093,31 @@
|
|||||||
"version": "Version"
|
"version": "Version"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"columns": "Les colonnes",
|
||||||
"columns_hidden_one": "{{count}} Colonne masquée",
|
"columns_hidden_one": "{{count}} Colonne masquée",
|
||||||
"columns_hidden_other": "{{count}} colonnes masquées",
|
"columns_hidden_other": "{{count}} colonnes masquées",
|
||||||
|
"display_column": "Afficher",
|
||||||
|
"drag_always_show": "Vous ne pouvez pas masquer cette colonne, mais vous pouvez modifier sa position",
|
||||||
|
"drag_explanation": "Glisser-déposer pour réorganiser et modifier la visibilité des colonnes",
|
||||||
|
"drag_locked": "Cette colonne est verrouillée dans sa position",
|
||||||
|
"export_current_page": "Page actuelle uniquement",
|
||||||
"first_page": "Première page",
|
"first_page": "Première page",
|
||||||
"go_to_page": "Aller à la page",
|
"go_to_page": "Aller à la page",
|
||||||
|
"hide_column": "Cacher",
|
||||||
"last_page": "Dernière page",
|
"last_page": "Dernière page",
|
||||||
"next_page": "Page suivante",
|
"next_page": "Page suivante",
|
||||||
"page": "Page",
|
"page": "Page",
|
||||||
"previous_page": "Page précédente"
|
"preferences": "Préférences de tableau",
|
||||||
|
"previous_page": "Page précédente",
|
||||||
|
"reset": "Remettre à zéro les préférences",
|
||||||
|
"settings": "Réglages"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "Mail non valide",
|
||||||
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
"error_fetching": "Erreur lors de la récupération des informations utilisateur : {{e}}",
|
||||||
"password": "Mot de passe",
|
"password": "Mot de passe",
|
||||||
"role": "Rôle",
|
"role": "Rôle",
|
||||||
|
"suspended": "Suspendu",
|
||||||
"title": "Utilisateur"
|
"title": "Utilisateur"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1162,11 @@
|
|||||||
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
"successfully_update_devices": "Mise à jour de {{num}} appareils !",
|
||||||
"title": "Les lieux",
|
"title": "Les lieux",
|
||||||
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
"update_all_devices": "Mettre à jour toutes les configurations de périphérique",
|
||||||
"upgrade_all_devices": "Mettre à niveau tous les appareils vers le dernier micrologiciel",
|
"update_success": "Lieu mis à jour !",
|
||||||
|
"upgrade_all_devices": "Mettre à niveau le micrologiciel de tous les appareils",
|
||||||
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
|
"upgrade_all_devices_error": "Erreur lors de la mise à jour des appareils : {{e}}",
|
||||||
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
|
"upgrade_all_devices_success": "La mise à niveau des appareils a démarré avec succès !",
|
||||||
|
"upgrade_options_available": "Voici toutes les révisions disponibles, veuillez sélectionner celle vers laquelle vous souhaitez que TOUS les appareils de ce lieu soient mis à niveau",
|
||||||
"use_existing": "Utiliser l'existant",
|
"use_existing": "Utiliser l'existant",
|
||||||
"use_existing_contacts": "Utiliser les contacts existants",
|
"use_existing_contacts": "Utiliser les contacts existants",
|
||||||
"use_this_contact": "Utilisez ce contact"
|
"use_this_contact": "Utilisez ce contact"
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
"health": "Saúde",
|
"health": "Saúde",
|
||||||
"inactive": "Inativo",
|
"inactive": "Inativo",
|
||||||
"interval": "intervalo",
|
"interval": "intervalo",
|
||||||
|
"last_connected": "última conexão",
|
||||||
|
"last_connected_tooltip": "Última vez que este dispositivo foi conectado ao controlador. Isso pode ser usado para estimar quando um dispositivo desconectado",
|
||||||
"last_connection": "última conexão",
|
"last_connection": "última conexão",
|
||||||
"last_contact": "Último contato",
|
"last_contact": "Último contato",
|
||||||
"last_disconnection": "Última desconexão",
|
"last_disconnection": "Última desconexão",
|
||||||
@@ -79,8 +81,11 @@
|
|||||||
"live_view_help": "Ajuda da visualização ao vivo",
|
"live_view_help": "Ajuda da visualização ao vivo",
|
||||||
"memory": "Memória",
|
"memory": "Memória",
|
||||||
"memory_used": "Memória Usada",
|
"memory_used": "Memória Usada",
|
||||||
"missing_board": "O monitoramento analítico neste local não está mais ativo, reinicie o monitoramento usando o menu superior",
|
"missing_board": "O monitoramento analítico neste local não está mais ativo. Clique aqui para reiniciar o monitoramento",
|
||||||
"mode": "Modo",
|
"mode": "Modo",
|
||||||
|
"monitoring": "Monitoramento",
|
||||||
|
"no_board": "Sem monitoramento",
|
||||||
|
"no_board_description": "Você não está monitorando este local no momento, clique aqui para começar",
|
||||||
"noise": "Barulho",
|
"noise": "Barulho",
|
||||||
"packets": "Pacotes",
|
"packets": "Pacotes",
|
||||||
"radio": "Rádio",
|
"radio": "Rádio",
|
||||||
@@ -91,6 +96,8 @@
|
|||||||
"retries": "Novas tentativas",
|
"retries": "Novas tentativas",
|
||||||
"search_serials": "Pesquisar séries",
|
"search_serials": "Pesquisar séries",
|
||||||
"stop_monitoring": "Parar o monitoramento",
|
"stop_monitoring": "Parar o monitoramento",
|
||||||
|
"stop_monitoring_success": "Local de monitoramento interrompido!",
|
||||||
|
"stop_monitoring_warning": "Tem certeza? Isso apagará todos os dados de monitoramento gravados para este local",
|
||||||
"temperature": "Temperatura",
|
"temperature": "Temperatura",
|
||||||
"title": "Analytics",
|
"title": "Analytics",
|
||||||
"total_data": "Dados totais",
|
"total_data": "Dados totais",
|
||||||
@@ -175,6 +182,7 @@
|
|||||||
"other": "comandos",
|
"other": "comandos",
|
||||||
"override_dfs": "Substituir DFS",
|
"override_dfs": "Substituir DFS",
|
||||||
"reboot": "Reiniciar",
|
"reboot": "Reiniciar",
|
||||||
|
"reboot_description": "Deseja reiniciar este dispositivo?",
|
||||||
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
|
"reboot_error": "Erro ao enviar o comando de reinicialização: {{e}}",
|
||||||
"reboot_success": "Comando de reinicialização enviado com sucesso!",
|
"reboot_success": "Comando de reinicialização enviado com sucesso!",
|
||||||
"revision": "revisão",
|
"revision": "revisão",
|
||||||
@@ -235,6 +243,7 @@
|
|||||||
"error_download": "Erro ao tentar fazer o download: {{e}}",
|
"error_download": "Erro ao tentar fazer o download: {{e}}",
|
||||||
"errors": "Erros",
|
"errors": "Erros",
|
||||||
"exit_fullscreen": "Saída",
|
"exit_fullscreen": "Saída",
|
||||||
|
"export": "Exportar",
|
||||||
"finished": "acabado",
|
"finished": "acabado",
|
||||||
"fullscreen": "Tela cheia",
|
"fullscreen": "Tela cheia",
|
||||||
"general_error": "Erro ao se conectar ao servidor. Consulte seu administrador.",
|
"general_error": "Erro ao se conectar ao servidor. Consulte seu administrador.",
|
||||||
@@ -350,6 +359,7 @@
|
|||||||
"error_pushes_one": "Erro (pode ser devido à configuração incorreta): {{count}}",
|
"error_pushes_one": "Erro (pode ser devido à configuração incorreta): {{count}}",
|
||||||
"error_pushes_other": "Erros (podem ser devido à configuração incorreta): {{count}}",
|
"error_pushes_other": "Erros (podem ser devido à configuração incorreta): {{count}}",
|
||||||
"expert_name": "MODO EXPERT",
|
"expert_name": "MODO EXPERT",
|
||||||
|
"expert_name_explanation": "Você pode usar o modo especialista para modificar diretamente sua configuração, incluindo a adição de valores que não são atualmente suportados pela interface do usuário. Use este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||||
"explanation": "Explicação",
|
"explanation": "Explicação",
|
||||||
"firewall": "Firewall",
|
"firewall": "Firewall",
|
||||||
"firmware_upgrade": "Atualização de firmware",
|
"firmware_upgrade": "Atualização de firmware",
|
||||||
@@ -391,6 +401,7 @@
|
|||||||
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
|
"warning_pushes_one": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||||
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
|
"warning_pushes_other": "Aguardando a conexão dos dispositivos: {{count}}",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
"wifi_bands_max": "Não pode haver mais de 8 SSIDs usando esta banda wi-fi",
|
||||||
"wifi_frames": "Quadros WiFi"
|
"wifi_frames": "Quadros WiFi"
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
@@ -515,6 +526,7 @@
|
|||||||
"ouis_explanation": "OUIs de dispositivos que se conectaram a este servidor de firmware",
|
"ouis_explanation": "OUIs de dispositivos que se conectaram a este servidor de firmware",
|
||||||
"outdated_one": "Firmware com {{count}} dias",
|
"outdated_one": "Firmware com {{count}} dias",
|
||||||
"outdated_other": "Firmware com {{count}} dias",
|
"outdated_other": "Firmware com {{count}} dias",
|
||||||
|
"outdated_unknown": "Firmware de idade desconhecida",
|
||||||
"release": "LANÇAMENTO",
|
"release": "LANÇAMENTO",
|
||||||
"show_dev_releases": "Lançamentos do desenvolvedor",
|
"show_dev_releases": "Lançamentos do desenvolvedor",
|
||||||
"status_explanation": "Status da conexão dos dispositivos que se conectaram a este servidor de firmware",
|
"status_explanation": "Status da conexão dos dispositivos que se conectaram a este servidor de firmware",
|
||||||
@@ -530,6 +542,16 @@
|
|||||||
"queue": {
|
"queue": {
|
||||||
"title": "Fila de Eventos"
|
"title": "Fila de Eventos"
|
||||||
},
|
},
|
||||||
|
"radius": {
|
||||||
|
"calling_station_id": "estação",
|
||||||
|
"disconnect": "Desconectar",
|
||||||
|
"disconnect_success": "Sessão Radius desconectada!",
|
||||||
|
"input_octets": "Entrada",
|
||||||
|
"output_octets": "Saída",
|
||||||
|
"radius_clients": "Clientes Radius",
|
||||||
|
"session_time": "Tempo de sessão",
|
||||||
|
"username": "Nome de usuário"
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"load": "Carga (1 | 5 | 15 m.)",
|
"load": "Carga (1 | 5 | 15 m.)",
|
||||||
"seconds_ago": "{{s}} segundos atrás",
|
"seconds_ago": "{{s}} segundos atrás",
|
||||||
@@ -600,6 +622,7 @@
|
|||||||
"certificate_expires_in": "Certificado expira em",
|
"certificate_expires_in": "Certificado expira em",
|
||||||
"certificate_expiry": "Certificado expira em",
|
"certificate_expiry": "Certificado expira em",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"crash_logs": "Registros de falhas",
|
||||||
"create_errors": "erros ao tentar criar dispositivos",
|
"create_errors": "erros ao tentar criar dispositivos",
|
||||||
"create_success": " dispositivos criados com sucesso",
|
"create_success": " dispositivos criados com sucesso",
|
||||||
"current_firmware": "Firmware atual",
|
"current_firmware": "Firmware atual",
|
||||||
@@ -613,6 +636,7 @@
|
|||||||
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
"import_device_warning": "Certifique-se de que não há espaços extras no início ou no final de nenhum valor, a menos que faça parte do valor desejado",
|
||||||
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
"import_explanation": "Para importar dispositivos em massa, você precisa usar um arquivo CSV com as seguintes colunas: SerialNumber, DeviceType, Name, Description, Note",
|
||||||
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
"invalid_serial_number": "Número de série inválido (precisa ter 12 caracteres HEX)",
|
||||||
|
"logs_one": "Registro",
|
||||||
"new_devices": "novos dispositivos",
|
"new_devices": "novos dispositivos",
|
||||||
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
"no_model_image": "Nenhuma imagem de modelo encontrada",
|
||||||
"not_connected": "Não conectado",
|
"not_connected": "Não conectado",
|
||||||
@@ -620,7 +644,10 @@
|
|||||||
"notifications": "Notificações do dispositivo",
|
"notifications": "Notificações do dispositivo",
|
||||||
"one": "Dispositivo",
|
"one": "Dispositivo",
|
||||||
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
||||||
|
"reboot_logs": "Registros de reinicialização",
|
||||||
"restricted": "Restrito",
|
"restricted": "Restrito",
|
||||||
|
"restricted_overriden": "Este é um dispositivo restrito, mas está em modo de desenvolvimento. Todas as restrições são atualmente ignoradas",
|
||||||
|
"restrictions_overriden_title": "Modo de desenvolvedor",
|
||||||
"sanity": "Sanidade",
|
"sanity": "Sanidade",
|
||||||
"start_import": "Iniciar importação de dispositivos",
|
"start_import": "Iniciar importação de dispositivos",
|
||||||
"test_batch": "Dados de importação de teste",
|
"test_batch": "Dados de importação de teste",
|
||||||
@@ -671,7 +698,15 @@
|
|||||||
"test_digicert_creds": "Credenciais de teste",
|
"test_digicert_creds": "Credenciais de teste",
|
||||||
"title": "Entidades",
|
"title": "Entidades",
|
||||||
"tree": "Árvore de entidades",
|
"tree": "Árvore de entidades",
|
||||||
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz. Por favor, crie novas entidades e crie locais sob elas."
|
"update_success": "Entidade atualizada!",
|
||||||
|
"venues_under_root": "Os locais não podem ser criados diretamente na entidade raiz"
|
||||||
|
},
|
||||||
|
"firmware": {
|
||||||
|
"db_update_warning": "Esta operação é feita automaticamente diariamente sem necessidade de usar esta atualização manual. A atualização deste banco de dados pode levar até 25 minutos",
|
||||||
|
"last_db_update_modal": "banco de dados de firmware",
|
||||||
|
"last_db_update_title": "base de dados",
|
||||||
|
"start_db_update": "Iniciar atualização do banco de dados",
|
||||||
|
"started_db_update": "Atualização do banco de dados iniciada, esta operação deve levar até 25 minutos para ser concluída"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"powered_by": "Distribuído por",
|
"powered_by": "Distribuído por",
|
||||||
@@ -693,7 +728,7 @@
|
|||||||
"invalid_ipv6": "Endereço IPv6 inválido (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
"invalid_ipv6": "Endereço IPv6 inválido (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||||
"invalid_json": "Sequência JSON inválida",
|
"invalid_json": "Sequência JSON inválida",
|
||||||
"invalid_lease_time": "Valor de tempo de locação inválido! Eles precisam estar no formato digitUnit. Por exemplo: 6d2h5m, que significa 6 dias, 2 horas e 5 minutos. Aqui estão as unidades aceitas: m, h, d. Se você não quiser usar uma unidade, omita-a completamente. Então, em vez de dizer 0d2h0m, use 2h",
|
"invalid_lease_time": "Valor de tempo de locação inválido! Eles precisam estar no formato digitUnit. Por exemplo: 6d2h5m, que significa 6 dias, 2 horas e 5 minutos. Aqui estão as unidades aceitas: m, h, d. Se você não quiser usar uma unidade, omita-a completamente. Então, em vez de dizer 0d2h0m, use 2h",
|
||||||
"invalid_mac_uc": "Valor UC-MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
"invalid_mac_uc": "Valor MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
||||||
"invalid_password": "Senha inválida, consulte a política de senha",
|
"invalid_password": "Senha inválida, consulte a política de senha",
|
||||||
"invalid_phone_number": "Número de telefone inválido",
|
"invalid_phone_number": "Número de telefone inválido",
|
||||||
"invalid_phone_numbers": "Um ou mais números de telefone são inválidos. Forneça-os sem símbolos e espaços ou neste formato: +1(123)123-1234",
|
"invalid_phone_numbers": "Um ou mais números de telefone são inválidos. Forneça-os sem símbolos e espaços ou neste formato: +1(123)123-1234",
|
||||||
@@ -765,13 +800,17 @@
|
|||||||
"city": "Cidade",
|
"city": "Cidade",
|
||||||
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
|
"claim_explanation": "Para reivindicar locais, você pode usar a tabela abaixo",
|
||||||
"country": "País",
|
"country": "País",
|
||||||
|
"elevation": "elevação",
|
||||||
"geocode": "Código geográfico",
|
"geocode": "Código geográfico",
|
||||||
|
"lat": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
"one": "Localização",
|
"one": "Localização",
|
||||||
"other": "Localizações",
|
"other": "Localizações",
|
||||||
"postal": "CEP / Código Postal",
|
"postal": "CEP / Código Postal",
|
||||||
"state": "Estado / Província",
|
"state": "Estado / Província",
|
||||||
"title": "Localizações",
|
"title": "Localizações",
|
||||||
"to_claim": "Locais para reivindicar"
|
"to_claim": "Locais para reivindicar",
|
||||||
|
"view_gps": ""
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"access_policy": "Política de Acesso",
|
"access_policy": "Política de Acesso",
|
||||||
@@ -797,6 +836,7 @@
|
|||||||
"reset_password": "Redefinir senha",
|
"reset_password": "Redefinir senha",
|
||||||
"sign_in": "assinar em",
|
"sign_in": "assinar em",
|
||||||
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
"sms_instructions": "Você deve receber um código de 6 dígitos em seu telefone em breve. Por favor, insira-o abaixo para fazer login",
|
||||||
|
"suspended_error": "Conta suspensa, entre em contato com seu administrador",
|
||||||
"verification": "Verifique seu login",
|
"verification": "Verifique seu login",
|
||||||
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
"waiting_for_email_verification": "Conta ainda não validada por e-mail. Verifique sua caixa de entrada ou peça ao administrador para reenviar uma validação",
|
||||||
"welcome_back": "Bem vindo de volta!",
|
"welcome_back": "Bem vindo de volta!",
|
||||||
@@ -902,7 +942,7 @@
|
|||||||
"dfs": "Substituição DFS",
|
"dfs": "Substituição DFS",
|
||||||
"gw_commands": "Comandos de gateway",
|
"gw_commands": "Comandos de gateway",
|
||||||
"identifier": "Identificador",
|
"identifier": "Identificador",
|
||||||
"key_verification": "Verificação da chave de assinatura",
|
"key_verification": "Informações Chave de Assinatura",
|
||||||
"restricted": "Restrito",
|
"restricted": "Restrito",
|
||||||
"signed_upgrade": "Somente atualização assinada",
|
"signed_upgrade": "Somente atualização assinada",
|
||||||
"title": "RESTRIÇÕES",
|
"title": "RESTRIÇÕES",
|
||||||
@@ -966,6 +1006,8 @@
|
|||||||
"concurrent_devices": "Dispositivos Simultâneos",
|
"concurrent_devices": "Dispositivos Simultâneos",
|
||||||
"controller": "Controlador",
|
"controller": "Controlador",
|
||||||
"current_live_devices": "Dispositivos ativos atuais",
|
"current_live_devices": "Dispositivos ativos atuais",
|
||||||
|
"currently_running_one": "Atualmente, há {{count}} simulação em execução",
|
||||||
|
"currently_running_other": "Existem atualmente {{count}} simulações em execução",
|
||||||
"delete_success": "Simulação excluída!",
|
"delete_success": "Simulação excluída!",
|
||||||
"duration": "Duração",
|
"duration": "Duração",
|
||||||
"error_devices": "Dispositivos de Erro",
|
"error_devices": "Dispositivos de Erro",
|
||||||
@@ -973,6 +1015,7 @@
|
|||||||
"infinite": "Infinito",
|
"infinite": "Infinito",
|
||||||
"keep_alive": "Mantenha vivo",
|
"keep_alive": "Mantenha vivo",
|
||||||
"mac_prefix": "Prefixo MAC",
|
"mac_prefix": "Prefixo MAC",
|
||||||
|
"mac_prefix_length": "Seu prefixo MAC precisa ter 6 dígitos HEX válidos (ex.: 00112233)",
|
||||||
"max_associations": "Máx. Associações",
|
"max_associations": "Máx. Associações",
|
||||||
"max_clients": "Máx. Clientes",
|
"max_clients": "Máx. Clientes",
|
||||||
"min_associations": "Min. Associações",
|
"min_associations": "Min. Associações",
|
||||||
@@ -989,6 +1032,7 @@
|
|||||||
"rx_messages": "Mensagens Rx",
|
"rx_messages": "Mensagens Rx",
|
||||||
"sim_currently_running": "Simulação \"{{sim}}\" em andamento",
|
"sim_currently_running": "Simulação \"{{sim}}\" em andamento",
|
||||||
"sim_history": "{{sim}} execuções anteriores",
|
"sim_history": "{{sim}} execuções anteriores",
|
||||||
|
"simulated": "Simulado",
|
||||||
"start": "Iniciar simulação",
|
"start": "Iniciar simulação",
|
||||||
"start_success": "Corrida de simulação iniciada!",
|
"start_success": "Corrida de simulação iniciada!",
|
||||||
"state_interval": "Intervalo de estado",
|
"state_interval": "Intervalo de estado",
|
||||||
@@ -1002,6 +1046,7 @@
|
|||||||
},
|
},
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"last_stats": "Últimas estatísticas",
|
"last_stats": "Últimas estatísticas",
|
||||||
|
"latest": "Estatísticas mais recentes",
|
||||||
"memory": "Memória"
|
"memory": "Memória"
|
||||||
},
|
},
|
||||||
"subscribers": {
|
"subscribers": {
|
||||||
@@ -1022,6 +1067,7 @@
|
|||||||
},
|
},
|
||||||
"system": {
|
"system": {
|
||||||
"backend_logs": "Registros de back-end",
|
"backend_logs": "Registros de back-end",
|
||||||
|
"configuration": "Configuração",
|
||||||
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
"could_not_retrieve": "Erro: não foi possível recuperar {{name}} informações do sistema",
|
||||||
"endpoint": "Ponto final",
|
"endpoint": "Ponto final",
|
||||||
"hostname": "Nome de anfitrião",
|
"hostname": "Nome de anfitrião",
|
||||||
@@ -1032,6 +1078,10 @@
|
|||||||
"os": "Sistema Operacional",
|
"os": "Sistema Operacional",
|
||||||
"processors": "Processadores",
|
"processors": "Processadores",
|
||||||
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
|
"reload_chosen_subsystems": "Recarregar Subsistemas Escolhidos",
|
||||||
|
"secrets": "Segredos",
|
||||||
|
"secrets_create": "Criar Segredo",
|
||||||
|
"secrets_one": "Segredo",
|
||||||
|
"services": "Serviços",
|
||||||
"start": "Começar",
|
"start": "Começar",
|
||||||
"subsystems": "Subsistemas",
|
"subsystems": "Subsistemas",
|
||||||
"success_reload": "Comando de recarga enviado com sucesso!",
|
"success_reload": "Comando de recarga enviado com sucesso!",
|
||||||
@@ -1043,19 +1093,31 @@
|
|||||||
"version": "Versão"
|
"version": "Versão"
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
"columns": "Colunas",
|
||||||
"columns_hidden_one": "{{count}} Coluna oculta",
|
"columns_hidden_one": "{{count}} Coluna oculta",
|
||||||
"columns_hidden_other": "{{count}} Colunas ocultas",
|
"columns_hidden_other": "{{count}} Colunas ocultas",
|
||||||
|
"display_column": "Exibição",
|
||||||
|
"drag_always_show": "Você não pode ocultar esta coluna, mas pode alterar sua posição",
|
||||||
|
"drag_explanation": "Arraste e solte para reordenar e alterar a visibilidade da coluna",
|
||||||
|
"drag_locked": "Esta coluna está travada em sua posição",
|
||||||
|
"export_current_page": "Somente página atual",
|
||||||
"first_page": "Primeira Página",
|
"first_page": "Primeira Página",
|
||||||
"go_to_page": "Vá para página",
|
"go_to_page": "Vá para página",
|
||||||
|
"hide_column": "Ocultar",
|
||||||
"last_page": "Última Página",
|
"last_page": "Última Página",
|
||||||
"next_page": "Próxima página",
|
"next_page": "Próxima página",
|
||||||
"page": "Página",
|
"page": "Página",
|
||||||
"previous_page": "Página anterior"
|
"preferences": "Preferências de Tabela",
|
||||||
|
"previous_page": "Página anterior",
|
||||||
|
"reset": "Reiniciar preferências",
|
||||||
|
"settings": "Definições"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"email_not_validated": "e-mail não validado",
|
||||||
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
"error_fetching": "Erro ao buscar informações do usuário: {{e}}",
|
||||||
"password": "Senha",
|
"password": "Senha",
|
||||||
"role": "Função",
|
"role": "Função",
|
||||||
|
"suspended": "Suspenso",
|
||||||
"title": "Do utilizador"
|
"title": "Do utilizador"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
@@ -1100,9 +1162,11 @@
|
|||||||
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
"successfully_update_devices": "Atualizando {{num}} dispositivos!",
|
||||||
"title": "Locais",
|
"title": "Locais",
|
||||||
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
"update_all_devices": "Atualizar todas as configurações do dispositivo",
|
||||||
"upgrade_all_devices": "Atualize todos os dispositivos para o firmware mais recente",
|
"update_success": "Local atualizado!",
|
||||||
|
"upgrade_all_devices": "Atualize o firmware de todos os dispositivos",
|
||||||
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
|
"upgrade_all_devices_error": "Erro ao atualizar dispositivos: {{e}}",
|
||||||
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
|
"upgrade_all_devices_success": "Atualização de dispositivos iniciada com sucesso!",
|
||||||
|
"upgrade_options_available": "Aqui estão todas as revisões disponíveis, selecione aquela para a qual você deseja que TODOS os dispositivos deste local sejam atualizados",
|
||||||
"use_existing": "Usar existente",
|
"use_existing": "Usar existente",
|
||||||
"use_existing_contacts": "Usar contatos existentes",
|
"use_existing_contacts": "Usar contatos existentes",
|
||||||
"use_this_contact": "Use este contato"
|
"use_this_contact": "Use este contato"
|
||||||
|
|||||||
24
src/@tanstack.react-table.d.ts
vendored
Normal file
24
src/@tanstack.react-table.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { BoxProps } from '@chakra-ui/react';
|
||||||
|
import '@tanstack/react-table';
|
||||||
|
|
||||||
|
declare module '@tanstack/table-core' {
|
||||||
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
|
stopPropagation?: boolean;
|
||||||
|
alwaysShow?: boolean;
|
||||||
|
anchored?: boolean;
|
||||||
|
hasPopover?: boolean;
|
||||||
|
customMaxWidth?: string;
|
||||||
|
customMinWidth?: string;
|
||||||
|
customWidth?: string;
|
||||||
|
isMonospace?: boolean;
|
||||||
|
isCentered?: boolean;
|
||||||
|
columnSelectorOptions?: {
|
||||||
|
label?: string;
|
||||||
|
};
|
||||||
|
headerOptions?: {
|
||||||
|
tooltip?: string;
|
||||||
|
};
|
||||||
|
headerStyleProps?: BoxProps;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { Warning } from 'phosphor-react';
|
import { Warning } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ThemeProps } from 'models/Theme';
|
import { ThemeProps } from 'models/Theme';
|
||||||
|
|
||||||
@@ -12,7 +12,14 @@ export interface AlertButtonProps extends ThemeProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _AlertButton: React.FC<AlertButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
const _AlertButton: React.FC<AlertButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconButton, SpaceProps } from '@chakra-ui/react';
|
import { IconButton, SpaceProps } from '@chakra-ui/react';
|
||||||
import { X } from 'phosphor-react';
|
import { X } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface CloseButtonProps extends SpaceProps {
|
export interface CloseButtonProps extends SpaceProps {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint, SpaceProps } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint, SpaceProps } from '@chakra-ui/react';
|
||||||
import { Plus } from 'phosphor-react';
|
import { Plus } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface CreateButtonProps extends SpaceProps {
|
export interface CreateButtonProps extends SpaceProps {
|
||||||
@@ -11,7 +11,14 @@ export interface CreateButtonProps extends SpaceProps {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CreateButton: React.FC<CreateButtonProps> = ({ onClick, isDisabled, isLoading, isCompact, label, ...props }) => {
|
const _CreateButton: React.FC<CreateButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
label,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { Trash } from 'phosphor-react';
|
import { Trash } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface DeleteButtonProps {
|
export interface DeleteButtonProps {
|
||||||
@@ -16,7 +16,7 @@ const _DeleteButton: React.FC<DeleteButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
label,
|
label,
|
||||||
ml,
|
ml,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Menu, MenuButton, MenuItem, MenuList, Spinner, Tooltip, useToast } from '@chakra-ui/react';
|
import {
|
||||||
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Portal,
|
||||||
|
Tooltip,
|
||||||
|
useColorModeValue,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Wrench } from 'phosphor-react';
|
import { Barcode, Power, TerminalWindow, WifiHigh, Wrench } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import RebootMenuItem from './RebootButton';
|
|
||||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
|
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
|
||||||
import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware';
|
import { useUpdateDeviceToLatest } from 'hooks/Network/Firmware';
|
||||||
@@ -22,6 +31,7 @@ interface Props {
|
|||||||
onOpenConfigureModal: (serialNumber: string) => void;
|
onOpenConfigureModal: (serialNumber: string) => void;
|
||||||
onOpenTelemetryModal: (serialNumber: string) => void;
|
onOpenTelemetryModal: (serialNumber: string) => void;
|
||||||
onOpenScriptModal: (device: GatewayDevice) => void;
|
onOpenScriptModal: (device: GatewayDevice) => void;
|
||||||
|
onOpenRebootModal: (serialNumber: string) => void;
|
||||||
size?: 'sm' | 'md' | 'lg';
|
size?: 'sm' | 'md' | 'lg';
|
||||||
isCompact?: boolean;
|
isCompact?: boolean;
|
||||||
}
|
}
|
||||||
@@ -38,13 +48,15 @@ const DeviceActionDropdown = ({
|
|||||||
onOpenTelemetryModal,
|
onOpenTelemetryModal,
|
||||||
onOpenConfigureModal,
|
onOpenConfigureModal,
|
||||||
onOpenScriptModal,
|
onOpenScriptModal,
|
||||||
|
onOpenRebootModal,
|
||||||
size,
|
size,
|
||||||
isCompact,
|
isCompact,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const connectColor = useColorModeValue('blackAlpha', 'gray');
|
||||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||||
const { refetch: getRtty, isInitialLoading: isRtty } = useGetDeviceRtty({
|
const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({
|
||||||
serialNumber: device.serialNumber,
|
serialNumber: device.serialNumber,
|
||||||
extraId: 'inventory-modal',
|
extraId: 'inventory-modal',
|
||||||
});
|
});
|
||||||
@@ -145,50 +157,95 @@ const DeviceActionDropdown = ({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConnectClick = () => getRtty();
|
const handleConnectClick = () => getRtty();
|
||||||
|
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<>
|
||||||
<Tooltip label={t('commands.other')}>
|
<Tooltip label={t('commands.connect')}>
|
||||||
{size === undefined || isCompact ? (
|
<IconButton
|
||||||
|
aria-label="Connect"
|
||||||
|
icon={<TerminalWindow size={20} />}
|
||||||
|
size={size ?? 'sm'}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isLoading={isRtty}
|
||||||
|
onClick={handleConnectClick}
|
||||||
|
colorScheme={connectColor}
|
||||||
|
hidden={isCompact}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('controller.configure.title')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('controller.configure.title')}
|
||||||
|
icon={<Barcode size={20} />}
|
||||||
|
size={size ?? 'sm'}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={handleOpenConfigure}
|
||||||
|
colorScheme="purple"
|
||||||
|
hidden={isCompact}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('commands.reboot')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('commands.reboot')}
|
||||||
|
icon={<Power size={20} />}
|
||||||
|
size={size ?? 'sm'}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={handleRebootClick}
|
||||||
|
colorScheme="green"
|
||||||
|
hidden={isCompact}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('commands.wifiscan')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('commands.wifiscan')}
|
||||||
|
icon={<WifiHigh size={20} />}
|
||||||
|
size={size ?? 'sm'}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={handleOpenScan}
|
||||||
|
colorScheme="teal"
|
||||||
|
hidden={isCompact}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Menu>
|
||||||
|
<Tooltip label={t('common.actions')}>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label="Commands"
|
aria-label="Commands"
|
||||||
icon={isRtty ? <Spinner /> : <Wrench size={20} />}
|
icon={<Wrench size={20} />}
|
||||||
size={size ?? 'sm'}
|
size={size ?? 'sm'}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
ml={2}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</Tooltip>
|
||||||
<MenuButton
|
<Portal>
|
||||||
as={Button}
|
<MenuList maxH="315px" overflowY="auto">
|
||||||
aria-label="Commands"
|
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||||
rightIcon={isRtty ? <Spinner /> : <Wrench size={20} />}
|
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact}>
|
||||||
size={size ?? 'sm'}
|
{t('controller.configure.title')}
|
||||||
isDisabled={isDisabled}
|
</MenuItem>
|
||||||
ml={2}
|
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
|
||||||
>
|
{t('commands.connect')}
|
||||||
{t('commands.other')}
|
</MenuItem>
|
||||||
</MenuButton>
|
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
||||||
)}
|
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
||||||
</Tooltip>
|
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
||||||
<MenuList>
|
<MenuItem onClick={handleRebootClick} hidden={!isCompact}>
|
||||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
{t('commands.reboot')}
|
||||||
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||||
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||||
<RebootMenuItem device={device} refresh={refresh} />
|
{t('premium.toolbox.upgrade_to_latest')}
|
||||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
<MenuItem onClick={handleOpenScan} hidden={!isCompact}>
|
||||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
{t('commands.wifiscan')}
|
||||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
</MenuItem>
|
||||||
{t('premium.toolbox.upgrade_to_latest')}
|
</MenuList>
|
||||||
</MenuItem>
|
</Portal>
|
||||||
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
</Menu>
|
||||||
</MenuList>
|
</>
|
||||||
</Menu>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { Pen } from 'phosphor-react';
|
import { Pen } from '@phosphor-icons/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface EditButtonProps {
|
export interface EditButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@@ -11,7 +12,15 @@ export interface EditButtonProps {
|
|||||||
ml?: string | number;
|
ml?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, isLoading, isCompact, ...props }) => {
|
const _EditButton: React.FC<EditButtonProps> = ({
|
||||||
|
onClick,
|
||||||
|
label,
|
||||||
|
isDisabled,
|
||||||
|
isLoading,
|
||||||
|
isCompact = true,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
if (!isCompact && breakpoint !== 'base' && breakpoint !== 'sm') {
|
||||||
@@ -24,12 +33,12 @@ const _EditButton: React.FC<EditButtonProps> = ({ onClick, label, isDisabled, is
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{label}
|
{label ?? t('common.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label}>
|
<Tooltip label={label ?? t('common.edit')} hasArrow>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="edit"
|
aria-label="edit"
|
||||||
colorScheme="gray"
|
colorScheme="gray"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, ThemeTypings, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, ThemeTypings, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { ArrowsClockwise } from 'phosphor-react';
|
import { ArrowsClockwise } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface RefreshButtonProps {
|
export interface RefreshButtonProps {
|
||||||
@@ -17,7 +17,7 @@ const _RefreshButton: React.FC<RefreshButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isFetching,
|
isFetching,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
ml,
|
ml,
|
||||||
size,
|
size,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const _ResponsiveButton: React.FC<ResponsiveButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
color,
|
color,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { FloppyDisk } from 'phosphor-react';
|
import { FloppyDisk } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface SaveButtonProps
|
export interface SaveButtonProps
|
||||||
@@ -18,7 +18,7 @@ const _SaveButton: React.FC<SaveButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
isDirty,
|
isDirty,
|
||||||
dirtyCheck,
|
dirtyCheck,
|
||||||
...props
|
...props
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { ArrowRight, FloppyDisk } from 'phosphor-react';
|
import { ArrowRight, FloppyDisk } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface StepButtonProps {
|
export interface StepButtonProps {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||||
import { Pencil, X } from 'phosphor-react';
|
import { Pencil, X } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
|
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ const _ToggleEditButton: React.FC<ToggleEditButtonProps> = ({
|
|||||||
isDirty,
|
isDirty,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
ml,
|
ml,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { Warning } from 'phosphor-react';
|
import { Warning } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ThemeProps } from 'models/Theme';
|
import { ThemeProps } from 'models/Theme';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ const _WarningButton: React.FC<WarningButtonProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
label,
|
label,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1,17 +1,57 @@
|
|||||||
import React from 'react';
|
import React, { DOMAttributes } from 'react';
|
||||||
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
import {
|
||||||
|
BackgroundProps,
|
||||||
|
Box,
|
||||||
|
EffectProps,
|
||||||
|
InteractivityProps,
|
||||||
|
LayoutProps,
|
||||||
|
PositionProps,
|
||||||
|
SpaceProps,
|
||||||
|
useColorModeValue,
|
||||||
|
useStyleConfig,
|
||||||
|
useToken,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
export interface CardHeaderProps extends LayoutProps, SpaceProps {
|
export interface CardHeaderProps
|
||||||
variant?: string;
|
extends LayoutProps,
|
||||||
|
SpaceProps,
|
||||||
|
BackgroundProps,
|
||||||
|
InteractivityProps,
|
||||||
|
PositionProps,
|
||||||
|
EffectProps,
|
||||||
|
DOMAttributes<HTMLDivElement> {
|
||||||
|
variant?: 'panel' | 'unstyled';
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
headerStyle?: {
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const _CardHeader: React.FC<CardHeaderProps> = ({ variant, children, ...rest }) => {
|
const _CardHeader: React.FC<CardHeaderProps> = ({
|
||||||
// @ts-ignore
|
variant,
|
||||||
|
children,
|
||||||
|
icon,
|
||||||
|
headerStyle = {
|
||||||
|
color: 'blue',
|
||||||
|
},
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
const iconBgcolor = useToken('colors', [`${headerStyle?.color}.500`, `${headerStyle?.color}.300`]);
|
||||||
|
const bgColor = useToken('colors', [`${headerStyle?.color}.50`, `${headerStyle?.color}.700`]);
|
||||||
|
const iconColor = useColorModeValue(iconBgcolor[0], iconBgcolor[1]);
|
||||||
|
const headerBgColor = useColorModeValue(bgColor[0], bgColor[1]);
|
||||||
|
|
||||||
const styles = useStyleConfig('CardHeader', { variant });
|
const styles = useStyleConfig('CardHeader', { variant });
|
||||||
|
|
||||||
// Pass the computed styles into the `__css` prop
|
// Pass the computed styles into the `__css` prop
|
||||||
return (
|
return (
|
||||||
<Box __css={styles} {...rest}>
|
<Box __css={styles} bgColor={variant === 'unstyled' ? undefined : headerBgColor} {...rest}>
|
||||||
|
{icon ? (
|
||||||
|
<Box mr={2} color={headerStyle ? iconColor : undefined} bgColor="unset">
|
||||||
|
{icon}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
{children}
|
{children}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { BackgroundProps, Box, InteractivityProps, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||||
BackgroundProps,
|
|
||||||
Box,
|
|
||||||
EffectProps,
|
|
||||||
InteractivityProps,
|
|
||||||
LayoutProps,
|
|
||||||
PositionProps,
|
|
||||||
SpaceProps,
|
|
||||||
useStyleConfig,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
|
|
||||||
export interface CardProps
|
export interface CardProps extends LayoutProps, SpaceProps, BackgroundProps, InteractivityProps {
|
||||||
extends LayoutProps,
|
|
||||||
SpaceProps,
|
|
||||||
BackgroundProps,
|
|
||||||
InteractivityProps,
|
|
||||||
PositionProps,
|
|
||||||
EffectProps {
|
|
||||||
variant?: string;
|
variant?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Box, Heading, IconButton, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
import { Box, Heading, IconButton, Spacer, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||||
import { ArrowsOut, Info } from 'phosphor-react';
|
import { ArrowsOut, Info } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from '../Card';
|
import { Card } from '../Card';
|
||||||
import { CardBody } from '../Card/CardBody';
|
import { CardBody } from '../Card/CardBody';
|
||||||
@@ -18,7 +18,7 @@ const GraphStatDisplay = ({ chart, title, explanation }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card variant="widget" w="100%">
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<Heading mr={2} my="auto" size="md">
|
<Heading mr={2} my="auto" size="md">
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, LayoutProps, ModalHeader as Header, SpaceProps, Spacer } from '@chakra-ui/react';
|
import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
|
||||||
export interface ModalHeaderProps extends LayoutProps, SpaceProps {
|
export interface ModalHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
right?: React.ReactNode;
|
left?: React.ReactNode;
|
||||||
|
right: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalHeader = ({ title, right }: ModalHeaderProps) => (
|
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
|
||||||
<Header>
|
const bg = useColorModeValue('blue.50', 'blue.700');
|
||||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
|
||||||
|
return (
|
||||||
|
<Header bg={bg}>
|
||||||
{title}
|
{title}
|
||||||
|
{left ? (
|
||||||
|
<HStack spacing={2} ml={2}>
|
||||||
|
{left}
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{right}
|
{right}
|
||||||
</Flex>
|
</Header>
|
||||||
</Header>
|
);
|
||||||
);
|
};
|
||||||
|
export const ModalHeader = React.memo(_ModalHeader);
|
||||||
|
|||||||
34
src/components/Containers/ResponsiveTag/index.tsx
Normal file
34
src/components/Containers/ResponsiveTag/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { As, Icon, Tag, TagLabel, TagLeftIcon, TagProps, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export interface ResponsiveTagProps extends TagProps {
|
||||||
|
label: string;
|
||||||
|
icon: As<any>;
|
||||||
|
tooltip?: string;
|
||||||
|
isCompact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResponsiveTag = React.memo(({ label, icon, tooltip, isCompact, ...props }: ResponsiveTagProps) => {
|
||||||
|
const breakpoint = useBreakpoint();
|
||||||
|
|
||||||
|
const isCompactVersion = isCompact || breakpoint === 'base' || breakpoint === 'sm';
|
||||||
|
|
||||||
|
if (isCompactVersion) {
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltip ?? label}>
|
||||||
|
<Tag size="lg" colorScheme="blue" {...props}>
|
||||||
|
<Icon as={icon} boxSize="18px" />
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={tooltip ?? label}>
|
||||||
|
<Tag size="lg" colorScheme="blue" {...props}>
|
||||||
|
<TagLeftIcon boxSize="18px" as={icon} />
|
||||||
|
<TagLabel>{label}</TagLabel>
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { As, Flex, Heading, Icon, Spacer, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
import { As, Flex, Heading, Icon, Spacer, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||||
import { Info } from 'phosphor-react';
|
import { Info } from '@phosphor-icons/react';
|
||||||
import { Card } from '../Card';
|
import { Card } from '../Card';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,19 +15,19 @@ const SimpleIconStatDisplay = ({ title, description, icon, value, color }: Props
|
|||||||
const bgColor = useColorModeValue(color[0], color[1]);
|
const bgColor = useColorModeValue(color[0], color[1]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card variant="widget" w="100%" p={3}>
|
<Card p={3} bgColor={bgColor}>
|
||||||
<Flex h="70px" w="100%">
|
<Flex h="70px" w="100%" color="white">
|
||||||
<Flex direction="column" justifyContent="center">
|
<Flex direction="column" justifyContent="center">
|
||||||
<Heading size="lg">{value}</Heading>
|
<Heading size="lg">{value}</Heading>
|
||||||
<Heading size="sm" display="flex">
|
<Heading size="sm" display="flex">
|
||||||
<Text opacity={0.8}>{title}</Text>
|
<Text>{title}</Text>
|
||||||
<Tooltip label={description} hasArrow>
|
<Tooltip label={description} hasArrow>
|
||||||
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
|
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Heading>
|
</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" bgColor={bgColor} color="white" />
|
<Icon borderRadius="15px" my="auto" as={icon} boxSize="70px" color="white" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
|
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
|
||||||
import { FunnelSimple } from 'phosphor-react';
|
import { FunnelSimple } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
@@ -21,7 +21,7 @@ export const ColumnPicker = ({
|
|||||||
hiddenColumns,
|
hiddenColumns,
|
||||||
setHiddenColumns,
|
setHiddenColumns,
|
||||||
size,
|
size,
|
||||||
isCompact,
|
isCompact = true,
|
||||||
}: ColumnPickerProps) => {
|
}: ColumnPickerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { getPref, setPref } = useAuth();
|
const { getPref, setPref } = useAuth();
|
||||||
|
|||||||
64
src/components/DataTables/DataGrid/CellRow.tsx
Normal file
64
src/components/DataTables/DataGrid/CellRow.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Td, Tr } from '@chakra-ui/react';
|
||||||
|
import { Row, flexRender } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
export type DataGridCellRowProps<TValue extends object> = {
|
||||||
|
row: Row<TValue>;
|
||||||
|
onRowClick: ((row: TValue) => (() => void) | undefined) | undefined;
|
||||||
|
rowStyle: {
|
||||||
|
hoveredRowBg: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataGridCellRow = <TValue extends object>({
|
||||||
|
row,
|
||||||
|
rowStyle: { hoveredRowBg },
|
||||||
|
onRowClick,
|
||||||
|
}: DataGridCellRowProps<TValue>) => {
|
||||||
|
const onClick = onRowClick ? onRowClick(row.original) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
key={row.id}
|
||||||
|
_hover={{
|
||||||
|
backgroundColor: hoveredRowBg,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
borderRight="1px solid gray"
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<Td
|
||||||
|
px={1}
|
||||||
|
key={cell.id}
|
||||||
|
textOverflow="ellipsis"
|
||||||
|
overflow="hidden"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
minWidth={cell.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||||
|
maxWidth={cell.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||||
|
width={cell.column.columnDef.meta?.customWidth}
|
||||||
|
textAlign={cell.column.columnDef.meta?.isCentered ? 'center' : undefined}
|
||||||
|
fontFamily={
|
||||||
|
cell.column.columnDef.meta?.isMonospace
|
||||||
|
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
cell.column.columnDef.meta?.stopPropagation || (cell.column.id === 'actions' && onClick)
|
||||||
|
? (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
cursor={
|
||||||
|
!cell.column.columnDef.meta?.stopPropagation && cell.column.id !== 'actions' && onClick
|
||||||
|
? 'pointer'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
border="0.5px solid gray"
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</Td>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
54
src/components/DataTables/DataGrid/DataGridColumnPicker.tsx
Normal file
54
src/components/DataTables/DataGrid/DataGridColumnPicker.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Box, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, Tooltip } from '@chakra-ui/react';
|
||||||
|
import { FunnelSimple } from '@phosphor-icons/react';
|
||||||
|
import { VisibilityState } from '@tanstack/react-table';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { DataGridColumn } from './useDataGrid';
|
||||||
|
|
||||||
|
export type DataGridColumnPickerProps<TValue extends object> = {
|
||||||
|
columns: DataGridColumn<TValue>[];
|
||||||
|
columnVisibility: VisibilityState;
|
||||||
|
toggleVisibility: (id: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataGridColumnPicker = <TValue extends object>({
|
||||||
|
columns,
|
||||||
|
columnVisibility,
|
||||||
|
toggleVisibility,
|
||||||
|
}: DataGridColumnPickerProps<TValue>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Menu closeOnSelect={false} isLazy>
|
||||||
|
<Tooltip label={t('common.columns')} hasArrow>
|
||||||
|
<MenuButton as={IconButton} icon={<FunnelSimple />} />
|
||||||
|
</Tooltip>
|
||||||
|
<MenuList maxH="200px" overflowY="auto">
|
||||||
|
{columns
|
||||||
|
.filter((col) => col.id && col.header)
|
||||||
|
.map((column) => {
|
||||||
|
const handleClick =
|
||||||
|
column.id !== undefined ? () => toggleVisibility(column.id as unknown as string) : undefined;
|
||||||
|
const id = column.id ?? uuid();
|
||||||
|
let label = column.header?.toString() ?? 'Unrecognized column';
|
||||||
|
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={id}
|
||||||
|
as={Checkbox}
|
||||||
|
isChecked={columnVisibility[id] === undefined || columnVisibility[id]}
|
||||||
|
onChange={column.meta?.alwaysShow ? undefined : handleClick}
|
||||||
|
isDisabled={column.meta?.alwaysShow}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
45
src/components/DataTables/DataGrid/HeaderRow.tsx
Normal file
45
src/components/DataTables/DataGrid/HeaderRow.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Flex, Th, Tooltip, Tr } from '@chakra-ui/react';
|
||||||
|
import { HeaderGroup, flexRender } from '@tanstack/react-table';
|
||||||
|
import { DataGridSortIcon } from './SortIcon';
|
||||||
|
|
||||||
|
export type DataGridHeaderRowProps<TValue extends object> = {
|
||||||
|
headerGroup: HeaderGroup<TValue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataGridHeaderRow = <TValue extends object>({ headerGroup }: DataGridHeaderRowProps<TValue>) => (
|
||||||
|
<Tr p={0} borderRight="1px solid gray">
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<Th
|
||||||
|
color="gray.400"
|
||||||
|
key={header.id}
|
||||||
|
colSpan={header.colSpan}
|
||||||
|
minWidth={header.column.columnDef.meta?.customMinWidth ?? undefined}
|
||||||
|
maxWidth={header.column.columnDef.meta?.customMaxWidth ?? undefined}
|
||||||
|
width={header.column.columnDef.meta?.customWidth}
|
||||||
|
fontSize="sm"
|
||||||
|
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
|
||||||
|
cursor={header.column.getCanSort() ? 'pointer' : undefined}
|
||||||
|
border="0.5px solid gray"
|
||||||
|
px={1}
|
||||||
|
>
|
||||||
|
<Flex display="flex" alignItems="center">
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<Tooltip label={header.column.columnDef.meta?.headerOptions?.tooltip}>
|
||||||
|
<Box
|
||||||
|
overflow="hidden"
|
||||||
|
whiteSpace="nowrap"
|
||||||
|
alignContent="center"
|
||||||
|
width="100%"
|
||||||
|
{...header.column.columnDef.meta?.headerStyleProps}
|
||||||
|
>
|
||||||
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<DataGridSortIcon sortInfo={header.column.getIsSorted()} canSort={header.column.getCanSort()} />
|
||||||
|
</Flex>
|
||||||
|
</Th>
|
||||||
|
))}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
124
src/components/DataTables/DataGrid/Input.tsx
Normal file
124
src/components/DataTables/DataGrid/Input.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
Text,
|
||||||
|
Select,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Table } from '@tanstack/react-table';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { useContainerDimensions } from 'hooks/useContainerDimensions';
|
||||||
|
|
||||||
|
type Props<T extends object> = {
|
||||||
|
table: Table<T>;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DataGridControls = <T extends object>({ table, isDisabled }: Props<T>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { ref, dimensions } = useContainerDimensions({ precision: 100 });
|
||||||
|
const isCompact = dimensions.width !== 0 && dimensions.width <= 800;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex ref={ref} justifyContent="space-between" m={4} alignItems="center">
|
||||||
|
<Flex>
|
||||||
|
<Tooltip label={t('table.first_page')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Go to first page"
|
||||||
|
onClick={() => table.setPageIndex(0)}
|
||||||
|
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||||
|
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||||
|
mr={4}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('table.previous_page')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Previous page"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
isDisabled={isDisabled || !table.getCanPreviousPage()}
|
||||||
|
icon={<ChevronLeftIcon h={6} w={6} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex alignItems="center">
|
||||||
|
{isCompact ? null : (
|
||||||
|
<>
|
||||||
|
<Text flexShrink={0} mr={8}>
|
||||||
|
{t('table.page')}{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{table.getState().pagination.pageIndex + 1}
|
||||||
|
</Text>{' '}
|
||||||
|
{t('common.of')}{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{table.getPageCount()}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text flexShrink={0}>{t('table.go_to_page')}</Text>{' '}
|
||||||
|
<NumberInput
|
||||||
|
ml={2}
|
||||||
|
mr={8}
|
||||||
|
w={28}
|
||||||
|
min={1}
|
||||||
|
max={table.getPageCount()}
|
||||||
|
onChange={(_, numberValue) => {
|
||||||
|
const newPage = numberValue ? numberValue - 1 : 0;
|
||||||
|
table.setPageIndex(newPage);
|
||||||
|
}}
|
||||||
|
value={table.getState().pagination.pageIndex + 1}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Select
|
||||||
|
w={32}
|
||||||
|
value={table.getState().pagination.pageSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
table.setPageSize(Number(e.target.value));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{[10, 20, 30, 40, 50].map((opt) => (
|
||||||
|
<option key={uuid()} value={opt}>
|
||||||
|
{t('common.show')} {opt}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Flex>
|
||||||
|
<Tooltip label={t('table.next_page')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Go to next page"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||||
|
icon={<ChevronRightIcon h={6} w={6} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t('table.last_page')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Go to last page"
|
||||||
|
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||||
|
isDisabled={isDisabled || !table.getCanNextPage()}
|
||||||
|
icon={<ArrowRightIcon h={3} w={3} />}
|
||||||
|
ml={4}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataGridControls;
|
||||||
23
src/components/DataTables/DataGrid/SortIcon.tsx
Normal file
23
src/components/DataTables/DataGrid/SortIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '@chakra-ui/react';
|
||||||
|
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||||
|
import { SortDirection } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
export type DataGridSortIconProps = {
|
||||||
|
sortInfo: false | SortDirection;
|
||||||
|
canSort: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataGridSortIcon = ({ sortInfo, canSort }: DataGridSortIconProps) => {
|
||||||
|
if (canSort) {
|
||||||
|
if (sortInfo) {
|
||||||
|
return sortInfo === 'desc' ? (
|
||||||
|
<Icon ml={1} boxSize={3} as={ArrowDown} />
|
||||||
|
) : (
|
||||||
|
<Icon ml={1} boxSize={3} as={ArrowUp} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Icon ml={1} boxSize={3} as={Circle} />;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Icon, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import { Draggable } from '@hello-pangea/dnd';
|
||||||
|
import { ArrowsDownUp, Lock } from '@phosphor-icons/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DataGridColumn } from '../../useDataGrid';
|
||||||
|
|
||||||
|
type Props<TValue> = {
|
||||||
|
draggableId: string;
|
||||||
|
index: number;
|
||||||
|
column: DataGridColumn<TValue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DraggableColumn = <TValue extends object>({ draggableId, index, column }: Props<TValue>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const isDraggingBackground = useColorModeValue('blue.100', 'blue.600');
|
||||||
|
const notDraggingBackground = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
|
let label = column.header?.toString() ?? 'Unrecognized column';
|
||||||
|
if (column.meta?.columnSelectorOptions?.label) label = column.meta.columnSelectorOptions.label;
|
||||||
|
|
||||||
|
const tooltipLabel = () => {
|
||||||
|
if (column.meta?.anchored) return t('table.drag_locked');
|
||||||
|
if (column.meta?.alwaysShow) return t('table.drag_always_show');
|
||||||
|
|
||||||
|
return t('table.drag_explanation');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={draggableId} index={index} isDragDisabled={column.meta?.anchored}>
|
||||||
|
{(itemProvided, itemSnapshot) => (
|
||||||
|
<Tooltip label={tooltipLabel()}>
|
||||||
|
<Box
|
||||||
|
ref={itemProvided.innerRef}
|
||||||
|
{...itemProvided.draggableProps}
|
||||||
|
{...itemProvided.dragHandleProps}
|
||||||
|
display="flex"
|
||||||
|
backgroundColor={itemSnapshot.isDragging ? isDraggingBackground : notDraggingBackground}
|
||||||
|
px={6}
|
||||||
|
py={2}
|
||||||
|
my={2}
|
||||||
|
borderRadius={15}
|
||||||
|
cursor={column.meta?.anchored ? 'not-allowed' : undefined}
|
||||||
|
>
|
||||||
|
<Icon as={column.meta?.anchored ? Lock : ArrowsDownUp} boxSize={5} ml={0.5} mr={2} my="auto" />
|
||||||
|
<Text my="auto">{label}</Text>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DraggableColumn;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import { Droppable } from '@hello-pangea/dnd';
|
||||||
|
import { DataGridColumn } from '../../useDataGrid';
|
||||||
|
import DraggableColumn from './DraggableColumn';
|
||||||
|
|
||||||
|
type Props<TValue> = {
|
||||||
|
items: string[];
|
||||||
|
columns: DataGridColumn<TValue>[];
|
||||||
|
droppableId: string;
|
||||||
|
isDropDisabled?: boolean;
|
||||||
|
};
|
||||||
|
const DroppableBox = <TValue extends object>({ items, columns, droppableId, isDropDisabled }: Props<TValue>) => {
|
||||||
|
const notDraggingBackground = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const isDraggingOverBackground = useColorModeValue('blue.300', 'blue.500');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Droppable droppableId={droppableId} direction="vertical" isCombineEnabled={false} isDropDisabled={isDropDisabled}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<Box
|
||||||
|
ref={provided.innerRef}
|
||||||
|
backgroundColor={snapshot.isDraggingOver ? isDraggingOverBackground : notDraggingBackground}
|
||||||
|
padding={2}
|
||||||
|
borderRadius={15}
|
||||||
|
>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
const found = columns.find((col) => col.id === item);
|
||||||
|
return found ? <DraggableColumn key={item} draggableId={item} index={index} column={found} /> : null;
|
||||||
|
})}
|
||||||
|
{provided.placeholder}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DroppableBox);
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||||
|
import { DragDropContext, DragStart, DropResult } from '@hello-pangea/dnd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DataGridColumn, UseDataGridReturn } from '../../useDataGrid';
|
||||||
|
import DroppableBox from './DroppableBox';
|
||||||
|
|
||||||
|
const reorder = (list: string[], startIndex: number, endIndex: number) => {
|
||||||
|
const result = Array.from(list);
|
||||||
|
const [removed] = result.splice(startIndex, 1);
|
||||||
|
if (removed) {
|
||||||
|
result.splice(endIndex, 0, removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShownColumns = <TValue extends object>(columns: DataGridColumn<TValue>[], columnOrder: string[]) => {
|
||||||
|
const order = [...columnOrder];
|
||||||
|
|
||||||
|
for (const col of columns) {
|
||||||
|
if (!order.includes(col.id)) {
|
||||||
|
order.push(col.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return order;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props<TValue extends object> = {
|
||||||
|
controller: UseDataGridReturn;
|
||||||
|
shownColumns: DataGridColumn<TValue>[];
|
||||||
|
hiddenColumns: DataGridColumn<TValue>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableDragDrop = <TValue extends object>({ controller, shownColumns, hiddenColumns }: Props<TValue>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [shownOrder, setShowOrder] = React.useState(getShownColumns(shownColumns, controller.columnOrder));
|
||||||
|
const [hiddenOrder, setHiddenOrder] = React.useState(hiddenColumns.map((col) => col.id));
|
||||||
|
const [currentDraggingColumn, setCurrentDraggingColumn] = React.useState<DataGridColumn<TValue>>();
|
||||||
|
|
||||||
|
const handleDragStart = React.useCallback(
|
||||||
|
(start: DragStart) => {
|
||||||
|
const foundColumn =
|
||||||
|
shownColumns.find(({ id }) => id === start.draggableId) ??
|
||||||
|
hiddenColumns.find(({ id }) => id === start.draggableId);
|
||||||
|
setCurrentDraggingColumn(foundColumn);
|
||||||
|
},
|
||||||
|
[shownColumns, hiddenColumns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const minimumIndex = React.useMemo(() => {
|
||||||
|
let index = 0;
|
||||||
|
for (const [i, col] of shownColumns.entries()) {
|
||||||
|
if (col.meta?.anchored) {
|
||||||
|
index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return index + 1;
|
||||||
|
}, [shownColumns]);
|
||||||
|
|
||||||
|
const handleDragEnd = React.useCallback(
|
||||||
|
(result: DropResult) => {
|
||||||
|
const { source, destination, draggableId } = result;
|
||||||
|
|
||||||
|
if (destination === null) return;
|
||||||
|
|
||||||
|
if (source.droppableId === destination.droppableId) {
|
||||||
|
const newOrder = reorder(shownOrder, source.index, Math.max(destination.index, minimumIndex));
|
||||||
|
if (destination.droppableId === 'displayed-columns') {
|
||||||
|
controller.setColumnOrder(newOrder);
|
||||||
|
setShowOrder(newOrder);
|
||||||
|
} else setHiddenOrder(newOrder);
|
||||||
|
}
|
||||||
|
// This means we are moving from displayed to hidden
|
||||||
|
else if (source.droppableId === 'displayed-columns') {
|
||||||
|
// Toggle the column visibility in user preferences
|
||||||
|
const results = controller.hideColumn(draggableId);
|
||||||
|
if (results) {
|
||||||
|
setHiddenOrder([...results.hiddenColumns]);
|
||||||
|
setShowOrder([...results.columnOrder]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This means we are moving from hidden to displayed
|
||||||
|
else if (source.droppableId === 'hidden-columns') {
|
||||||
|
const newOrder = Array.from(shownOrder);
|
||||||
|
newOrder.splice(Math.max(destination.index, minimumIndex), 0, draggableId);
|
||||||
|
const results = controller.unhideColumn(draggableId, newOrder);
|
||||||
|
if (results) {
|
||||||
|
setHiddenOrder(results.hiddenColumns);
|
||||||
|
setShowOrder([...results.columnOrder]);
|
||||||
|
setHiddenOrder([...results.hiddenColumns]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentDraggingColumn(undefined);
|
||||||
|
},
|
||||||
|
[shownColumns, hiddenColumns, controller.hideColumn, controller.unhideColumn, minimumIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading size="md">{t('table.columns')}</Heading>
|
||||||
|
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
||||||
|
<Flex mt={4}>
|
||||||
|
<Box w="50%" mr={2}>
|
||||||
|
<Heading size="sm" mb={4}>
|
||||||
|
Visible ({shownOrder.length})
|
||||||
|
</Heading>
|
||||||
|
<DroppableBox droppableId="displayed-columns" items={shownOrder} columns={shownColumns} />
|
||||||
|
</Box>
|
||||||
|
<Box ml={2} w="50%">
|
||||||
|
<Heading size="sm" mb={4}>
|
||||||
|
Hidden ({hiddenColumns.length})
|
||||||
|
</Heading>
|
||||||
|
<DroppableBox
|
||||||
|
droppableId="hidden-columns"
|
||||||
|
items={hiddenOrder}
|
||||||
|
columns={hiddenColumns}
|
||||||
|
isDropDisabled={currentDraggingColumn?.meta?.alwaysShow}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</DragDropContext>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableDragDrop;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { SettingsIcon } from '@chakra-ui/icons';
|
||||||
|
import { Box, IconButton, Tooltip, useDisclosure } from '@chakra-ui/react';
|
||||||
|
import { ClockCounterClockwise } from '@phosphor-icons/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '../../../Modals/Modal';
|
||||||
|
import { DataGridColumn, UseDataGridReturn } from '../useDataGrid';
|
||||||
|
import TableDragDrop from './DragDrop';
|
||||||
|
|
||||||
|
type Props<TValue extends object> = {
|
||||||
|
controller: UseDataGridReturn;
|
||||||
|
columns: DataGridColumn<TValue>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableSettingsModal = <TValue extends object>({ controller, columns }: Props<TValue>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const modalProps = useDisclosure();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip label={t('table.preferences')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('table.preferences')}
|
||||||
|
icon={<SettingsIcon weight="bold" />}
|
||||||
|
onClick={modalProps.onOpen}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Modal
|
||||||
|
title={t('table.preferences')}
|
||||||
|
topRightButtons={
|
||||||
|
<Tooltip label={t('table.reset')}>
|
||||||
|
<IconButton
|
||||||
|
aria-label={t('table.reset')}
|
||||||
|
icon={<ClockCounterClockwise size={20} />}
|
||||||
|
onClick={controller.resetPreferences}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
modalSize: 'md',
|
||||||
|
maxWidth: { sm: '600px', md: '600px', lg: '600px', xl: '600px' },
|
||||||
|
}}
|
||||||
|
{...modalProps}
|
||||||
|
>
|
||||||
|
<Box w="100%">
|
||||||
|
<TableDragDrop<TValue>
|
||||||
|
shownColumns={columns.filter((col) => controller.columnVisibility[col.id] !== false)}
|
||||||
|
hiddenColumns={columns.filter((col) => controller.columnVisibility[col.id] === false)}
|
||||||
|
controller={controller}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TableSettingsModal);
|
||||||
251
src/components/DataTables/DataGrid/index.tsx
Normal file
251
src/components/DataTables/DataGrid/index.tsx
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Heading,
|
||||||
|
LayoutProps,
|
||||||
|
Spacer,
|
||||||
|
Spinner,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
Tbody,
|
||||||
|
Thead,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { RefreshButton } from '../../Buttons/RefreshButton';
|
||||||
|
import { DataGridCellRow } from './CellRow';
|
||||||
|
import { DataGridHeaderRow } from './HeaderRow';
|
||||||
|
import DataGridControls from './Input';
|
||||||
|
import TableSettingsModal from './TableSettingsModal';
|
||||||
|
import { DataGridColumn, UseDataGridReturn } from './useDataGrid';
|
||||||
|
import { Card } from 'components/Containers/Card';
|
||||||
|
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||||
|
import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||||
|
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
|
|
||||||
|
export type ColumnOptions = {
|
||||||
|
isSortable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataGridOptions<TValue extends object> = {
|
||||||
|
count?: number;
|
||||||
|
isFullScreen?: boolean;
|
||||||
|
isHidingControls?: boolean;
|
||||||
|
isManual?: boolean;
|
||||||
|
minimumHeight?: LayoutProps['minH'];
|
||||||
|
onRowClick?: (row: TValue) => (() => void) | undefined;
|
||||||
|
refetch?: () => void;
|
||||||
|
showAsCard?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataGridProps<TValue extends object> = {
|
||||||
|
controller: UseDataGridReturn;
|
||||||
|
columns: DataGridColumn<TValue>[];
|
||||||
|
header: {
|
||||||
|
title: string;
|
||||||
|
objectListed: string;
|
||||||
|
leftContent?: React.ReactNode;
|
||||||
|
addButton?: React.ReactNode;
|
||||||
|
otherButtons?: React.ReactNode;
|
||||||
|
};
|
||||||
|
data?: TValue[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
options?: DataGridOptions<TValue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DataGrid = <TValue extends object>({
|
||||||
|
controller,
|
||||||
|
columns,
|
||||||
|
header,
|
||||||
|
data = [],
|
||||||
|
options = {},
|
||||||
|
isLoading = false,
|
||||||
|
}: DataGridProps<TValue>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
/*
|
||||||
|
Table Styling
|
||||||
|
*/
|
||||||
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
|
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||||
|
|
||||||
|
const minimumHeight: LayoutProps['minH'] = React.useMemo(() => {
|
||||||
|
if (options.isFullScreen) {
|
||||||
|
return { base: 'calc(100vh - 360px)', md: 'calc(100vh - 288px)' };
|
||||||
|
}
|
||||||
|
return options.minimumHeight ?? '300px';
|
||||||
|
}, [options.isFullScreen, options.minimumHeight]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Table Options
|
||||||
|
*/
|
||||||
|
const onRowClick = React.useMemo(() => options.onRowClick, [options.onRowClick]);
|
||||||
|
|
||||||
|
const pagination = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
pageIndex: controller.pageInfo.pageIndex,
|
||||||
|
pageSize: controller.pageInfo.pageSize,
|
||||||
|
}),
|
||||||
|
[controller.pageInfo.pageIndex, controller.pageInfo.pageSize],
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageCount = React.useMemo(() => {
|
||||||
|
if (options.isManual && options.count) {
|
||||||
|
return Math.ceil(options.count / pagination.pageSize);
|
||||||
|
}
|
||||||
|
return Math.ceil((data?.length ?? 0) / pagination.pageSize);
|
||||||
|
}, [options.count, options.isManual, data?.length, pagination.pageSize]);
|
||||||
|
|
||||||
|
const tableOptions = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
pageCount: pageCount > 0 ? pageCount : 1,
|
||||||
|
initialState: { sorting: controller.sortBy, pagination },
|
||||||
|
manualPagination: options.isManual,
|
||||||
|
manualSorting: options.isManual,
|
||||||
|
autoResetPageIndex: false,
|
||||||
|
}),
|
||||||
|
[options.isManual, controller.sortBy, pageCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
const orderedColumns = React.useMemo(() => {
|
||||||
|
const order = controller.columnOrder.filter((id) => columns.find((col) => col.id === id));
|
||||||
|
if (order.length !== columns.length) {
|
||||||
|
for (const col of columns) {
|
||||||
|
if (!order.includes(col.id)) {
|
||||||
|
order.push(col.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns.slice().sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||||
|
}, [columns, controller.columnOrder]);
|
||||||
|
|
||||||
|
const table = useReactTable<TValue>({
|
||||||
|
// react-table base functions
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
|
||||||
|
// Table State
|
||||||
|
data,
|
||||||
|
columns: orderedColumns,
|
||||||
|
state: {
|
||||||
|
sorting: controller.sortBy,
|
||||||
|
columnVisibility: controller.columnVisibility,
|
||||||
|
pagination,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Change Handlers
|
||||||
|
onSortingChange: controller.setSortBy,
|
||||||
|
onPaginationChange: controller.onPaginationChange,
|
||||||
|
|
||||||
|
// debugTable: true,
|
||||||
|
|
||||||
|
// Table Options
|
||||||
|
...tableOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading && data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center>
|
||||||
|
<Spinner size="xl" />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.showAsCard ? (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<Heading size="md" my="auto" mr={2}>
|
||||||
|
{header.title}
|
||||||
|
</Heading>
|
||||||
|
{header.leftContent}
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{header.otherButtons}
|
||||||
|
{header.addButton}
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||||
|
}
|
||||||
|
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||||
|
</HStack>
|
||||||
|
</CardHeader>
|
||||||
|
<CardBody display="flex" flexDirection="column">
|
||||||
|
<LoadingOverlay isLoading={isLoading}>
|
||||||
|
<TableContainer minH={minimumHeight}>
|
||||||
|
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||||
|
<Thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||||
|
))}
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<Center mt={8}>
|
||||||
|
<Heading size="md">
|
||||||
|
{header.objectListed
|
||||||
|
? t('common.no_obj_found', { obj: header.objectListed })
|
||||||
|
: t('common.empty_list')}
|
||||||
|
</Heading>
|
||||||
|
</Center>
|
||||||
|
) : null}
|
||||||
|
</TableContainer>
|
||||||
|
</LoadingOverlay>
|
||||||
|
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Box w="100%">
|
||||||
|
<Flex mb={2}>
|
||||||
|
<Heading size="md" my="auto" mr={2}>
|
||||||
|
{header.title}
|
||||||
|
</Heading>
|
||||||
|
{header.leftContent}
|
||||||
|
<Spacer />
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{header.otherButtons}
|
||||||
|
{header.addButton}
|
||||||
|
{
|
||||||
|
// @ts-ignore
|
||||||
|
<TableSettingsModal<TValue> controller={controller} columns={columns} />
|
||||||
|
}
|
||||||
|
{options.refetch ? <RefreshButton onClick={options.refetch} isCompact isFetching={isLoading} /> : null}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
<LoadingOverlay isLoading={isLoading}>
|
||||||
|
<TableContainer minH={minimumHeight}>
|
||||||
|
<Table size="small" variant="simple" textColor={textColor} w="100%" fontSize="14px">
|
||||||
|
<Thead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<DataGridHeaderRow<TValue> key={headerGroup.id} headerGroup={headerGroup} />
|
||||||
|
))}
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{table.getRowModel().rows.map((row) => (
|
||||||
|
<DataGridCellRow<TValue> key={row.id} row={row} onRowClick={onRowClick} rowStyle={{ hoveredRowBg }} />
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<Center mt={8}>
|
||||||
|
<Heading size="md">
|
||||||
|
{header.objectListed ? t('common.no_obj_found', { obj: header.objectListed }) : t('common.empty_list')}
|
||||||
|
</Heading>
|
||||||
|
</Center>
|
||||||
|
) : null}
|
||||||
|
</TableContainer>
|
||||||
|
</LoadingOverlay>
|
||||||
|
{!options.isHidingControls ? <DataGridControls table={table} isDisabled={isLoading} /> : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
195
src/components/DataTables/DataGrid/useDataGrid.ts
Normal file
195
src/components/DataTables/DataGrid/useDataGrid.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ColumnDef, PaginationState, SortingColumnDef, SortingState, VisibilityState } from '@tanstack/react-table';
|
||||||
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
|
|
||||||
|
const getDefaultSettings = (settings?: string) => {
|
||||||
|
let limit = 10;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
if (settings) {
|
||||||
|
const savedSizeSetting = localStorage.getItem(settings);
|
||||||
|
if (savedSizeSetting) {
|
||||||
|
try {
|
||||||
|
limit = parseInt(savedSizeSetting, 10);
|
||||||
|
} catch (e) {
|
||||||
|
limit = 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedPageSetting = localStorage.getItem(`${settings}.page`);
|
||||||
|
if (savedPageSetting) {
|
||||||
|
try {
|
||||||
|
index = parseInt(savedPageSetting, 10);
|
||||||
|
} catch (e) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageSize: limit,
|
||||||
|
pageIndex: index,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSavedColumnOrder = (defaultValue: string[], settings?: string) => {
|
||||||
|
if (settings) {
|
||||||
|
const savedOrderSetting = localStorage.getItem(`${settings}.order`);
|
||||||
|
if (savedOrderSetting) {
|
||||||
|
try {
|
||||||
|
const savedOrder = JSON.parse(savedOrderSetting);
|
||||||
|
return savedOrder.length > 0 ? savedOrder : defaultValue;
|
||||||
|
} catch (e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DataGridColumn<T> = ColumnDef<T> & SortingColumnDef<T> & { id: string };
|
||||||
|
|
||||||
|
export type UseDataGridProps = {
|
||||||
|
tableSettingsId: string;
|
||||||
|
defaultOrder: string[];
|
||||||
|
defaultSortBy?: SortingState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDataGrid = ({ tableSettingsId, defaultSortBy, defaultOrder }: UseDataGridProps) => {
|
||||||
|
const orderSetting = `${tableSettingsId}.order`;
|
||||||
|
const hiddenColumnSetting = `${tableSettingsId}.hiddenColumns`;
|
||||||
|
const pageSetting = `${tableSettingsId}.page`;
|
||||||
|
const { getPref, setPref, setPrefs, deletePref } = useAuth();
|
||||||
|
const [sortBy, setSortBy] = React.useState<SortingState>(defaultSortBy ?? []);
|
||||||
|
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
|
||||||
|
const [columnOrder, setColumnOrder] = React.useState<string[]>(
|
||||||
|
getSavedColumnOrder(defaultOrder ?? [], tableSettingsId),
|
||||||
|
);
|
||||||
|
const [pageInfo, setPageInfo] = React.useState<PaginationState>(getDefaultSettings(tableSettingsId));
|
||||||
|
|
||||||
|
const setNewColumnOrder = React.useCallback(
|
||||||
|
(newOrder: string[]) => {
|
||||||
|
setColumnOrder(newOrder);
|
||||||
|
if (tableSettingsId) {
|
||||||
|
localStorage.setItem(orderSetting, JSON.stringify(newOrder));
|
||||||
|
setPref({ preference: orderSetting, value: newOrder.join(',') });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setPref],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetPreferences = React.useCallback(async () => {
|
||||||
|
if (tableSettingsId) {
|
||||||
|
localStorage.removeItem(orderSetting);
|
||||||
|
localStorage.removeItem(hiddenColumnSetting);
|
||||||
|
await deletePref([orderSetting, hiddenColumnSetting]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setColumnOrder(defaultOrder ?? []);
|
||||||
|
setColumnVisibility({});
|
||||||
|
}, [deletePref]);
|
||||||
|
|
||||||
|
const hideColumn = React.useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const newVisibility = { ...columnVisibility };
|
||||||
|
newVisibility[id] = false;
|
||||||
|
let hiddenColumnsArray = Object.entries(newVisibility)
|
||||||
|
.filter(([, value]) => !value)
|
||||||
|
.map(([key]) => key);
|
||||||
|
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
|
||||||
|
|
||||||
|
// New column order without hidden columns
|
||||||
|
let filteredColumnOrder = columnOrder.filter((columnId) => !hiddenColumnsArray.includes(columnId));
|
||||||
|
filteredColumnOrder = [...new Set(filteredColumnOrder)]; // Remove duplicates
|
||||||
|
|
||||||
|
setPrefs([
|
||||||
|
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
|
||||||
|
{ tag: orderSetting, value: filteredColumnOrder.join(',') },
|
||||||
|
]);
|
||||||
|
setColumnVisibility({ ...newVisibility });
|
||||||
|
setColumnOrder(filteredColumnOrder);
|
||||||
|
localStorage.setItem(orderSetting, JSON.stringify(filteredColumnOrder));
|
||||||
|
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenColumns: hiddenColumnsArray,
|
||||||
|
columnOrder: filteredColumnOrder,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[columnOrder, columnVisibility, setPrefs],
|
||||||
|
);
|
||||||
|
|
||||||
|
const unhideColumn = React.useCallback(
|
||||||
|
(id: string, newOrder: string[]) => {
|
||||||
|
const newVisibility = { ...columnVisibility };
|
||||||
|
newVisibility[id] = true;
|
||||||
|
let hiddenColumnsArray = Object.entries(newVisibility)
|
||||||
|
.filter(([, value]) => !value)
|
||||||
|
.map(([key]) => key);
|
||||||
|
hiddenColumnsArray = [...new Set(hiddenColumnsArray)]; // Remove duplicates
|
||||||
|
|
||||||
|
const newColumnOrder = [...new Set(newOrder)]; // Remove duplicates
|
||||||
|
|
||||||
|
setPrefs([
|
||||||
|
{ tag: hiddenColumnSetting, value: hiddenColumnsArray.join(',') },
|
||||||
|
{ tag: orderSetting, value: newColumnOrder.join(',') },
|
||||||
|
]);
|
||||||
|
setColumnVisibility({ ...newVisibility });
|
||||||
|
setColumnOrder(newColumnOrder);
|
||||||
|
localStorage.setItem(orderSetting, JSON.stringify(newColumnOrder));
|
||||||
|
localStorage.setItem(hiddenColumnSetting, hiddenColumnsArray.join(','));
|
||||||
|
|
||||||
|
return {
|
||||||
|
hiddenColumns: hiddenColumnsArray,
|
||||||
|
columnOrder: newColumnOrder,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[columnOrder, columnVisibility, setPrefs],
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const savedPrefs = getPref(hiddenColumnSetting);
|
||||||
|
|
||||||
|
if (savedPrefs) {
|
||||||
|
const savedHiddenColumns = savedPrefs.split(',');
|
||||||
|
setColumnVisibility(savedHiddenColumns.reduce((acc, curr) => ({ ...acc, [curr]: false }), {}));
|
||||||
|
} else {
|
||||||
|
setColumnVisibility({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedOrderSetting = getPref(orderSetting);
|
||||||
|
|
||||||
|
if (savedOrderSetting) {
|
||||||
|
const savedHiddenColumns = savedOrderSetting.split(',');
|
||||||
|
setColumnOrder(savedHiddenColumns);
|
||||||
|
}
|
||||||
|
}, [tableSettingsId]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (tableSettingsId) {
|
||||||
|
localStorage.setItem(pageSetting, String(pageInfo.pageIndex));
|
||||||
|
if (tableSettingsId) localStorage.setItem(`${tableSettingsId}`, String(pageInfo.pageSize));
|
||||||
|
}
|
||||||
|
}, [pageInfo.pageIndex, pageInfo.pageSize]);
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => ({
|
||||||
|
tableSettingsId,
|
||||||
|
pageInfo,
|
||||||
|
sortBy,
|
||||||
|
setSortBy,
|
||||||
|
columnOrder,
|
||||||
|
setColumnOrder: setNewColumnOrder,
|
||||||
|
hideColumn,
|
||||||
|
unhideColumn,
|
||||||
|
columnVisibility,
|
||||||
|
setColumnVisibility,
|
||||||
|
onPaginationChange: setPageInfo,
|
||||||
|
resetPreferences,
|
||||||
|
}),
|
||||||
|
[pageInfo, hideColumn, unhideColumn, columnVisibility, sortBy, columnOrder, setNewColumnOrder],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseDataGridReturn = ReturnType<typeof useDataGrid>;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@chakra-ui/react';
|
import { Icon } from '@chakra-ui/react';
|
||||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
|
|||||||
@@ -44,15 +44,18 @@ const defaultProps = {
|
|||||||
sortBy: [],
|
sortBy: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableProps = {
|
export type DataTableProps<TValue> = {
|
||||||
columns: readonly Column<object>[];
|
columns: Column<TValue>[];
|
||||||
data: object[];
|
data: TValue[];
|
||||||
count?: number;
|
count?: number;
|
||||||
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
setPageInfo?: React.Dispatch<React.SetStateAction<PageInfo | undefined>>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
onRowClick?: (row: TValue) => void;
|
||||||
|
isRowClickable?: (row: TValue) => boolean;
|
||||||
obj?: string;
|
obj?: string;
|
||||||
sortBy?: { id: string; desc: boolean }[];
|
sortBy?: { id: string; desc: boolean }[];
|
||||||
hiddenColumns?: string[];
|
hiddenColumns?: string[];
|
||||||
|
hideEmptyListText?: boolean;
|
||||||
hideControls?: boolean;
|
hideControls?: boolean;
|
||||||
minHeight?: string | number;
|
minHeight?: string | number;
|
||||||
fullScreen?: boolean;
|
fullScreen?: boolean;
|
||||||
@@ -67,7 +70,7 @@ type TableInstanceWithHooks<T extends object> = TableInstance<T> &
|
|||||||
state: UsePaginationState<T>;
|
state: UsePaginationState<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _DataTable = ({
|
const _DataTable = <TValue extends object>({
|
||||||
columns,
|
columns,
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -77,15 +80,19 @@ const _DataTable = ({
|
|||||||
sortBy,
|
sortBy,
|
||||||
hiddenColumns,
|
hiddenColumns,
|
||||||
hideControls,
|
hideControls,
|
||||||
|
hideEmptyListText,
|
||||||
count,
|
count,
|
||||||
setPageInfo,
|
setPageInfo,
|
||||||
isManual,
|
isManual,
|
||||||
saveSettingsId,
|
saveSettingsId,
|
||||||
showAllRows,
|
showAllRows,
|
||||||
}: DataTableProps) => {
|
onRowClick,
|
||||||
|
isRowClickable,
|
||||||
|
}: DataTableProps<TValue>) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
|
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||||
const getPageSize = () => {
|
const getPageSize = () => {
|
||||||
try {
|
try {
|
||||||
if (showAllRows) return 1000000;
|
if (showAllRows) return 1000000;
|
||||||
@@ -140,8 +147,12 @@ const _DataTable = ({
|
|||||||
},
|
},
|
||||||
useSortBy,
|
useSortBy,
|
||||||
usePagination,
|
usePagination,
|
||||||
) as TableInstanceWithHooks<object>;
|
) as TableInstanceWithHooks<TValue>;
|
||||||
|
|
||||||
|
const handleGoToPage = (newPage: number) => {
|
||||||
|
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(newPage));
|
||||||
|
gotoPage(newPage);
|
||||||
|
};
|
||||||
const handleNextPage = () => {
|
const handleNextPage = () => {
|
||||||
nextPage();
|
nextPage();
|
||||||
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
|
if (saveSettingsId) localStorage.setItem(`${saveSettingsId}.page`, String(pageIndex + 1));
|
||||||
@@ -253,10 +264,19 @@ const _DataTable = ({
|
|||||||
</Thead>
|
</Thead>
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<Tbody {...getTableBodyProps()}>
|
<Tbody {...getTableBodyProps()}>
|
||||||
{page.map((row: Row) => {
|
{page.map((row: Row<TValue>) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
|
const rowIsClickable = isRowClickable ? isRowClickable(row.original) : true;
|
||||||
|
const onClick = rowIsClickable && onRowClick ? () => onRowClick(row.original) : undefined;
|
||||||
return (
|
return (
|
||||||
<Tr {...row.getRowProps()} key={uuid()}>
|
<Tr
|
||||||
|
{...row.getRowProps()}
|
||||||
|
key={uuid()}
|
||||||
|
_hover={{
|
||||||
|
backgroundColor: hoveredRowBg,
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
row.cells.map((cell) => (
|
row.cells.map((cell) => (
|
||||||
@@ -275,8 +295,26 @@ const _DataTable = ({
|
|||||||
fontSize="14px"
|
fontSize="14px"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||||
// @ts-ignore
|
fontFamily={
|
||||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
// @ts-ignore
|
||||||
|
cell.column.isMonospace
|
||||||
|
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onClick={
|
||||||
|
// @ts-ignore
|
||||||
|
cell.column.stopPropagation || (cell.column.id === 'actions' && onRowClick)
|
||||||
|
? (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
cursor={
|
||||||
|
// @ts-ignore
|
||||||
|
!cell.column.stopPropagation && cell.column.id !== 'actions' && onRowClick
|
||||||
|
? 'pointer'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render('Cell')}
|
||||||
</Td>
|
</Td>
|
||||||
@@ -288,7 +326,7 @@ const _DataTable = ({
|
|||||||
</Tbody>
|
</Tbody>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</Table>
|
||||||
{!isLoading && data.length === 0 && (
|
{!isLoading && data.length === 0 && !hideEmptyListText && (
|
||||||
<Center>
|
<Center>
|
||||||
{obj ? (
|
{obj ? (
|
||||||
<Heading size="md" pt={12}>
|
<Heading size="md" pt={12}>
|
||||||
@@ -309,7 +347,7 @@ const _DataTable = ({
|
|||||||
<Tooltip label={t('table.first_page')}>
|
<Tooltip label={t('table.first_page')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Go to first page"
|
aria-label="Go to first page"
|
||||||
onClick={() => gotoPage(0)}
|
onClick={() => handleGoToPage(0)}
|
||||||
isDisabled={!canPreviousPage}
|
isDisabled={!canPreviousPage}
|
||||||
icon={<ArrowLeftIcon h={3} w={3} />}
|
icon={<ArrowLeftIcon h={3} w={3} />}
|
||||||
mr={4}
|
mr={4}
|
||||||
@@ -347,7 +385,7 @@ const _DataTable = ({
|
|||||||
max={pageOptions.length}
|
max={pageOptions.length}
|
||||||
onChange={(_: unknown, numberValue: number) => {
|
onChange={(_: unknown, numberValue: number) => {
|
||||||
const newPage = numberValue ? numberValue - 1 : 0;
|
const newPage = numberValue ? numberValue - 1 : 0;
|
||||||
gotoPage(newPage);
|
handleGoToPage(newPage);
|
||||||
}}
|
}}
|
||||||
defaultValue={pageIndex + 1}
|
defaultValue={pageIndex + 1}
|
||||||
>
|
>
|
||||||
@@ -386,7 +424,7 @@ const _DataTable = ({
|
|||||||
<Tooltip label={t('table.last_page')}>
|
<Tooltip label={t('table.last_page')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Go to last page"
|
aria-label="Go to last page"
|
||||||
onClick={() => gotoPage(pageCount - 1)}
|
onClick={() => handleGoToPage(pageCount - 1)}
|
||||||
isDisabled={!canNextPage}
|
isDisabled={!canNextPage}
|
||||||
icon={<ArrowRightIcon h={3} w={3} />}
|
icon={<ArrowRightIcon h={3} w={3} />}
|
||||||
ml={4}
|
ml={4}
|
||||||
@@ -401,4 +439,4 @@ const _DataTable = ({
|
|||||||
|
|
||||||
_DataTable.defaultProps = defaultProps;
|
_DataTable.defaultProps = defaultProps;
|
||||||
|
|
||||||
export const DataTable = React.memo(_DataTable);
|
export const DataTable = React.memo(_DataTable) as unknown as typeof _DataTable;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Icon } from '@chakra-ui/react';
|
import { Icon } from '@chakra-ui/react';
|
||||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const breakpoint = useBreakpoint();
|
const breakpoint = useBreakpoint();
|
||||||
|
const hoveredRowBg = useColorModeValue('gray.100', 'gray.600');
|
||||||
const textColor = useColorModeValue('gray.700', 'white');
|
const textColor = useColorModeValue('gray.700', 'white');
|
||||||
const getPageSize = () => {
|
const getPageSize = () => {
|
||||||
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
const saved = saveSettingsId ? localStorage.getItem(saveSettingsId) : undefined;
|
||||||
@@ -223,7 +224,13 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
{page.map((row: Row) => {
|
{page.map((row: Row) => {
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
return (
|
return (
|
||||||
<Tr {...row.getRowProps()} key={uuid()}>
|
<Tr
|
||||||
|
{...row.getRowProps()}
|
||||||
|
key={uuid()}
|
||||||
|
_hover={{
|
||||||
|
backgroundColor: hoveredRowBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
row.cells.map((cell) => (
|
row.cells.map((cell) => (
|
||||||
@@ -242,8 +249,12 @@ const SortableDataTable: React.FC<Props> = ({
|
|||||||
fontSize="14px"
|
fontSize="14px"
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
textAlign={cell.column.isCentered ? 'center' : undefined}
|
textAlign={cell.column.isCentered ? 'center' : undefined}
|
||||||
// @ts-ignore
|
fontFamily={
|
||||||
fontFamily={cell.column.isMonospace ? 'monospace' : undefined}
|
// @ts-ignore
|
||||||
|
cell.column.isMonospace
|
||||||
|
? 'Inter, SFMono-Regular, Menlo, Monaco, Consolas, monospace'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cell.render('Cell')}
|
{cell.render('Cell')}
|
||||||
</Td>
|
</Td>
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import { Heading } from '@chakra-ui/react';
|
|
||||||
import { Select } from 'chakra-react-select';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
|
||||||
import { useControllerDeviceSearch } from 'contexts/ControllerSocketProvider/hooks/Commands/useDeviceSearch';
|
|
||||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
|
||||||
|
|
||||||
const DeviceSearchBar = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { token } = useAuth();
|
|
||||||
const { startWebSocket, isWebSocketOpen } = useControllerStore((state) => ({
|
|
||||||
startWebSocket: state.startWebSocket,
|
|
||||||
isWebSocketOpen: state.isWebSocketOpen,
|
|
||||||
}));
|
|
||||||
const { inputValue, results, onInputChange } = useControllerDeviceSearch({
|
|
||||||
minLength: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const NoOptionsMessage = React.useCallback(
|
|
||||||
() => (
|
|
||||||
<Heading size="sm" textAlign="center">
|
|
||||||
{isWebSocketOpen ? t('common.no_devices_found') : `${t('controller.devices.connecting')}...`}
|
|
||||||
</Heading>
|
|
||||||
),
|
|
||||||
[t, isWebSocketOpen],
|
|
||||||
);
|
|
||||||
const onClick = React.useCallback((v: { value: string }) => {
|
|
||||||
navigate(`/devices/${v.value}`);
|
|
||||||
}, []);
|
|
||||||
const onChange = React.useCallback((v: string) => {
|
|
||||||
if ((v.length === 0 || v.match('^[a-fA-F0-9-*]+$')) && v.length <= 13) onInputChange(v);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onFocus = () => {
|
|
||||||
if (!isWebSocketOpen && token && token.length > 0) {
|
|
||||||
startWebSocket(token, 0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
chakraStyles={{
|
|
||||||
control: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
borderRadius: '15px',
|
|
||||||
color: 'unset',
|
|
||||||
}),
|
|
||||||
input: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
width: '140px',
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
backgroundColor: 'unset',
|
|
||||||
border: 'unset',
|
|
||||||
}),
|
|
||||||
menu: (provided) => ({
|
|
||||||
...provided,
|
|
||||||
color: 'black',
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
components={{ NoOptionsMessage }}
|
|
||||||
// @ts-ignore
|
|
||||||
options={results.map((v: string) => ({ label: v, value: v }))}
|
|
||||||
filterOption={() => true}
|
|
||||||
inputValue={inputValue}
|
|
||||||
value={inputValue}
|
|
||||||
placeholder={t('common.search')}
|
|
||||||
onInputChange={onChange}
|
|
||||||
onFocus={onFocus}
|
|
||||||
// @ts-ignore
|
|
||||||
onChange={onClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DeviceSearchBar;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { AddIcon } from '@chakra-ui/icons';
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
|
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
|
||||||
import { Trash } from 'phosphor-react';
|
import { Trash } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { DataTable } from '../../../DataTables/DataTable';
|
import { DataTable } from '../../../DataTables/DataTable';
|
||||||
|
|||||||
162
src/components/GlobalSearchBar/index.tsx
Normal file
162
src/components/GlobalSearchBar/index.tsx
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Tooltip, useColorMode, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
AsyncSelect,
|
||||||
|
ChakraStylesConfig,
|
||||||
|
GroupBase,
|
||||||
|
LoadingIndicatorProps,
|
||||||
|
OptionBase,
|
||||||
|
OptionsOrGroups,
|
||||||
|
chakraComponents,
|
||||||
|
} from 'chakra-react-select';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
|
import debounce from 'helpers/debounce';
|
||||||
|
import { getUsernameRadiusSessions } from 'hooks/Network/Radius';
|
||||||
|
|
||||||
|
const chakraStyles: (
|
||||||
|
colorMode: 'light' | 'dark',
|
||||||
|
) => ChakraStylesConfig<SearchOption, false, GroupBase<SearchOption>> = (colorMode) => ({
|
||||||
|
dropdownIndicator: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
width: '32px',
|
||||||
|
}),
|
||||||
|
placeholder: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
lineHeight: '1',
|
||||||
|
}),
|
||||||
|
container: (provided) => ({
|
||||||
|
...provided,
|
||||||
|
width: '320px',
|
||||||
|
backgroundColor: colorMode === 'light' ? 'white' : 'gray.600',
|
||||||
|
borderRadius: '15px',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SearchOption extends OptionBase {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
type: 'serial' | 'radius-username' | 'radius-mac';
|
||||||
|
}
|
||||||
|
|
||||||
|
const asyncComponents = {
|
||||||
|
LoadingIndicator: (props: LoadingIndicatorProps<SearchOption, false, GroupBase<SearchOption>>) => {
|
||||||
|
const { color, emptyColor } = useColorModeValue(
|
||||||
|
{
|
||||||
|
color: 'blue.500',
|
||||||
|
emptyColor: 'blue.100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: 'blue.300',
|
||||||
|
emptyColor: 'blue.900',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<chakraComponents.LoadingIndicator
|
||||||
|
color={color}
|
||||||
|
emptyColor={emptyColor}
|
||||||
|
speed="750ms"
|
||||||
|
spinnerSize="md"
|
||||||
|
thickness="3px"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalSearchBar = () => {
|
||||||
|
const { colorMode } = useColorMode();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const store = useControllerStore((state) => ({
|
||||||
|
searchSerialNumber: state.searchSerialNumber,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onNewSearch = React.useCallback(
|
||||||
|
async (v: string, callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void) => {
|
||||||
|
if (v.length < 3) return callback([]);
|
||||||
|
|
||||||
|
if (v.includes('rad:')) {
|
||||||
|
const trimmed = v.replace('rad:', '').trim();
|
||||||
|
if (trimmed.length < 3) return callback([]);
|
||||||
|
const cleaned = trimmed.toLowerCase();
|
||||||
|
return getUsernameRadiusSessions(cleaned)
|
||||||
|
.then((res) =>
|
||||||
|
callback(
|
||||||
|
res
|
||||||
|
.map((r) => ({
|
||||||
|
label: r.serialNumber,
|
||||||
|
value: r.serialNumber,
|
||||||
|
type: 'radius-username',
|
||||||
|
}))
|
||||||
|
.filter(({ value }, i, a) => a.findIndex((t) => t.value === value) === i) as SearchOption[],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.then(() => callback([]));
|
||||||
|
}
|
||||||
|
if (v.match('^[a-fA-F0-9-*]+$')) {
|
||||||
|
await store
|
||||||
|
.searchSerialNumber(v)
|
||||||
|
.then((res) => {
|
||||||
|
callback(
|
||||||
|
res.map((r) => ({
|
||||||
|
label: r,
|
||||||
|
value: r,
|
||||||
|
type: 'serial',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => []);
|
||||||
|
}
|
||||||
|
return callback([]);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedNewSearch = React.useCallback(
|
||||||
|
debounce(
|
||||||
|
// @ts-ignore
|
||||||
|
({
|
||||||
|
v,
|
||||||
|
callback,
|
||||||
|
}: {
|
||||||
|
v: string;
|
||||||
|
callback: (options: OptionsOrGroups<SearchOption, GroupBase<SearchOption>>) => void;
|
||||||
|
}) => {
|
||||||
|
onNewSearch(v as string, callback);
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const styles = React.useMemo(() => chakraStyles(colorMode), [colorMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={`Search serial numbers and radius clients. For radius clients you can either use the client's username (rad:client@client.com)
|
||||||
|
or use the client's station ID (rad:11:22:33:44:55:66)`}
|
||||||
|
shouldWrapChildren
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<AsyncSelect<SearchOption, false, GroupBase<SearchOption>>
|
||||||
|
name="global_search"
|
||||||
|
chakraStyles={styles}
|
||||||
|
closeMenuOnSelect
|
||||||
|
placeholder="Search MACs or radius clients"
|
||||||
|
components={asyncComponents}
|
||||||
|
loadOptions={(inputValue, callback) => {
|
||||||
|
debouncedNewSearch({ v: inputValue, callback });
|
||||||
|
}}
|
||||||
|
value={null}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
navigate(`/devices/${newValue.value}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GlobalSearchBar;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tooltip } from '@chakra-ui/react';
|
import { Tooltip } from '@chakra-ui/react';
|
||||||
import { compactDate, formatDaysAgo } from 'helpers/dateFormatting';
|
import { compactDate, formatDaysAgo, formatDaysAgoCompact } from 'helpers/dateFormatting';
|
||||||
|
|
||||||
type Props = { date?: number; hidePrefix?: boolean };
|
type Props = { date?: number; hidePrefix?: boolean; isCompact?: boolean };
|
||||||
|
|
||||||
const getDaysAgo = ({ date, hidePrefix }: { date?: number; hidePrefix?: boolean }) => {
|
const getDaysAgo = ({ date, hidePrefix, isCompact }: { date?: number; hidePrefix?: boolean; isCompact?: boolean }) => {
|
||||||
if (!date || date === 0) return '-';
|
if (!date || date === 0) return '-';
|
||||||
|
if (isCompact)
|
||||||
|
return hidePrefix ? formatDaysAgoCompact(date).split(' ').slice(1).join(' ') : formatDaysAgoCompact(date);
|
||||||
return hidePrefix ? formatDaysAgo(date).split(' ').slice(1).join(' ') : formatDaysAgo(date);
|
return hidePrefix ? formatDaysAgo(date).split(' ').slice(1).join(' ') : formatDaysAgo(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FormattedDate = ({ date, hidePrefix }: Props) => (
|
const FormattedDate = ({ date, hidePrefix, isCompact }: Props) => (
|
||||||
<Tooltip hasArrow placement="top" label={compactDate(date ?? 0)}>
|
<Tooltip hasArrow placement="top" label={compactDate(date ?? 0)}>
|
||||||
{getDaysAgo({ date, hidePrefix })}
|
{getDaysAgo({ date, hidePrefix, isCompact })}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PopoverHeader,
|
PopoverHeader,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Question } from 'phosphor-react';
|
import { Question } from '@phosphor-icons/react';
|
||||||
|
|
||||||
export type InfoPopoverProps = {
|
export type InfoPopoverProps = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { InfoIcon } from '@chakra-ui/icons';
|
import { InfoIcon } from '@chakra-ui/icons';
|
||||||
import { Heading, IconButton, LayoutProps, LightMode, SpaceProps, Spacer, Tooltip } from '@chakra-ui/react';
|
import { Heading, IconButton, LayoutProps, LightMode, SpaceProps, Spacer, Tooltip } from '@chakra-ui/react';
|
||||||
import { MagnifyingGlass } from 'phosphor-react';
|
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,14 @@ const LanguageSwitcher = () => {
|
|||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<Tooltip label={t('common.language')}>
|
<Tooltip label={t('common.language')}>
|
||||||
<MenuButton background="transparent" as={IconButton} aria-label="Commands" icon={languageIcon} size="sm" />
|
<MenuButton
|
||||||
|
background="transparent"
|
||||||
|
variant="ghost"
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Commands"
|
||||||
|
icon={languageIcon}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>
|
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>
|
||||||
|
|||||||
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
27
src/components/Maps/GoogleMap/Marker.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const _GoogleMapMarker = (options: google.maps.MarkerOptions) => {
|
||||||
|
const [marker, setMarker] = React.useState<google.maps.Marker>();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!marker) {
|
||||||
|
setMarker(new google.maps.Marker());
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (marker) {
|
||||||
|
marker.setMap(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [marker]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (marker) {
|
||||||
|
marker.setOptions(options);
|
||||||
|
}
|
||||||
|
}, [marker, options]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GoogleMapMarker = React.memo(_GoogleMapMarker);
|
||||||
89
src/components/Maps/GoogleMap/index.tsx
Normal file
89
src/components/Maps/GoogleMap/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { isLatLngLiteral } from '@googlemaps/typescript-guards';
|
||||||
|
import { createCustomEqual } from 'fast-equals';
|
||||||
|
|
||||||
|
const deepCompareEqualsForMaps = createCustomEqual((deepEqual) =>
|
||||||
|
// @ts-ignore
|
||||||
|
(a: number | google.maps.LatLng | google.maps.LatLngLiteral, b: number | google.maps.LatLng | google.maps.LatLngLiteral) => {
|
||||||
|
if (
|
||||||
|
isLatLngLiteral(a) ||
|
||||||
|
a instanceof google.maps.LatLng ||
|
||||||
|
isLatLngLiteral(b) ||
|
||||||
|
b instanceof google.maps.LatLng
|
||||||
|
) {
|
||||||
|
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
return deepEqual(a, b);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const useDeepCompareMemoize = (value: unknown) => {
|
||||||
|
const ref = React.useRef<unknown>();
|
||||||
|
|
||||||
|
if (!deepCompareEqualsForMaps(value, ref.current)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDeepCompareEffectForMaps = (callback: React.EffectCallback, dependencies: unknown[]) => {
|
||||||
|
React.useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface GoogleMapProps extends google.maps.MapOptions {
|
||||||
|
style: { [key: string]: string };
|
||||||
|
onClick?: (e: google.maps.MapMouseEvent) => void;
|
||||||
|
onIdle?: (map: google.maps.Map) => void;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _GoogleMap = ({ style, onClick, onIdle, children, ...options }: GoogleMapProps) => {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [map, setMap] = React.useState<google.maps.Map>();
|
||||||
|
|
||||||
|
// because React does not do deep comparisons, a custom hook is used
|
||||||
|
useDeepCompareEffectForMaps(() => {
|
||||||
|
if (map) {
|
||||||
|
map.setOptions(options);
|
||||||
|
}
|
||||||
|
}, [map, options]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (ref.current && !map) {
|
||||||
|
setMap(new window.google.maps.Map(ref.current, {}));
|
||||||
|
}
|
||||||
|
}, [ref, map]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (map) {
|
||||||
|
['click', 'idle'].forEach((eventName) => google.maps.event.clearListeners(map, eventName));
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
map.addListener('click', onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onIdle) {
|
||||||
|
map.addListener('idle', () => onIdle(map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [map, onClick, onIdle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={ref} style={style} />
|
||||||
|
{React.Children.map(children, (child) => {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
// set the map prop on the child component
|
||||||
|
// @ts-ignore
|
||||||
|
return React.cloneElement(child, { map });
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GoogleMap = React.memo(_GoogleMap);
|
||||||
@@ -5,17 +5,22 @@ import {
|
|||||||
AlertIcon,
|
AlertIcon,
|
||||||
AlertTitle,
|
AlertTitle,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Textarea,
|
Textarea,
|
||||||
useToast,
|
useToast,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
|
import { ClipboardText } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { SaveButton } from '../../Buttons/SaveButton';
|
import { SaveButton } from '../../Buttons/SaveButton';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
import { FileInputButton } from 'components/Buttons/FileInputButton';
|
||||||
import { useConfigureDevice } from 'hooks/Network/Commands';
|
import { useConfigureDevice } from 'hooks/Network/Commands';
|
||||||
|
import { useGetDevice } from 'hooks/Network/Devices';
|
||||||
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
export type ConfigureModalProps = {
|
export type ConfigureModalProps = {
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
@@ -29,11 +34,17 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const configure = useConfigureDevice({ serialNumber });
|
const configure = useConfigureDevice({ serialNumber });
|
||||||
|
const getDevice = useGetDevice({ serialNumber });
|
||||||
|
|
||||||
const [newConfig, setNewConfig] = React.useState('');
|
const [newConfig, setNewConfig] = React.useState('');
|
||||||
|
|
||||||
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const onChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
setNewConfig(e.target.value);
|
setNewConfig(e.target.value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onImportConfiguration = () => {
|
||||||
|
setNewConfig(getDevice.data?.configuration ? JSON.stringify(getDevice.data.configuration, null, 4) : '');
|
||||||
|
};
|
||||||
const isValid = React.useMemo(() => {
|
const isValid = React.useMemo(() => {
|
||||||
try {
|
try {
|
||||||
JSON.parse(newConfig);
|
JSON.parse(newConfig);
|
||||||
@@ -60,9 +71,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
modalProps.onClose();
|
modalProps.onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// console.log(e);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,10 +88,7 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<Box>
|
<Box>
|
||||||
<AlertTitle>{t('common.error')}</AlertTitle>
|
<AlertTitle>{t('common.error')}</AlertTitle>
|
||||||
{
|
<AlertDescription>{(configure.error as AxiosError)?.response?.data?.ErrorDescription}</AlertDescription>
|
||||||
// @ts-ignore
|
|
||||||
<AlertDescription>{configure.error?.response?.data?.ErrorDescription}</AlertDescription>
|
|
||||||
}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
@@ -92,15 +98,25 @@ export const ConfigureModal = ({ serialNumber, modalProps }: ConfigureModalProps
|
|||||||
</Alert>
|
</Alert>
|
||||||
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
<FormControl isInvalid={!isValid && newConfig.length > 0}>
|
||||||
<FormLabel>{t('configurations.one')}</FormLabel>
|
<FormLabel>{t('configurations.one')}</FormLabel>
|
||||||
<Box mb={2} w="240px">
|
<Flex mb={2}>
|
||||||
<FileInputButton
|
<Box w="240px">
|
||||||
value={newConfig}
|
<FileInputButton
|
||||||
setValue={(v) => setNewConfig(v)}
|
value={newConfig}
|
||||||
refreshId="1"
|
setValue={(v) => setNewConfig(v)}
|
||||||
accept=".json"
|
refreshId="1"
|
||||||
isStringFile
|
accept=".json"
|
||||||
/>
|
isStringFile
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
rightIcon={<ClipboardText size={20} />}
|
||||||
|
onClick={onImportConfiguration}
|
||||||
|
hidden={!getDevice.data}
|
||||||
|
ml={2}
|
||||||
|
>
|
||||||
|
Current Configuration
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
|
<Textarea height="auto" minH="600px" value={newConfig} onChange={onChange} />
|
||||||
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
<FormErrorMessage>{t('controller.configure.invalid')}</FormErrorMessage>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface Props {
|
|||||||
const FirmwareList: React.FC<Props> = ({ firmware, upgrade, isLoading }) => {
|
const FirmwareList: React.FC<Props> = ({ firmware, upgrade, isLoading }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const sortedFirmware = useMemo(() => firmware.sort((a, b) => b.created - a.created), [firmware]);
|
const sortedFirmware = useMemo(() => firmware.sort((a, b) => b.imageDate - a.imageDate), [firmware]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h="600px" w="100%" overflowY="auto" px={0}>
|
<Box h="600px" w="100%" overflowY="auto" px={0}>
|
||||||
@@ -34,10 +34,10 @@ const FirmwareList: React.FC<Props> = ({ firmware, upgrade, isLoading }) => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{sortedFirmware.map(({ created, size, revision, uri }) => (
|
{sortedFirmware.map(({ imageDate, size, revision, uri }) => (
|
||||||
<Tr key={uuid()}>
|
<Tr key={uuid()}>
|
||||||
<Td px={0} py={1} w="200px">
|
<Td px={0} py={1} w="200px">
|
||||||
{compactDate(created)}
|
{compactDate(imageDate)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td px={0} py={1} width="160px">
|
<Td px={0} py={1} width="160px">
|
||||||
{bytesString(size)}
|
{bytesString(size)}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
Switch,
|
Switch,
|
||||||
Heading,
|
Heading,
|
||||||
|
Text,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Formik, FormikProps } from 'formik';
|
import { Formik, FormikProps } from 'formik';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -32,6 +33,7 @@ export type FirmwareUpgradeModalProps = {
|
|||||||
|
|
||||||
export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNumber }: FirmwareUpgradeModalProps) => {
|
export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNumber }: FirmwareUpgradeModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [showDevFirmware, { toggle: toggleDev }] = useBoolean();
|
||||||
const [formKey, setFormKey] = React.useState(uuid());
|
const [formKey, setFormKey] = React.useState(uuid());
|
||||||
const ref = useRef<
|
const ref = useRef<
|
||||||
| FormikProps<{
|
| FormikProps<{
|
||||||
@@ -57,7 +59,8 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
upgrade({
|
upgrade({
|
||||||
keepRedirector: isRedirector,
|
keepRedirector: isRedirector,
|
||||||
uri,
|
uri,
|
||||||
signature: device?.restrictedDevice ? ref.current?.values?.signature : undefined,
|
signature:
|
||||||
|
device?.restrictedDevice && !device?.restrictionDetails?.developer ? ref.current?.values?.signature : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,7 +74,13 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
title={`${t('commands.firmware_upgrade')} #${serialNumber}`}
|
title={`${t('commands.firmware_upgrade')} #${serialNumber}`}
|
||||||
right={<CloseButton ml={2} onClick={closeModal} />}
|
right={
|
||||||
|
<>
|
||||||
|
<Text>{t('controller.firmware.show_dev_releases')}</Text>
|
||||||
|
<Switch mx={2} isChecked={showDevFirmware} onChange={toggleDev} size="lg" />
|
||||||
|
<CloseButton onClick={closeModal} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{isUpgrading || isFetchingDevice || isFetchingFirmware ? (
|
{isUpgrading || isFetchingDevice || isFetchingFirmware ? (
|
||||||
@@ -89,7 +98,7 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
|
<Switch isChecked={isRedirector} onChange={toggle} borderRadius="15px" size="lg" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{device?.restrictedDevice && (
|
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||||
<Formik<{ signature?: string }>
|
<Formik<{ signature?: string }>
|
||||||
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
|
innerRef={ref as Ref<FormikProps<{ signature?: string | undefined }>> | undefined}
|
||||||
key={formKey}
|
key={formKey}
|
||||||
@@ -103,7 +112,11 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
|||||||
</Formik>
|
</Formik>
|
||||||
)}
|
)}
|
||||||
{firmware?.firmwares && (
|
{firmware?.firmwares && (
|
||||||
<FirmwareList firmware={firmware.firmwares} upgrade={submit} isLoading={isUpgrading} />
|
<FirmwareList
|
||||||
|
firmware={firmware.firmwares.filter((firmw) => showDevFirmware || !firmw.revision.includes('devel'))}
|
||||||
|
upgrade={submit}
|
||||||
|
isLoading={isUpgrading}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
|
||||||
export interface ModalHeaderProps {
|
export interface ModalHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
left?: React.ReactNode;
|
||||||
right: React.ReactNode;
|
right: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, right }) => (
|
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
|
||||||
<Header>
|
const bg = useColorModeValue('blue.50', 'blue.700');
|
||||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
|
||||||
|
return (
|
||||||
|
<Header bg={bg}>
|
||||||
{title}
|
{title}
|
||||||
|
{left ? (
|
||||||
|
<HStack spacing={2} ml={2}>
|
||||||
|
{left}
|
||||||
|
</HStack>
|
||||||
|
) : null}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{right}
|
{right}
|
||||||
</Flex>
|
</Header>
|
||||||
</Header>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export const ModalHeader = React.memo(_ModalHeader);
|
export const ModalHeader = React.memo(_ModalHeader);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type ModalProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
topRightButtons?: React.ReactNode;
|
topRightButtons?: React.ReactNode;
|
||||||
|
tags?: React.ReactNode;
|
||||||
options?: {
|
options?: {
|
||||||
modalSize?: 'sm' | 'md' | 'lg';
|
modalSize?: 'sm' | 'md' | 'lg';
|
||||||
maxWidth?: LayoutProps['maxWidth'];
|
maxWidth?: LayoutProps['maxWidth'];
|
||||||
@@ -15,7 +16,7 @@ export type ModalProps = {
|
|||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }: ModalProps) => {
|
const _Modal = ({ isOpen, onClose, title, topRightButtons, tags, options, children }: ModalProps) => {
|
||||||
const maxWidth = React.useMemo(() => {
|
const maxWidth = React.useMemo(() => {
|
||||||
if (options?.maxWidth) return options.maxWidth;
|
if (options?.maxWidth) return options.maxWidth;
|
||||||
if (options?.modalSize === 'sm') return undefined;
|
if (options?.modalSize === 'sm') return undefined;
|
||||||
@@ -32,6 +33,7 @@ const _Modal = ({ isOpen, onClose, title, topRightButtons, options, children }:
|
|||||||
<ModalContent maxWidth={maxWidth}>
|
<ModalContent maxWidth={maxWidth}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
title={title}
|
title={title}
|
||||||
|
left={tags}
|
||||||
right={
|
right={
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
{topRightButtons}
|
{topRightButtons}
|
||||||
|
|||||||
@@ -1,40 +1,42 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { MenuItem, useToast } from '@chakra-ui/react';
|
import { Alert, AlertIcon, Box, Button, Center, useToast } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Modal } from '../Modal';
|
||||||
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
import { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||||
import { useRebootDevice } from 'hooks/Network/Devices';
|
import { useRebootDevice } from 'hooks/Network/Devices';
|
||||||
import { useMutationResult } from 'hooks/useMutationResult';
|
import { useMutationResult } from 'hooks/useMutationResult';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
import { GatewayDevice } from 'models/Device';
|
|
||||||
|
|
||||||
type Props = {
|
export type RebootModalProps = {
|
||||||
device: GatewayDevice;
|
serialNumber: string;
|
||||||
refresh: () => void;
|
modalProps: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RebootMenuItem = ({ device, refresh }: Props) => {
|
export const RebootModal = ({ serialNumber, modalProps }: RebootModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||||
const { mutateAsync: reboot } = useRebootDevice({ serialNumber: device.serialNumber });
|
const { mutateAsync: reboot, isLoading } = useRebootDevice({ serialNumber });
|
||||||
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
|
const { onSuccess: onRebootSuccess, onError: onRebootError } = useMutationResult({
|
||||||
objName: t('devices.one'),
|
objName: t('devices.one'),
|
||||||
operationType: 'reboot',
|
operationType: 'reboot',
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
refresh();
|
|
||||||
addEventListeners([
|
addEventListeners([
|
||||||
{
|
{
|
||||||
id: `device-connection-${device.serialNumber}`,
|
id: `device-connection-${serialNumber}`,
|
||||||
type: 'DEVICE_CONNECTION',
|
type: 'DEVICE_CONNECTION',
|
||||||
serialNumber: device.serialNumber,
|
serialNumber,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const id = `device-connection-notification-${device.serialNumber}`;
|
const id = `device-connection-notification-${serialNumber}`;
|
||||||
|
|
||||||
if (!toast.isActive(id)) {
|
if (!toast.isActive(id)) {
|
||||||
toast({
|
toast({
|
||||||
id,
|
id,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
description: t('controller.devices.finished_reboot', { serialNumber: device.serialNumber }),
|
description: t('controller.devices.finished_reboot', { serialNumber }),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -44,17 +46,17 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: `device-disconnected-${device.serialNumber}`,
|
id: `device-disconnected-${serialNumber}`,
|
||||||
type: 'DEVICE_DISCONNECTION',
|
type: 'DEVICE_DISCONNECTION',
|
||||||
serialNumber: device.serialNumber,
|
serialNumber,
|
||||||
callback: () => {
|
callback: () => {
|
||||||
const id = `device-disconnection-notification-${device.serialNumber}`;
|
const id = `device-disconnection-notification-${serialNumber}`;
|
||||||
|
|
||||||
if (!toast.isActive(id)) {
|
if (!toast.isActive(id)) {
|
||||||
toast({
|
toast({
|
||||||
id,
|
id,
|
||||||
title: t('common.success'),
|
title: t('common.success'),
|
||||||
description: t('controller.devices.started_reboot', { serialNumber: device.serialNumber }),
|
description: t('controller.devices.started_reboot', { serialNumber }),
|
||||||
status: 'success',
|
status: 'success',
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
isClosable: true,
|
isClosable: true,
|
||||||
@@ -66,17 +68,39 @@ const RebootMenuItem = ({ device, refresh }: Props) => {
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRebootClick = () =>
|
const handleRebootClick = () =>
|
||||||
reboot(undefined, {
|
reboot(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
onRebootSuccess();
|
onRebootSuccess();
|
||||||
|
modalProps.onClose();
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
onRebootError(e as AxiosError);
|
onRebootError(e as AxiosError);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return <MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>;
|
return (
|
||||||
|
<Modal
|
||||||
|
{...modalProps}
|
||||||
|
title={t('commands.reboot')}
|
||||||
|
topRightButtons={
|
||||||
|
<Button colorScheme="blue" onClick={handleRebootClick} isLoading={isLoading}>
|
||||||
|
{t('commands.reboot')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
options={{
|
||||||
|
modalSize: 'sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Center mb={2}>
|
||||||
|
<Alert status="info" w="unset">
|
||||||
|
<AlertIcon />
|
||||||
|
{t('commands.reboot_description')}
|
||||||
|
</Alert>
|
||||||
|
</Center>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RebootMenuItem;
|
|
||||||
@@ -183,7 +183,9 @@ const CustomScriptForm = ({
|
|||||||
<>
|
<>
|
||||||
<Flex>
|
<Flex>
|
||||||
<Box>
|
<Box>
|
||||||
{device?.restrictedDevice && <SignatureField name="signature" isDisabled={areFieldsDisabled} />}
|
{device?.restrictedDevice && !device?.restrictionDetails?.developer && (
|
||||||
|
<SignatureField name="signature" isDisabled={areFieldsDisabled} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
useBoolean,
|
useBoolean,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { UploadSimple } from 'phosphor-react';
|
import { UploadSimple } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useFastField } from 'hooks/useFastField';
|
import { useFastField } from 'hooks/useFastField';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { FormikProps } from 'formik';
|
import { FormikProps } from 'formik';
|
||||||
import { ArrowLeft } from 'phosphor-react';
|
import { ArrowLeft } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
||||||
@@ -60,8 +60,14 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
let requestData: {
|
let requestData: {
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
script?: string;
|
||||||
timeout?: number | undefined;
|
timeout?: number | undefined;
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
|
if (requestData.script) {
|
||||||
|
requestData.script = btoa(requestData.script);
|
||||||
|
}
|
||||||
|
|
||||||
if (selectedScript === 'diagnostics') {
|
if (selectedScript === 'diagnostics') {
|
||||||
requestData = {
|
requestData = {
|
||||||
serialNumber: device?.serialNumber ?? '',
|
serialNumber: device?.serialNumber ?? '',
|
||||||
@@ -76,7 +82,7 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
when: 0,
|
when: 0,
|
||||||
deferred: data.deferred,
|
deferred: data.deferred,
|
||||||
timeout: data.timeout,
|
timeout: data.timeout,
|
||||||
signature: device?.restrictedDevice ? data.signature : undefined,
|
signature: device?.restrictedDevice && !device?.restrictionDetails?.developer ? data.signature : undefined,
|
||||||
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
|
uri: data.defaultUploadURI && data.defaultUploadURI?.length > 0 ? data.defaultUploadURI : undefined,
|
||||||
scriptId: selectedScript,
|
scriptId: selectedScript,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
@@ -88,6 +94,19 @@ export const ScriptModal = ({ device, modalProps }: ScriptModalProps) => {
|
|||||||
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
setValue(response.results?.status?.result ?? JSON.stringify(response.results ?? {}, null, 2));
|
||||||
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
|
queryClient.invalidateQueries(['commands', device?.serialNumber ?? '']);
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e) && e.response?.data?.ErrorDescription) {
|
||||||
|
toast({
|
||||||
|
id: 'script-update-error',
|
||||||
|
title: t('common.error'),
|
||||||
|
description: e.response?.data?.ErrorDescription,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!waitForResponse) {
|
if (!waitForResponse) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
Switch,
|
Switch,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ArrowLeft } from 'phosphor-react';
|
import { ArrowLeft } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
import { useDownloadTrace, useTrace } from 'hooks/Network/Trace';
|
import { useDownloadTrace, useTrace } from 'hooks/Network/Trace';
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
useColorMode,
|
useColorMode,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { JsonViewer } from '@textea/json-viewer';
|
import { JsonViewer } from '@textea/json-viewer';
|
||||||
import { ArrowLeft } from 'phosphor-react';
|
import { ArrowLeft } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { Card } from 'components/Containers/Card';
|
import { Card } from 'components/Containers/Card';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { Modal, ModalOverlay, ModalContent, ModalBody, Center, Spinner } from '@chakra-ui/react';
|
import { Modal, ModalOverlay, ModalContent, ModalBody, Center, Spinner } from '@chakra-ui/react';
|
||||||
import { ArrowLeft, Download, Gauge } from 'phosphor-react';
|
import { ArrowLeft, Download, Gauge } from '@phosphor-icons/react';
|
||||||
import { CSVLink } from 'react-csv';
|
import { CSVLink } from 'react-csv';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
||||||
@@ -57,7 +57,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
if (isOpen) resetData();
|
if (isOpen) resetData();
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
return (
|
return (
|
||||||
(<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
<Modal onClose={closeModal} isOpen={isOpen} size="xl" scrollBehavior="inside">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
<ModalContent maxWidth={{ sm: '600px', md: '700px', lg: '800px', xl: '50%' }}>
|
||||||
<ModalHeader
|
<ModalHeader
|
||||||
@@ -66,7 +66,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
<>
|
<>
|
||||||
{csvData ? (
|
{csvData ? (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(<CSVLink
|
<CSVLink
|
||||||
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
filename={`wifi_scan_${serialNumber}_${dateForFilename(new Date().getTime() / 1000)}.csv`}
|
||||||
data={csvData as object[]}
|
data={csvData as object[]}
|
||||||
>
|
>
|
||||||
@@ -77,7 +77,7 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
label={t('common.download')}
|
label={t('common.download')}
|
||||||
onClick={() => {}}
|
onClick={() => {}}
|
||||||
/>
|
/>
|
||||||
</CSVLink>)
|
</CSVLink>
|
||||||
) : (
|
) : (
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
color="gray"
|
color="gray"
|
||||||
@@ -118,6 +118,6 @@ export const WifiScanModal = ({ modalProps: { isOpen, onClose }, serialNumber }:
|
|||||||
confirm={closeCancelAndForm}
|
confirm={closeCancelAndForm}
|
||||||
cancel={closeConfirm}
|
cancel={closeConfirm}
|
||||||
/>
|
/>
|
||||||
</Modal>)
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { Box, BoxProps } from '@chakra-ui/react';
|
||||||
import { bytesString } from 'helpers/stringHelper';
|
import { bytesString } from 'helpers/stringHelper';
|
||||||
|
|
||||||
const DataCell: React.FC<{ bytes?: number }> = ({ bytes }) => {
|
type Props = { bytes?: number; showZerosAs?: string; boxProps?: BoxProps };
|
||||||
|
const DataCell = ({ bytes, showZerosAs, boxProps }: Props) => {
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (bytes === undefined) return '-';
|
if (bytes === undefined) return '-';
|
||||||
|
if (showZerosAs && bytes === 0) return showZerosAs;
|
||||||
return bytesString(bytes);
|
return bytesString(bytes);
|
||||||
}, [bytes]);
|
}, [bytes]);
|
||||||
|
|
||||||
return <div>{data}</div>;
|
return <Box {...boxProps}>{data}</Box>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(DataCell);
|
export default React.memo(DataCell);
|
||||||
|
|||||||
17
src/components/TableCells/DurationCell/index.tsx
Normal file
17
src/components/TableCells/DurationCell/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { secondsDuration } from 'helpers/dateFormatting';
|
||||||
|
|
||||||
|
const DurationCell: React.FC<{ seconds?: number }> = ({ seconds }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const data = useMemo(() => {
|
||||||
|
if (seconds === undefined) return '-';
|
||||||
|
|
||||||
|
return secondsDuration(seconds, t);
|
||||||
|
}, [seconds]);
|
||||||
|
|
||||||
|
return <div>{data}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(DurationCell);
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
|
import { Box, BoxProps } from '@chakra-ui/react';
|
||||||
|
|
||||||
const NumberCell = ({ value }: { value?: number }) => {
|
type Props = {
|
||||||
const data = useMemo(() => {
|
value?: number;
|
||||||
|
boxProps?: BoxProps;
|
||||||
|
showZerosAs?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NumberCell = ({ value, boxProps, showZerosAs }: Props) => {
|
||||||
|
const getData = () => {
|
||||||
if (value === undefined) return '-';
|
if (value === undefined) return '-';
|
||||||
|
if (value === 0 && showZerosAs) return showZerosAs;
|
||||||
return value.toLocaleString();
|
return value.toLocaleString();
|
||||||
}, [value]);
|
};
|
||||||
|
|
||||||
return <div>{data}</div>;
|
return <Box {...boxProps}>{getData()}</Box>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(NumberCell);
|
export default React.memo(NumberCell);
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const setPref = ({ preference, value }: { preference: string; value: string }) => {
|
const setPref = async ({ preference, value }: { preference: string; value: string }) => {
|
||||||
let updated = false;
|
let updated = false;
|
||||||
if (preferences) {
|
if (preferences) {
|
||||||
const newPreferences: Preference[] = preferences.map((pref: Preference) => {
|
const newPreferences: Preference[] = preferences.map((pref: Preference) => {
|
||||||
@@ -102,15 +102,41 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
|||||||
|
|
||||||
if (!updated) newPreferences.push({ tag: preference, value });
|
if (!updated) newPreferences.push({ tag: preference, value });
|
||||||
|
|
||||||
updatePreferences.mutateAsync(newPreferences);
|
await updatePreferences.mutateAsync(newPreferences);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePref = (preference: string) => {
|
const setPrefs = async (preferencesToUpdate: Preference[]) => {
|
||||||
if (preferences) {
|
if (preferences) {
|
||||||
const newPreferences: Preference[] = preferences.filter((pref: Preference) => pref.tag !== preference);
|
const updatedPreferences: string[] = [];
|
||||||
|
const newPreferences = preferences.map((pref: Preference) => {
|
||||||
|
const preferenceToUpdate = preferencesToUpdate.find(
|
||||||
|
(prefToUpdate: Preference) => prefToUpdate.tag === pref.tag,
|
||||||
|
);
|
||||||
|
if (preferenceToUpdate) {
|
||||||
|
updatedPreferences.push(pref.tag);
|
||||||
|
return { tag: pref.tag, value: preferenceToUpdate.value };
|
||||||
|
}
|
||||||
|
return pref;
|
||||||
|
});
|
||||||
|
|
||||||
updatePreferences.mutateAsync(newPreferences);
|
for (const preferenceToUpdate of preferencesToUpdate) {
|
||||||
|
if (!updatedPreferences.includes(preferenceToUpdate.tag)) {
|
||||||
|
newPreferences.push(preferenceToUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updatePreferences.mutateAsync(newPreferences);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePref = async (preference: string | string[]) => {
|
||||||
|
if (preferences) {
|
||||||
|
const newPreferences: Preference[] = preferences.filter((pref: Preference) =>
|
||||||
|
typeof preference === 'string' ? pref.tag !== preference : !preference.includes(pref.tag),
|
||||||
|
);
|
||||||
|
|
||||||
|
await updatePreferences.mutateAsync(newPreferences);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +172,7 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
|||||||
ref,
|
ref,
|
||||||
getPref,
|
getPref,
|
||||||
setPref,
|
setPref,
|
||||||
|
setPrefs,
|
||||||
deletePref,
|
deletePref,
|
||||||
endpoints,
|
endpoints,
|
||||||
configurationDescriptions,
|
configurationDescriptions,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { axiosProv } from 'constants/axiosInstances';
|
import { axiosProv } from 'constants/axiosInstances';
|
||||||
|
import { Preference } from 'models/Preference';
|
||||||
import { User } from 'models/User';
|
import { User } from 'models/User';
|
||||||
|
|
||||||
const getConfigDescriptions = async (baseUrl: string) =>
|
const getConfigDescriptions = async (baseUrl: string) =>
|
||||||
@@ -26,7 +27,8 @@ export interface AuthProviderReturn {
|
|||||||
logout: () => void;
|
logout: () => void;
|
||||||
getPref: (preference: string) => string | null;
|
getPref: (preference: string) => string | null;
|
||||||
setPref: ({ preference, value }: { preference: string; value: string }) => void;
|
setPref: ({ preference, value }: { preference: string; value: string }) => void;
|
||||||
deletePref: (preference: string) => void;
|
setPrefs: (preferencesToUpdate: Preference[]) => void;
|
||||||
|
deletePref: (preference: string | string[]) => void;
|
||||||
ref: React.MutableRefObject<undefined>;
|
ref: React.MutableRefObject<undefined>;
|
||||||
endpoints: { [key: string]: string } | null;
|
endpoints: { [key: string]: string } | null;
|
||||||
configurationDescriptions: Record<string, unknown>;
|
configurationDescriptions: Record<string, unknown>;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
|
|||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import { ControllerSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
import { ControllerSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
|
import { randomIntId } from 'helpers/stringHelper';
|
||||||
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
||||||
import { NotificationType } from 'models/Socket';
|
import { NotificationType } from 'models/Socket';
|
||||||
|
|
||||||
@@ -122,6 +123,7 @@ export type ControllerStoreState = {
|
|||||||
lastSearchResults: string[];
|
lastSearchResults: string[];
|
||||||
setLastSearchResults: (result: string[]) => void;
|
setLastSearchResults: (result: string[]) => void;
|
||||||
errors: { str: string; timestamp: Date }[];
|
errors: { str: string; timestamp: Date }[];
|
||||||
|
searchSerialNumber: (serialNumber: string, timeout?: number) => Promise<string[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||||
@@ -149,6 +151,8 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
connectedDevices: msg.statistics.numberOfDevices,
|
connectedDevices: msg.statistics.numberOfDevices,
|
||||||
connectingDevices: msg.statistics.numberOfConnectingDevices,
|
connectingDevices: msg.statistics.numberOfConnectingDevices,
|
||||||
averageConnectionTime: msg.statistics.averageConnectedTime,
|
averageConnectionTime: msg.statistics.averageConnectedTime,
|
||||||
|
tx: msg.statistics.tx,
|
||||||
|
rx: msg.statistics.rx,
|
||||||
},
|
},
|
||||||
queryClient,
|
queryClient,
|
||||||
);
|
);
|
||||||
@@ -167,13 +171,23 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
id: uuid(),
|
id: uuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventsToFire = get().eventListeners.filter(
|
const eventsToFire = get().eventListeners.filter((event) => {
|
||||||
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
if (event.type === 'DEVICE_CONNECTION' || event.type === 'DEVICE_DISCONNECTION') {
|
||||||
);
|
return event.serialNumber === msg.serialNumber;
|
||||||
|
}
|
||||||
|
if (msg.type === 'DEVICE_SEARCH_RESULTS' && event.type === 'DEVICE_SEARCH_RESULTS') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
if (eventsToFire.length > 0) {
|
if (eventsToFire.length > 0) {
|
||||||
for (const event of eventsToFire) {
|
for (const event of eventsToFire) {
|
||||||
event.callback();
|
if (event.type === 'DEVICE_SEARCH_RESULTS' && msg.type === 'DEVICE_SEARCH_RESULTS') {
|
||||||
|
event.callback(msg.serialNumbers);
|
||||||
|
} else if (event.type === 'DEVICE_CONNECTION' || event.type === 'DEVICE_DISCONNECTION') {
|
||||||
|
event.callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return set((state) => ({
|
return set((state) => ({
|
||||||
@@ -209,6 +223,37 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
|||||||
const ws = get().webSocket;
|
const ws = get().webSocket;
|
||||||
if (ws) ws.send(str);
|
if (ws) ws.send(str);
|
||||||
},
|
},
|
||||||
|
searchSerialNumber: async (serialNumber: string, timeout = 1000 * 5): Promise<string[]> =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
reject(new Error(`Promise timed out after ${timeout} ms`));
|
||||||
|
}, timeout);
|
||||||
|
const ws = get().webSocket;
|
||||||
|
if (ws) {
|
||||||
|
const id = randomIntId();
|
||||||
|
get().addEventListeners([
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
type: 'DEVICE_SEARCH_RESULTS',
|
||||||
|
searchId: id,
|
||||||
|
callback: (serialNumbers: string[]) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(serialNumbers);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
command: 'serial_number_search',
|
||||||
|
serial_prefix: serialNumber,
|
||||||
|
id: randomIntId(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(new Error('No websocket connection'));
|
||||||
|
}
|
||||||
|
}),
|
||||||
startWebSocket: (token: string, tries = 0) => {
|
startWebSocket: (token: string, tries = 0) => {
|
||||||
const newTries = tries + 1;
|
const newTries = tries + 1;
|
||||||
if (tries <= 10) {
|
if (tries <= 10) {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ type ConnectionStatisticsMessage = {
|
|||||||
numberOfDevices: number;
|
numberOfDevices: number;
|
||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
|
tx: number;
|
||||||
|
rx: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
serialNumbers?: undefined;
|
serialNumbers?: undefined;
|
||||||
@@ -85,6 +87,8 @@ export type SocketWebSocketNotificationData =
|
|||||||
numberOfDevices: number;
|
numberOfDevices: number;
|
||||||
numberOfConnectingDevices: number;
|
numberOfConnectingDevices: number;
|
||||||
averageConnectedTime: number;
|
averageConnectedTime: number;
|
||||||
|
rx: number;
|
||||||
|
tx: number;
|
||||||
};
|
};
|
||||||
serialNumber?: undefined;
|
serialNumber?: undefined;
|
||||||
log?: undefined;
|
log?: undefined;
|
||||||
@@ -112,9 +116,16 @@ export type SocketWebSocketNotificationData =
|
|||||||
log?: undefined;
|
log?: undefined;
|
||||||
message: InitialSocketMessage;
|
message: InitialSocketMessage;
|
||||||
};
|
};
|
||||||
export type SocketEventCallback = {
|
export type SocketEventCallback =
|
||||||
id: string;
|
| {
|
||||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
id: string;
|
||||||
serialNumber: string;
|
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
||||||
callback: () => void;
|
serialNumber: string;
|
||||||
};
|
callback: () => void;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
id: string;
|
||||||
|
type: 'DEVICE_SEARCH_RESULTS';
|
||||||
|
searchId: number;
|
||||||
|
callback: (serialNumbers: string[]) => void;
|
||||||
|
};
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { TOptions } from 'i18next';
|
import { TOptions } from 'i18next';
|
||||||
import { X } from 'phosphor-react';
|
import { X } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
||||||
|
|||||||
@@ -122,7 +122,44 @@ export const getHoursAgo = (hoursAgo = 1, date = new Date()) => {
|
|||||||
export const dateForFilename = (dateString: number) => {
|
export const dateForFilename = (dateString: number) => {
|
||||||
const convertedTimestamp = unixToDateString(dateString);
|
const convertedTimestamp = unixToDateString(dateString);
|
||||||
const date = new Date(convertedTimestamp);
|
const date = new Date(convertedTimestamp);
|
||||||
return `${date.getFullYear()}_${twoDigitNumber(date.getMonth() + 1)}_${twoDigitNumber(
|
return `${date.getFullYear()}.${twoDigitNumber(date.getMonth() + 1)}.${twoDigitNumber(
|
||||||
date.getDate(),
|
date.getDate(),
|
||||||
)}_${twoDigitNumber(date.getHours())}h${twoDigitNumber(date.getMinutes())}m${twoDigitNumber(date.getSeconds())}s`;
|
)}-${twoDigitNumber(date.getHours())}.${twoDigitNumber(date.getMinutes())}.${twoDigitNumber(date.getSeconds())}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatDaysAgoCompact = (d1: number, d2: number = new Date().getTime()) => {
|
||||||
|
try {
|
||||||
|
const convertedTimestamp = unixToDateString(d1);
|
||||||
|
const date = new Date(convertedTimestamp).getTime();
|
||||||
|
const elapsed = date - d2;
|
||||||
|
|
||||||
|
for (const key of Object.keys(UNITS)) {
|
||||||
|
if (
|
||||||
|
Math.abs(elapsed) > UNITS[key as 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'] ||
|
||||||
|
key === 'second'
|
||||||
|
) {
|
||||||
|
const result = RTF.format(
|
||||||
|
Math.round(elapsed / UNITS[key as 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second']),
|
||||||
|
key as Intl.RelativeTimeFormatUnit,
|
||||||
|
);
|
||||||
|
return result
|
||||||
|
.replace(' years', 'y')
|
||||||
|
.replace(' year', 'y')
|
||||||
|
.replace(' months', 'm')
|
||||||
|
.replace(' month', 'm')
|
||||||
|
.replace(' days', 'd')
|
||||||
|
.replace(' day', 'd')
|
||||||
|
.replace(' hours', 'h')
|
||||||
|
.replace(' hour', 'h')
|
||||||
|
.replace(' minutes', 'm')
|
||||||
|
.replace(' minute', 'm')
|
||||||
|
.replace(' seconds', 's')
|
||||||
|
.replace(' second', 's');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return compactDate(date);
|
||||||
|
} catch {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -110,6 +110,18 @@ export const useDeleteCommand = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSingleCommandHistory = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
||||||
|
useQuery(
|
||||||
|
['commands', serialNumber, commandId],
|
||||||
|
() =>
|
||||||
|
axiosGw
|
||||||
|
.get(`command/${commandId}?serialNumber=${serialNumber}`)
|
||||||
|
.then((response) => response.data as DeviceCommandHistory),
|
||||||
|
{
|
||||||
|
enabled: serialNumber !== undefined && serialNumber !== '' && commandId !== undefined && commandId !== '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export type EventQueueResponse = {
|
export type EventQueueResponse = {
|
||||||
UUID: string;
|
UUID: string;
|
||||||
attachFile: number;
|
attachFile: number;
|
||||||
@@ -245,6 +257,7 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
|||||||
queryClient.invalidateQueries(['commands', serialNumber]);
|
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||||
},
|
},
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
|
queryClient.invalidateQueries(['commands', serialNumber]);
|
||||||
if (axios.isAxiosError(e)) {
|
if (axios.isAxiosError(e)) {
|
||||||
toast({
|
toast({
|
||||||
id: 'script-error',
|
id: 'script-error',
|
||||||
@@ -263,14 +276,44 @@ export const useDeviceScript = ({ serialNumber }: { serialNumber: string }) => {
|
|||||||
const downloadScript = (serialNumber: string, commandId: string) =>
|
const downloadScript = (serialNumber: string, commandId: string) =>
|
||||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
export const useDownloadScriptResult = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||||
useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return useQuery(['download-script', serialNumber, commandId], () => downloadScript(serialNumber, commandId), {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = window.URL.createObjectURL(blob);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
link.download = `Script_${commandId}.tar.gz`;
|
const headerLine =
|
||||||
|
(response.headers['content-disposition'] as string | undefined) ??
|
||||||
|
(response.headers['content-disposition'] as string | undefined);
|
||||||
|
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Script_${commandId}.tar.gz`;
|
||||||
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const bufferResponse = e.response?.data;
|
||||||
|
let errorMessage = '';
|
||||||
|
// If the response is a buffer, parse to JSON object
|
||||||
|
if (bufferResponse instanceof ArrayBuffer) {
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||||
|
errorMessage = json.ErrorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: `script-download-error-${serialNumber}`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: errorMessage,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,8 +11,10 @@ export type DeviceLog = {
|
|||||||
severity: number;
|
severity: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDeviceLogs = (limit: number, serialNumber?: string) => async () =>
|
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1 | 2) => async () =>
|
||||||
axiosGw.get(`device/${serialNumber}/logs?newest=true&limit=${limit}`).then((response) => response.data) as Promise<{
|
axiosGw
|
||||||
|
.get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`)
|
||||||
|
.then((response) => response.data) as Promise<{
|
||||||
values: DeviceLog[];
|
values: DeviceLog[];
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -21,20 +23,29 @@ export const useGetDeviceLogs = ({
|
|||||||
serialNumber,
|
serialNumber,
|
||||||
limit,
|
limit,
|
||||||
onError,
|
onError,
|
||||||
|
logType,
|
||||||
}: {
|
}: {
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
|
logType?: 0 | 1 | 2;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['devicelogs', serialNumber, { limit }], getDeviceLogs(limit, serialNumber), {
|
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 30000,
|
staleTime: 30000,
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteLogs = async ({ serialNumber, endDate }: { serialNumber: string; endDate: number }) =>
|
const deleteLogs = async ({
|
||||||
axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}`);
|
serialNumber,
|
||||||
|
endDate,
|
||||||
|
logType,
|
||||||
|
}: {
|
||||||
|
serialNumber: string;
|
||||||
|
endDate: number;
|
||||||
|
logType: 0 | 1 | 2;
|
||||||
|
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
|
||||||
export const useDeleteLogs = () => {
|
export const useDeleteLogs = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -45,46 +56,62 @@ export const useDeleteLogs = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLogsBatch = (serialNumber?: string, start?: number, end?: number, limit?: number, offset?: number) =>
|
const getLogsBatch = (
|
||||||
|
serialNumber?: string,
|
||||||
|
start?: number,
|
||||||
|
end?: number,
|
||||||
|
limit?: number,
|
||||||
|
offset?: number,
|
||||||
|
logType?: 0 | 1 | 2,
|
||||||
|
) =>
|
||||||
axiosGw
|
axiosGw
|
||||||
.get(`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}`)
|
.get(
|
||||||
|
`device/${serialNumber}/logs?startDate=${start}&endDate=${end}&limit=${limit}&offset=${offset}&logType=${logType}`,
|
||||||
|
)
|
||||||
.then((response) => response.data) as Promise<{
|
.then((response) => response.data) as Promise<{
|
||||||
values: DeviceLog[];
|
values: DeviceLog[];
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
const getDeviceLogsWithTimestamps = (serialNumber?: string, start?: number, end?: number) => async () => {
|
const getDeviceLogsWithTimestamps =
|
||||||
let offset = 0;
|
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1 | 2) => async () => {
|
||||||
const limit = 100;
|
let offset = 0;
|
||||||
let logs: DeviceLog[] = [];
|
const limit = 100;
|
||||||
let latestResponse: {
|
let logs: DeviceLog[] = [];
|
||||||
values: DeviceLog[];
|
let latestResponse: {
|
||||||
serialNumber: string;
|
values: DeviceLog[];
|
||||||
|
serialNumber: string;
|
||||||
|
};
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset, logType);
|
||||||
|
logs = logs.concat(latestResponse.values);
|
||||||
|
offset += limit;
|
||||||
|
} while (latestResponse.values.length === limit);
|
||||||
|
return {
|
||||||
|
values: logs,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
do {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
latestResponse = await getLogsBatch(serialNumber, start, end, limit, offset);
|
|
||||||
logs = logs.concat(latestResponse.values);
|
|
||||||
offset += limit;
|
|
||||||
} while (latestResponse.values.length === limit);
|
|
||||||
return {
|
|
||||||
values: logs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useGetDeviceLogsWithTimestamps = ({
|
export const useGetDeviceLogsWithTimestamps = ({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
onError,
|
onError,
|
||||||
|
logType,
|
||||||
}: {
|
}: {
|
||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
start?: number;
|
start?: number;
|
||||||
end?: number;
|
end?: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
|
logType?: 0 | 1 | 2;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['devicelogs', serialNumber, { start, end }], getDeviceLogsWithTimestamps(serialNumber, start, end), {
|
useQuery(
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
['devicelogs', serialNumber, { start, end, logType }],
|
||||||
staleTime: 1000 * 60,
|
getDeviceLogsWithTimestamps(serialNumber, start, end, logType ?? 0),
|
||||||
onError,
|
{
|
||||||
});
|
enabled: serialNumber !== undefined && serialNumber !== '' && start !== undefined && end !== undefined,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
onError,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export type DeviceWithStatus = {
|
|||||||
associations_6G: number;
|
associations_6G: number;
|
||||||
compatible: string;
|
compatible: string;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
connectReason?: string;
|
||||||
certificateExpiryDate?: number;
|
certificateExpiryDate?: number;
|
||||||
createdTimestamp: number;
|
createdTimestamp: number;
|
||||||
devicePassword: string;
|
devicePassword: string;
|
||||||
@@ -51,28 +52,49 @@ export type DeviceWithStatus = {
|
|||||||
entity: string;
|
entity: string;
|
||||||
firmware: string;
|
firmware: string;
|
||||||
fwUpdatePolicy: string;
|
fwUpdatePolicy: string;
|
||||||
|
hasGPS: boolean;
|
||||||
|
hasRADIUSSessions: number | boolean;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
lastConfigurationChange: number;
|
lastConfigurationChange: number;
|
||||||
lastConfigurationDownload: number;
|
lastConfigurationDownload: number;
|
||||||
lastContact: number | string;
|
lastContact: number | string;
|
||||||
lastFWUpdate: number;
|
lastFWUpdate: number;
|
||||||
|
lastRecordedContact: number;
|
||||||
|
load: number;
|
||||||
locale: string;
|
locale: string;
|
||||||
location: string;
|
location: string;
|
||||||
macAddress: string;
|
macAddress: string;
|
||||||
manufacturer: string;
|
manufacturer: string;
|
||||||
|
memoryUsed: number;
|
||||||
messageCount: number;
|
messageCount: number;
|
||||||
modified: number;
|
modified: number;
|
||||||
notes: Note[];
|
notes: Note[];
|
||||||
owner: string;
|
owner: string;
|
||||||
|
sanity: number;
|
||||||
|
started: number;
|
||||||
restrictedDevice: boolean;
|
restrictedDevice: boolean;
|
||||||
rxBytes: number;
|
rxBytes: number;
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
simulated: boolean;
|
||||||
subscriber: string;
|
subscriber: string;
|
||||||
|
temperature: number;
|
||||||
txBytes: number;
|
txBytes: number;
|
||||||
venue: string;
|
venue: string;
|
||||||
verifiedCertificate: string;
|
verifiedCertificate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSingleDeviceWithStatus = (serialNumber: string) =>
|
||||||
|
axiosGw
|
||||||
|
.get(`devices?deviceWithStatus=true&select=${serialNumber}`)
|
||||||
|
.then((response) => {
|
||||||
|
const deviceWithStatus: DeviceWithStatus | undefined = response.data.devicesWithStatus[0];
|
||||||
|
if (deviceWithStatus === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return deviceWithStatus;
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
|
||||||
const getDevices = (limit: number, offset: number) =>
|
const getDevices = (limit: number, offset: number) =>
|
||||||
axiosGw
|
axiosGw
|
||||||
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
|
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
|
||||||
@@ -127,6 +149,7 @@ export type DeviceStatus = {
|
|||||||
associations_2G: number;
|
associations_2G: number;
|
||||||
associations_5G: number;
|
associations_5G: number;
|
||||||
connected: boolean;
|
connected: boolean;
|
||||||
|
connectReason?: string;
|
||||||
certificateExpiryDate: number;
|
certificateExpiryDate: number;
|
||||||
connectionCompletionTime: number;
|
connectionCompletionTime: number;
|
||||||
firmware: string;
|
firmware: string;
|
||||||
@@ -165,7 +188,10 @@ export type DevicesStats = {
|
|||||||
averageConnectionTime: number;
|
averageConnectionTime: number;
|
||||||
connectedDevices: number;
|
connectedDevices: number;
|
||||||
connectingDevices: number;
|
connectingDevices: number;
|
||||||
|
tx: number;
|
||||||
|
rx: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInitialStats = async () =>
|
const getInitialStats = async () =>
|
||||||
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
|
axiosGw.get(`devices?connectionStatistics=true`).then(({ data }: { data: DevicesStats }) => data);
|
||||||
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
export const useGetDevicesStats = ({ onError }: { onError?: (e: AxiosError) => void }) => {
|
||||||
|
|||||||
@@ -7,35 +7,49 @@ import { AxiosError } from 'models/Axios';
|
|||||||
import { Firmware } from 'models/Firmware';
|
import { Firmware } from 'models/Firmware';
|
||||||
import { Note } from 'models/Note';
|
import { Note } from 'models/Note';
|
||||||
|
|
||||||
|
const getAvailableFirmwareBatch = async (deviceType: string, limit: number, offset: number) =>
|
||||||
|
axiosFms
|
||||||
|
.get(`firmwares?deviceType=${deviceType}&limit=${limit}&offset=${offset}`)
|
||||||
|
.then(({ data }: { data: { firmwares: Firmware[] } }) => data);
|
||||||
|
|
||||||
|
const getAllAvailableFirmware = async (deviceType: string) => {
|
||||||
|
const limit = 500;
|
||||||
|
let offset = 0;
|
||||||
|
let data: { firmwares: Firmware[] } = { firmwares: [] };
|
||||||
|
let lastResponse: { firmwares: Firmware[] } = { firmwares: [] };
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
lastResponse = await getAvailableFirmwareBatch(deviceType, limit, offset);
|
||||||
|
data = {
|
||||||
|
firmwares: [...data.firmwares, ...lastResponse.firmwares],
|
||||||
|
};
|
||||||
|
offset += 500;
|
||||||
|
} while (lastResponse.firmwares.length === 500);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
|
export const useGetAvailableFirmware = ({ deviceType }: { deviceType: string }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
return useQuery(
|
return useQuery(['get-device-profile'], () => getAllAvailableFirmware(deviceType), {
|
||||||
['get-device-profile'],
|
enabled: deviceType !== '',
|
||||||
() =>
|
onError: (e: AxiosError) => {
|
||||||
axiosFms
|
if (!toast.isActive('firmware-fetching-error'))
|
||||||
.get(`firmwares?deviceType=${deviceType}&limit=10000&offset=0`)
|
toast({
|
||||||
.then(({ data }: { data: { firmwares: Firmware[] } }) => data),
|
id: 'firmware-fetching-error',
|
||||||
{
|
title: t('common.error'),
|
||||||
enabled: deviceType !== '',
|
description: t('crud.error_fetching_obj', {
|
||||||
onError: (e: AxiosError) => {
|
e: e?.response?.data?.ErrorDescription,
|
||||||
if (!toast.isActive('firmware-fetching-error'))
|
obj: t('analytics.firmware'),
|
||||||
toast({
|
}),
|
||||||
id: 'firmware-fetching-error',
|
status: 'error',
|
||||||
title: t('common.error'),
|
duration: 5000,
|
||||||
description: t('crud.error_fetching_obj', {
|
isClosable: true,
|
||||||
e: e?.response?.data?.ErrorDescription,
|
position: 'top-right',
|
||||||
obj: t('analytics.firmware'),
|
});
|
||||||
}),
|
|
||||||
status: 'error',
|
|
||||||
duration: 5000,
|
|
||||||
isClosable: true,
|
|
||||||
position: 'top-right',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
|
export const useUpdateDeviceToLatest = ({ serialNumber, compatible }: { serialNumber: string; compatible: string }) =>
|
||||||
@@ -56,7 +70,13 @@ export const useUpdateDeviceFirmware = ({ serialNumber, onClose }: { serialNumbe
|
|||||||
|
|
||||||
return useMutation(
|
return useMutation(
|
||||||
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
({ keepRedirector, uri, signature }: { keepRedirector: boolean; uri: string; signature?: string }) =>
|
||||||
axiosGw.post(`device/${serialNumber}/upgrade`, { serialNumber, when: 0, keepRedirector, uri, signature }),
|
axiosGw.post(`device/${serialNumber}/upgrade${signature ? `?FWsignature=${signature}` : ''}`, {
|
||||||
|
serialNumber,
|
||||||
|
when: 0,
|
||||||
|
keepRedirector,
|
||||||
|
uri,
|
||||||
|
signature,
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast({
|
toast({
|
||||||
@@ -222,3 +242,23 @@ export const useGetFirmwareDashboard = () =>
|
|||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getLastDbUpdate = async () =>
|
||||||
|
axiosFms.get(`firmwares?updateTimeOnly=true`).then((response) => response.data as { lastUpdateTime: number });
|
||||||
|
export const useGetFirmwareDbUpdate = () =>
|
||||||
|
useQuery(['firmware', 'db'], getLastDbUpdate, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDb = async () => axiosFms.put(`firmwares?update=true`);
|
||||||
|
|
||||||
|
export const useUpdateFirmwareDb = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(updateDb, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['firmware', 'db']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
@@ -89,3 +89,18 @@ export const useDeleteHealthChecks = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDevicesWithHealthBetween = (
|
||||||
|
context: QueryFunctionContext<[string, string, { lowerLimit: number; upperLimit: number }]>,
|
||||||
|
) =>
|
||||||
|
axiosGw
|
||||||
|
.get(`devices?health=true&lowLimit=${context.queryKey[2].lowerLimit}&highLimit=${context.queryKey[2].upperLimit}`)
|
||||||
|
.then((res) => res.data.serialNumbers as string[]);
|
||||||
|
|
||||||
|
export const useGetDevicesWithHealthBetween = ({
|
||||||
|
lowerLimit,
|
||||||
|
upperLimit,
|
||||||
|
}: {
|
||||||
|
lowerLimit: number;
|
||||||
|
upperLimit: number;
|
||||||
|
}) => useQuery(['devices', 'health', { lowerLimit, upperLimit }], getDevicesWithHealthBetween);
|
||||||
|
|||||||
@@ -55,4 +55,5 @@ export const useGetTag = ({ serialNumber, onError }: { serialNumber?: string; on
|
|||||||
useQuery(['tag', serialNumber], () => getTag(serialNumber), {
|
useQuery(['tag', serialNumber], () => getTag(serialNumber), {
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
onError,
|
onError,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
});
|
});
|
||||||
|
|||||||
67
src/hooks/Network/Radius.ts
Normal file
67
src/hooks/Network/Radius.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
|
|
||||||
|
export type RadiusSession = {
|
||||||
|
started: number;
|
||||||
|
lastTransaction: number;
|
||||||
|
inputPackets: number;
|
||||||
|
outputPackets: number;
|
||||||
|
inputOctets: number;
|
||||||
|
outputOctets: number;
|
||||||
|
inputGigaWords: number;
|
||||||
|
outputGigaWords: number;
|
||||||
|
sessionTime: number;
|
||||||
|
serialNumber: string;
|
||||||
|
destination: string;
|
||||||
|
userName: string;
|
||||||
|
accountingSessionId: string;
|
||||||
|
accountingMultiSessionId: string;
|
||||||
|
callingStationId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeviceRadiusSessions = async (mac: string) =>
|
||||||
|
axiosGw.get(`/radiusSessions/${mac}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||||
|
const getDeviceSessions = async (context: QueryFunctionContext<[string, string]>) =>
|
||||||
|
getDeviceRadiusSessions(context.queryKey[1]);
|
||||||
|
export const useGetDeviceRadiusSessions = ({ serialNumber }: { serialNumber: string }) =>
|
||||||
|
useQuery(['radius-sessions', serialNumber], getDeviceSessions, {
|
||||||
|
keepPreviousData: true,
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUsernameRadiusSessions = async (username: string) =>
|
||||||
|
axiosGw.get(`/radiusSessions/0?userName=${username}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||||
|
const getUserSessions = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||||
|
getUsernameRadiusSessions(context.queryKey[2]);
|
||||||
|
export const useGetUserRadiusSessions = ({ userName }: { userName: string }) =>
|
||||||
|
useQuery(['radius-sessions', 'username', userName], getUserSessions, {
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getStationRadiusSessions = async (station: string) =>
|
||||||
|
axiosGw.get(`/radiusSessions/0?mac=${station}`).then((res) => res.data.sessions as RadiusSession[]);
|
||||||
|
const getStationSessions = async (context: QueryFunctionContext<[string, string, string]>) =>
|
||||||
|
getStationRadiusSessions(context.queryKey[2]);
|
||||||
|
export const useGetStationRadiusSessions = ({ station }: { station: string }) =>
|
||||||
|
useQuery(['radius-sessions', 'station', station], getStationSessions, {
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnectRadiusSession = async (session: RadiusSession) =>
|
||||||
|
axiosGw
|
||||||
|
.put(`radiusSessions/${session.serialNumber}?operation=coadm`, {
|
||||||
|
accountingSessionId: session.accountingSessionId,
|
||||||
|
accountingMultiSessionId: session.accountingMultiSessionId,
|
||||||
|
callingStationId: session.callingStationId,
|
||||||
|
})
|
||||||
|
.then(() => session);
|
||||||
|
|
||||||
|
export const useDisconnectRadiusSession = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(disconnectRadiusSession, {
|
||||||
|
onSuccess: (session) => {
|
||||||
|
queryClient.invalidateQueries(['radius-sessions', session.serialNumber]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
83
src/hooks/Network/Secrets.ts
Normal file
83
src/hooks/Network/Secrets.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { QueryFunctionContext, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
|
|
||||||
|
export type SecretName = 'google.maps.apikey' | string;
|
||||||
|
|
||||||
|
export type Secret = {
|
||||||
|
key: SecretName;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SecretDictionaryValue = {
|
||||||
|
key: SecretName;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecret = async (context: QueryFunctionContext<string[], unknown>) =>
|
||||||
|
axiosSec.get(`/systemSecret/${context.queryKey[1]}`).then(({ data }: { data: Secret }) => data);
|
||||||
|
|
||||||
|
export const useGetSystemSecret = ({ secret }: { secret: SecretName }) =>
|
||||||
|
useQuery(['secrets', secret], getSecret, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAllSecrets = async () =>
|
||||||
|
axiosSec.get('/systemSecret/0?all=true').then(({ data }: { data: { secrets: Secret[] } }) => data.secrets);
|
||||||
|
|
||||||
|
export const useGetAllSystemSecrets = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useQuery(['secrets'], getAllSecrets, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
for (const secret of data) {
|
||||||
|
queryClient.setQueryData(['secrets', secret.key], secret);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsDictionary = async () =>
|
||||||
|
axiosSec
|
||||||
|
.get('/systemSecret/0?dictionary=true')
|
||||||
|
.then(({ data }: { data: { knownKeys: SecretDictionaryValue[] } }) => data.knownKeys);
|
||||||
|
|
||||||
|
export const useGetSystemSecretsDictionary = () =>
|
||||||
|
useQuery(['secrets', 'dictionary'], getSecretsDictionary, {
|
||||||
|
staleTime: 1000 * 60 * 10,
|
||||||
|
refetchInterval: 1000 * 60 * 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSecret = async ({ key, value }: { key: string; value: string }) =>
|
||||||
|
axiosSec.put(`/systemSecret/${key}?value=${value}`, { key, value });
|
||||||
|
|
||||||
|
export const useUpdateSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(updateSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCreateSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(updateSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSecret = async (key: string) => axiosSec.delete(`/systemSecret/${key}`);
|
||||||
|
|
||||||
|
export const useDeleteSystemSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(deleteSecret, {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['secrets']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
|
|
||||||
@@ -21,12 +21,25 @@ type DeviceInterfaceStatistics = {
|
|||||||
tx_errors: number;
|
tx_errors: number;
|
||||||
tx_packets: number;
|
tx_packets: number;
|
||||||
};
|
};
|
||||||
|
'counters-aggregate'?: {
|
||||||
|
collisions: number;
|
||||||
|
multicast: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_dropped: number;
|
||||||
|
rx_errors: number;
|
||||||
|
rx_packets: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_dropped: number;
|
||||||
|
tx_errors: number;
|
||||||
|
tx_packets: number;
|
||||||
|
};
|
||||||
ssids?: {
|
ssids?: {
|
||||||
associations?: {
|
associations?: {
|
||||||
ack_signal: number;
|
ack_signal: number;
|
||||||
ack_signal_avg: number;
|
ack_signal_avg: number;
|
||||||
bssid: string;
|
bssid: string;
|
||||||
connected: number;
|
connected: number;
|
||||||
|
dynamic_vlan?: number;
|
||||||
inactive: number;
|
inactive: number;
|
||||||
ipaddr_v4: string;
|
ipaddr_v4: string;
|
||||||
rssi: number;
|
rssi: number;
|
||||||
@@ -105,6 +118,13 @@ export type DeviceStatistics = {
|
|||||||
transmit_ms: number;
|
transmit_ms: number;
|
||||||
tx_power: number;
|
tx_power: number;
|
||||||
}[];
|
}[];
|
||||||
|
dynamic_vlans?: {
|
||||||
|
vid: number;
|
||||||
|
rx_bytes: number;
|
||||||
|
rx_packets: number;
|
||||||
|
tx_bytes: number;
|
||||||
|
tx_packets: number;
|
||||||
|
}[];
|
||||||
unit?: {
|
unit?: {
|
||||||
load: [number, number, number];
|
load: [number, number, number];
|
||||||
localtime: number;
|
localtime: number;
|
||||||
@@ -148,6 +168,11 @@ export type DeviceStatistics = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
gps?: {
|
||||||
|
elevation: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
};
|
||||||
version?: number;
|
version?: number;
|
||||||
};
|
};
|
||||||
const getLastStats = (serialNumber?: string) =>
|
const getLastStats = (serialNumber?: string) =>
|
||||||
@@ -163,7 +188,7 @@ export const useGetDeviceLastStats = ({
|
|||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
}) =>
|
}) =>
|
||||||
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
useQuery(['device', serialNumber, 'last-statistics'], () => getLastStats(serialNumber), {
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '' && false,
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
@@ -183,24 +208,12 @@ export const useGetDeviceNewestStats = ({
|
|||||||
serialNumber?: string;
|
serialNumber?: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
onError?: (e: AxiosError) => void;
|
onError?: (e: AxiosError) => void;
|
||||||
}) => {
|
}) =>
|
||||||
const queryClient = useQueryClient();
|
useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
|
||||||
|
|
||||||
return useQuery(['deviceStatistics', serialNumber, 'newest', { limit }], getNewestStats(limit, serialNumber), {
|
|
||||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||||
staleTime: 1000 * 60,
|
staleTime: 1000 * 60,
|
||||||
onSuccess: (response) => {
|
|
||||||
const entry = response.data[0];
|
|
||||||
// If we have a valid entry, we prefill lastStats, if not we trigger a fetch of the last statistics
|
|
||||||
if (entry) {
|
|
||||||
queryClient.setQueryData(['device', serialNumber, 'last-statistics'], entry.data);
|
|
||||||
} else {
|
|
||||||
queryClient.fetchQuery(['device', serialNumber, 'last-statistics']);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError,
|
onError,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const getOuis = (macs?: string[]) => async () =>
|
const getOuis = (macs?: string[]) => async () =>
|
||||||
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{
|
axiosGw.get(`/ouis?macList=${macs?.join(',')}`).then((response) => response.data) as Promise<{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import axios from 'axios';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { axiosGw } from 'constants/axiosInstances';
|
import { axiosGw } from 'constants/axiosInstances';
|
||||||
|
|
||||||
@@ -85,14 +86,44 @@ export const useTrace = ({ serialNumber, alertOnCompletion }: { serialNumber: st
|
|||||||
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
export const downloadTrace = (serialNumber: string, commandId: string) =>
|
||||||
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
axiosGw.get(`file/${commandId}?serialNumber=${serialNumber}`, { responseType: 'arraybuffer' });
|
||||||
|
|
||||||
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) =>
|
export const useDownloadTrace = ({ serialNumber, commandId }: { serialNumber: string; commandId: string }) => {
|
||||||
useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
const { t } = useTranslation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
return useQuery(['download-trace', serialNumber, commandId], () => downloadTrace(serialNumber, commandId), {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
const blob = new Blob([response.data], { type: 'application/octet-stream' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = window.URL.createObjectURL(blob);
|
link.href = window.URL.createObjectURL(blob);
|
||||||
link.download = `Trace_${commandId}.pcap`;
|
const headerLine =
|
||||||
|
(response.headers['content-disposition'] as string | undefined) ??
|
||||||
|
(response.headers['content-disposition'] as string | undefined);
|
||||||
|
const filename = headerLine?.split('filename=')[1]?.split(',')[0] ?? `Trace_${commandId}.pcap`;
|
||||||
|
link.download = filename;
|
||||||
link.click();
|
link.click();
|
||||||
},
|
},
|
||||||
|
onError: (e) => {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const bufferResponse = e.response?.data;
|
||||||
|
let errorMessage = '';
|
||||||
|
// If the response is a buffer, parse to JSON object
|
||||||
|
if (bufferResponse instanceof ArrayBuffer) {
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
const json = JSON.parse(decoder.decode(bufferResponse));
|
||||||
|
errorMessage = json.ErrorDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
id: `trace-download-error-${serialNumber}`,
|
||||||
|
title: t('common.error'),
|
||||||
|
description: errorMessage,
|
||||||
|
status: 'error',
|
||||||
|
duration: 5000,
|
||||||
|
isClosable: true,
|
||||||
|
position: 'top-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { QueryClient, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { axiosSec } from 'constants/axiosInstances';
|
import { axiosSec } from 'constants/axiosInstances';
|
||||||
import { AxiosError } from 'models/Axios';
|
import { AxiosError } from 'models/Axios';
|
||||||
@@ -58,12 +58,24 @@ export type User = {
|
|||||||
waitingForEmailCheck: boolean;
|
waitingForEmailCheck: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvatarPromises = (userList: User[]) => {
|
const getAvatarPromises = (userList: User[], queryClient: QueryClient) => {
|
||||||
const promises = userList.map(async (user) => {
|
const promises = userList.map(async (user) => {
|
||||||
if (user.avatar !== '' && user.avatar !== '0') {
|
if (user.avatar !== '' && user.avatar !== '0') {
|
||||||
return axiosSec.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
// If the avatar is already in the cache, return it
|
||||||
responseType: 'arraybuffer',
|
const cachedAvatar = queryClient.getQueryData(['avatar', user.id, user.avatar]);
|
||||||
});
|
if (cachedAvatar) return cachedAvatar;
|
||||||
|
|
||||||
|
return axiosSec
|
||||||
|
.get(`avatar/${user.id}?cache=${user.avatar}`, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
queryClient.setQueryData(['avatar', user.id, user.avatar], response);
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return Promise.resolve('');
|
return Promise.resolve('');
|
||||||
});
|
});
|
||||||
@@ -71,10 +83,35 @@ const getAvatarPromises = (userList: User[]) => {
|
|||||||
return promises;
|
return promises;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUsers = async () => {
|
const getBatchUsers = async (offset: number, limit: number) => {
|
||||||
const users = await axiosSec.get('users').then(({ data }) => data.users as User[]);
|
const users = await axiosSec
|
||||||
|
.get(`users?offset=${offset}&limit=${limit}&withExtendedInfo=true`)
|
||||||
|
.then(({ data }) => data.users as User[]);
|
||||||
|
|
||||||
const avatars = await Promise.allSettled(getAvatarPromises(users)).then((results) =>
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllUsers = async () => {
|
||||||
|
let users: User[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 500;
|
||||||
|
let lastResponseLength = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const response = await getBatchUsers(offset, limit);
|
||||||
|
users = [...users, ...response];
|
||||||
|
offset += limit;
|
||||||
|
lastResponseLength = response.length;
|
||||||
|
} while (lastResponseLength === limit);
|
||||||
|
|
||||||
|
return users;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUsers = async (queryClient: QueryClient) => {
|
||||||
|
const users = await getAllUsers();
|
||||||
|
|
||||||
|
const avatars = await Promise.allSettled(getAvatarPromises(users, queryClient)).then((results) =>
|
||||||
results.map((response) => {
|
results.map((response) => {
|
||||||
if (response.status === 'fulfilled' && response?.value !== '') {
|
if (response.status === 'fulfilled' && response?.value !== '') {
|
||||||
const base64 = btoa(
|
const base64 = btoa(
|
||||||
@@ -93,8 +130,10 @@ const getUsers = async () => {
|
|||||||
export const useGetUsers = () => {
|
export const useGetUsers = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useQuery(['users'], getUsers, {
|
return useQuery(['users'], () => getUsers(queryClient), {
|
||||||
|
staleTime: 30 * 1000,
|
||||||
onError: (e: AxiosError) => {
|
onError: (e: AxiosError) => {
|
||||||
if (!toast.isActive('users-fetching-error'))
|
if (!toast.isActive('users-fetching-error'))
|
||||||
toast({
|
toast({
|
||||||
@@ -118,7 +157,7 @@ export const useGetUser = ({ id, enabled }: { id: string; enabled: boolean }) =>
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
return useQuery(
|
return useQuery(
|
||||||
['get-user', id],
|
['users', id],
|
||||||
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
() => axiosSec.get(`user/${id}?withExtendedInfo=true`).then(({ data }) => data as User),
|
||||||
{
|
{
|
||||||
enabled,
|
enabled,
|
||||||
@@ -173,16 +212,41 @@ export const useSendUserEmailValidation = ({ id, refresh }: { id: string; refres
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export const useSuspendUser = ({ id }: { id: string }) =>
|
export const useSuspendUser = ({ id }: { id: string }) => {
|
||||||
useMutation((isSuspended: boolean) =>
|
const queryClient = useQueryClient();
|
||||||
axiosSec.put(`user/${id}`, {
|
|
||||||
suspended: isSuspended,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
export const useResetMfa = ({ id }: { id: string }) => useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}));
|
|
||||||
|
|
||||||
export const useResetPassword = ({ id }: { id: string }) =>
|
return useMutation(
|
||||||
useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}));
|
(isSuspended: boolean) =>
|
||||||
|
axiosSec.put(`user/${id}`, {
|
||||||
|
suspended: isSuspended,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetMfa = ({ id }: { id: string }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(() => axiosSec.put(`user/${id}?resetMFA=true`, {}), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useResetPassword = ({ id }: { id: string }) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation(() => axiosSec.put(`user/${id}?forgotPassword=true`, {}), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(['users']);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
const deleteUser = async (userId: string) => axiosSec.delete(`/user/${userId}`);
|
||||||
export const useDeleteUser = () => {
|
export const useDeleteUser = () => {
|
||||||
|
|||||||
54
src/hooks/useContainerDimensions.ts
Normal file
54
src/hooks/useContainerDimensions.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const roundToNearest = (num: number, precision: number) => {
|
||||||
|
const factor = 1 / precision;
|
||||||
|
return Math.round(num * factor) / factor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseContainerDimensionsProps = {
|
||||||
|
precision?: 10 | 100 | 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useContainerDimensions = ({ precision }: UseContainerDimensionsProps) => {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [dimensions, setDimensions] = React.useState({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const getDimensions = () => ({
|
||||||
|
width: (ref && ref.current?.offsetWidth) || 0,
|
||||||
|
height: (ref && ref.current?.offsetHeight) || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const { width, height } = getDimensions();
|
||||||
|
if (!precision) {
|
||||||
|
if (dimensions.width !== width && dimensions.height !== height) setDimensions({ width, height });
|
||||||
|
} else {
|
||||||
|
const newDimensions = { width, height };
|
||||||
|
newDimensions.width = roundToNearest(newDimensions.width, precision);
|
||||||
|
newDimensions.height = roundToNearest(newDimensions.height, precision);
|
||||||
|
if (newDimensions.width !== dimensions.width || newDimensions.height !== dimensions.height) {
|
||||||
|
setDimensions(newDimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ref.current) {
|
||||||
|
handleResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, [ref, dimensions]);
|
||||||
|
|
||||||
|
return React.useMemo(
|
||||||
|
() => ({
|
||||||
|
dimensions,
|
||||||
|
ref,
|
||||||
|
}),
|
||||||
|
[dimensions.height, dimensions.width],
|
||||||
|
);
|
||||||
|
};
|
||||||
7
src/i18next.d.ts
vendored
Normal file
7
src/i18next.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import 'i18next';
|
||||||
|
|
||||||
|
declare module 'i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
returnNull: false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Flex, Heading, Tooltip, VStack } from '@chakra-ui/react';
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
CircularProgressLabel,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
Text,
|
||||||
|
Tooltip,
|
||||||
|
VStack,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ArrowSquareDown, ArrowSquareUp, Clock } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Card } from 'components/Containers/Card';
|
||||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||||
|
import { bytesString } from 'helpers/stringHelper';
|
||||||
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
import { useGetDevicesStats } from 'hooks/Network/Devices';
|
||||||
|
|
||||||
const SidebarDevices = () => {
|
const SidebarDevices = () => {
|
||||||
@@ -10,18 +23,19 @@ const SidebarDevices = () => {
|
|||||||
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
const [lastTime, setLastTime] = React.useState<Date | undefined>();
|
||||||
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
const [lastUpdate, setLastUpdate] = React.useState<Date | undefined>();
|
||||||
|
|
||||||
const getTime = () => {
|
const time = React.useMemo(() => {
|
||||||
if (lastTime === undefined || lastUpdate === undefined) return null;
|
if (lastTime === undefined || lastUpdate === undefined) return null;
|
||||||
|
|
||||||
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
const seconds = lastTime.getTime() - lastUpdate.getTime();
|
||||||
|
|
||||||
return Math.max(0, Math.floor(seconds / 1000));
|
return Math.max(0, Math.floor(seconds / 1000));
|
||||||
};
|
}, [lastTime, lastUpdate]);
|
||||||
|
|
||||||
const refresh = () => {
|
const circleColor = () => {
|
||||||
if (document.visibilityState !== 'hidden') {
|
if (time === null) return 'gray.300';
|
||||||
getStats.refetch();
|
if (time < 10) return 'green.300';
|
||||||
}
|
if (time < 30) return 'yellow.300';
|
||||||
|
return 'red.300';
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -37,37 +51,60 @@ const SidebarDevices = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
document.addEventListener('visibilitychange', refresh);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('visibilitychange', refresh);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!getStats.data) return null;
|
if (!getStats.data) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={4}>
|
<Card p={4}>
|
||||||
<Flex flexDir="column" textAlign="center">
|
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||||
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
<CircularProgress
|
||||||
<Heading size="xs">
|
isIndeterminate
|
||||||
{t('common.connected')} {t('devices.title')}
|
color={circleColor()}
|
||||||
</Heading>
|
position="absolute"
|
||||||
<Heading size="xs" mt={1} fontStyle="italic" fontWeight="normal" color="gray.400">
|
right="6px"
|
||||||
({getStats.data.connectingDevices} {t('controller.devices.connecting')})
|
top="6px"
|
||||||
</Heading>
|
w="unset"
|
||||||
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
size={6}
|
||||||
<Heading size="md" textAlign="center" mt={2}>
|
thickness="14px"
|
||||||
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
>
|
||||||
|
<CircularProgressLabel fontSize="1.9em">{time}s</CircularProgressLabel>
|
||||||
|
</CircularProgress>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||||
|
<Box position="absolute" right="8px" top="8px" w="unset" hidden>
|
||||||
|
<Clock size={16} />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
<VStack mb={-1}>
|
||||||
|
<Flex flexDir="column" textAlign="center">
|
||||||
|
<Heading size="md">{getStats.data.connectedDevices}</Heading>
|
||||||
|
<Heading size="xs" display="flex" justifyContent="center">
|
||||||
|
<Text>
|
||||||
|
{t('common.connected')} {t('devices.title')}{' '}
|
||||||
|
</Text>{' '}
|
||||||
</Heading>
|
</Heading>
|
||||||
</Tooltip>
|
<Tooltip hasArrow label={compactSecondsToDetailed(getStats.data.averageConnectionTime, t)}>
|
||||||
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
<Heading size="md" textAlign="center" mt={1}>
|
||||||
<Heading size="xs" mt={2} fontStyle="italic" fontWeight="normal" color="gray.400">
|
{minimalSecondsToDetailed(getStats.data.averageConnectionTime, t)}
|
||||||
{t('controller.stats.seconds_ago', { s: getTime() })}
|
</Heading>
|
||||||
</Heading>
|
</Tooltip>
|
||||||
</Flex>
|
<Heading size="xs">{t('controller.devices.average_uptime')}</Heading>
|
||||||
</VStack>
|
<Flex fontSize="sm" fontWeight="bold" alignItems="center" justifyContent="center" mt={1}>
|
||||||
|
<Tooltip hasArrow label="Rx">
|
||||||
|
<Flex alignItems="center" mr={1}>
|
||||||
|
<Icon as={ArrowSquareUp} weight="bold" boxSize={5} mt="1px" color="blue.400" />{' '}
|
||||||
|
{getStats.data.rx !== undefined ? bytesString(getStats.data.rx, 0) : '-'}
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip hasArrow label="Tx">
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Icon as={ArrowSquareDown} weight="bold" boxSize={5} mt="1px" color="purple.400" />{' '}
|
||||||
|
{getStats.data.tx !== undefined ? bytesString(getStats.data.tx, 0) : '-'}
|
||||||
|
</Flex>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</VStack>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
Portal,
|
Portal,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { ArrowCircleLeft } from 'phosphor-react';
|
import { ArrowCircleLeft } from '@phosphor-icons/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
@@ -97,14 +97,22 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
|||||||
ps="12px"
|
ps="12px"
|
||||||
pt="8px"
|
pt="8px"
|
||||||
top="15px"
|
top="15px"
|
||||||
w={isCompact ? '100%' : 'calc(100vw - 271px)'}
|
border={scrolled ? '0.5px solid' : undefined}
|
||||||
|
w={isCompact ? '100%' : 'calc(100% - 254px)'}
|
||||||
>
|
>
|
||||||
<Flex w="100%" flexDirection="row" alignItems="center">
|
<Flex
|
||||||
|
w="100%"
|
||||||
|
flexDirection="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyItems="center"
|
||||||
|
alignContent="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
{isCompact && <HamburgerIcon w="24px" h="24px" onClick={toggleSidebar} mr={10} mt={1} />}
|
||||||
<Heading>{activeRoute}</Heading>
|
<Heading size="lg">{activeRoute}</Heading>
|
||||||
<Tooltip label={t('common.go_back')}>
|
<Tooltip label={t('common.go_back')}>
|
||||||
<IconButton
|
<IconButton
|
||||||
mt={2}
|
mt={1}
|
||||||
ml={4}
|
ml={4}
|
||||||
colorScheme="blue"
|
colorScheme="blue"
|
||||||
aria-label={t('common.go_back')}
|
aria-label={t('common.go_back')}
|
||||||
|
|||||||
@@ -1,37 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
import { AccordionButton, AccordionItem, Flex, Text, useColorModeValue } from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import IconBox from 'components/Containers/IconBox';
|
import IconBox from 'components/Containers/IconBox';
|
||||||
import { Route } from 'models/Routes';
|
import { SingleRoute } from 'models/Routes';
|
||||||
|
|
||||||
const variantChange = '0.2s linear';
|
const variantChange = '0.2s linear';
|
||||||
|
|
||||||
const commonStyle = {
|
|
||||||
boxSize: 'initial',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
alignItems: 'center',
|
|
||||||
transition: variantChange,
|
|
||||||
bg: 'transparent',
|
|
||||||
ps: '6px',
|
|
||||||
py: '12px',
|
|
||||||
pe: '4px',
|
|
||||||
w: '100%',
|
|
||||||
borderRadius: '15px',
|
|
||||||
_active: {
|
|
||||||
bg: 'inherit',
|
|
||||||
transform: 'none',
|
|
||||||
borderColor: 'transparent',
|
|
||||||
},
|
|
||||||
_focus: {
|
|
||||||
boxShadow: '0px 7px 11px rgba(0, 0, 0, 0.04)',
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
route: Route;
|
route: SingleRoute;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,42 +17,64 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||||
|
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||||
|
|
||||||
if (route.navButton) {
|
if (route.navButton) {
|
||||||
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
|
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<Button {...commonStyle} boxShadow="none">
|
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||||
<Flex>
|
<AccordionButton
|
||||||
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
|
px={1}
|
||||||
{route.icon(true)}
|
h={{
|
||||||
</IconBox>
|
md: '40px',
|
||||||
<Text color={activeTextColor} my="auto" fontSize="md">
|
lg: '50px',
|
||||||
{t(route.name)}
|
}}
|
||||||
</Text>
|
borderRadius="15px"
|
||||||
</Flex>
|
w="100%"
|
||||||
</Button>
|
bg={activeBg}
|
||||||
|
_hover={{
|
||||||
|
bg: hoverBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex alignItems="center" w="100%">
|
||||||
|
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||||
|
{route.icon(false)}
|
||||||
|
</IconBox>
|
||||||
|
<Text color={activeTextColor} fontSize="md" fontWeight="bold">
|
||||||
|
{t(route.name)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</AccordionButton>
|
||||||
|
</AccordionItem>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||||
{...commonStyle}
|
<AccordionButton
|
||||||
ps="6px"
|
px={1}
|
||||||
_focus={{
|
h={{
|
||||||
boxShadow: 'none',
|
md: '40px',
|
||||||
}}
|
lg: '50px',
|
||||||
>
|
}}
|
||||||
<Flex>
|
borderRadius="15px"
|
||||||
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="6px" transition={variantChange}>
|
w="100%"
|
||||||
{route.icon(false)}
|
_hover={{
|
||||||
</IconBox>
|
bg: hoverBg,
|
||||||
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
}}
|
||||||
{t(route.name)}
|
>
|
||||||
</Text>
|
<Flex alignItems="center" w="100%">
|
||||||
</Flex>
|
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||||
</Button>
|
{route.icon(false)}
|
||||||
|
</IconBox>
|
||||||
|
<Text color={inactiveTextColor} fontSize="md" fontWeight="bold">
|
||||||
|
{t(route.name)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</AccordionButton>
|
||||||
|
</AccordionItem>
|
||||||
)}
|
)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
|
|||||||
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal file
39
src/layout/Sidebar/NestedNavButton/SubNavigationButton.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Button, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { SubRoute } from 'models/Routes';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isActive: (path: string) => boolean;
|
||||||
|
route: SubRoute;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubNavigationButton = ({ isActive, route }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||||
|
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||||
|
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||||
|
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||||
|
|
||||||
|
const isCurrentlyActive = isActive(route.path);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
w="100%"
|
||||||
|
justifyContent="left"
|
||||||
|
color={isCurrentlyActive ? activeTextColor : inactiveTextColor}
|
||||||
|
bg={isCurrentlyActive ? activeBg : 'transparent'}
|
||||||
|
_hover={{
|
||||||
|
bg: hoverBg,
|
||||||
|
}}
|
||||||
|
border="none"
|
||||||
|
>
|
||||||
|
{t(route.name)}
|
||||||
|
</Button>
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SubNavigationButton;
|
||||||
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal file
64
src/layout/Sidebar/NestedNavButton/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
AccordionButton,
|
||||||
|
AccordionIcon,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionPanel,
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import SubNavigationButton from './SubNavigationButton';
|
||||||
|
import IconBox from 'components/Containers/IconBox';
|
||||||
|
import { RouteGroup } from 'models/Routes';
|
||||||
|
|
||||||
|
const variantChange = '0.2s linear';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isActive: (path: string) => boolean;
|
||||||
|
route: RouteGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NestedNavButton = ({ isActive, route }: Props) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||||
|
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||||
|
<AccordionButton
|
||||||
|
px={1}
|
||||||
|
h={{
|
||||||
|
md: '40px',
|
||||||
|
lg: '50px',
|
||||||
|
}}
|
||||||
|
_hover={{
|
||||||
|
bg: hoverBg,
|
||||||
|
}}
|
||||||
|
borderRadius="15px"
|
||||||
|
w="100%"
|
||||||
|
>
|
||||||
|
<Flex alignItems="center" w="100%">
|
||||||
|
<IconBox color="blue.300" h="30px" w="30px" me="6px" transition={variantChange} fontWeight="bold">
|
||||||
|
{route.icon(false)}
|
||||||
|
</IconBox>
|
||||||
|
<Text size="md" fontWeight="bold" color={inactiveTextColor}>
|
||||||
|
{typeof route.name === 'string' ? t(route.name) : route.name(t)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pl="18px" paddingEnd={0} pr="-18px">
|
||||||
|
<Box pl={1} pr={-1} borderLeft="1px solid #63b3ed">
|
||||||
|
{route.children.map((subRoute) => (
|
||||||
|
<SubNavigationButton key={subRoute.path} route={subRoute} isActive={isActive} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NestedNavButton;
|
||||||
@@ -12,11 +12,12 @@ import {
|
|||||||
Spacer,
|
Spacer,
|
||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
VStack,
|
VStack,
|
||||||
|
Accordion,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { NavLinkButton } from './NavLinkButton';
|
import { NavLinkButton } from './NavLinkButton';
|
||||||
|
import NestedNavButton from './NestedNavButton';
|
||||||
import { useAuth } from 'contexts/AuthProvider';
|
import { useAuth } from 'contexts/AuthProvider';
|
||||||
import { Route } from 'models/Routes';
|
import { Route } from 'models/Routes';
|
||||||
|
|
||||||
@@ -60,14 +61,25 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
|||||||
const sidebarContent = React.useMemo(
|
const sidebarContent = React.useMemo(
|
||||||
() => (
|
() => (
|
||||||
<>
|
<>
|
||||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
<Accordion allowToggle>
|
||||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||||
{routes
|
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
{routes
|
||||||
.map((route) => (
|
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||||
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
|
.map((route) =>
|
||||||
))}
|
route.children ? (
|
||||||
</VStack>
|
<NestedNavButton key={route.id} isActive={isRouteActive} route={route} />
|
||||||
|
) : (
|
||||||
|
<NavLinkButton
|
||||||
|
key={route.id}
|
||||||
|
isActive={isRouteActive(route.path)}
|
||||||
|
route={route}
|
||||||
|
toggleSidebar={toggle}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Accordion>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Box mb={2}>{children}</Box>
|
<Box mb={2}>{children}</Box>
|
||||||
<Box>
|
<Box>
|
||||||
@@ -117,7 +129,8 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
|||||||
h="calc(100vh - 32px)"
|
h="calc(100vh - 32px)"
|
||||||
my="16px"
|
my="16px"
|
||||||
ml="16px"
|
ml="16px"
|
||||||
borderRadius="16px"
|
borderRadius="15px"
|
||||||
|
border="0.5px solid"
|
||||||
>
|
>
|
||||||
{brand}
|
{brand}
|
||||||
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">
|
<Flex direction="column" h="calc(100vh - 160px)" alignItems="center" overflowY="auto">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Sidebar } from './Sidebar';
|
|||||||
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
import darkLogo from 'assets/Logo_Dark_Mode.svg';
|
||||||
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
import lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||||
import { Route as RouteProps } from 'models/Routes';
|
import { RouteName } from 'models/Routes';
|
||||||
import NotFoundPage from 'pages/NotFound';
|
import NotFoundPage from 'pages/NotFound';
|
||||||
import routes from 'router/routes';
|
import routes from 'router/routes';
|
||||||
|
|
||||||
@@ -22,18 +22,57 @@ const Layout = () => {
|
|||||||
document.documentElement.dir = 'ltr';
|
document.documentElement.dir = 'ltr';
|
||||||
|
|
||||||
const activeRoute = React.useMemo(() => {
|
const activeRoute = React.useMemo(() => {
|
||||||
const route = routes.find(
|
let name: RouteName = '';
|
||||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
for (const route of routes) {
|
||||||
);
|
if (!route.children && route.path === location.pathname) {
|
||||||
|
name = route.navName ?? route.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (route.path?.includes('/:')) {
|
||||||
|
const routePath = route.path.split('/:')[0];
|
||||||
|
const currPath = location.pathname.split('/');
|
||||||
|
if (routePath && location.pathname.startsWith(routePath) && currPath.length === 3) {
|
||||||
|
name = route.navName ?? route.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (route.children) {
|
||||||
|
for (const child of route.children) {
|
||||||
|
if (child.path === location.pathname) {
|
||||||
|
name = child.navName ?? child.name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (route) return route.navName ? t(route.navName) : t(route.name);
|
if (typeof name === 'function') return name(t);
|
||||||
|
|
||||||
return '';
|
if (name.includes('PATH')) {
|
||||||
|
name = location.pathname.split('/')[location.pathname.split('/').length - 1] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.includes('RAW-')) name.replace('RAW-', '');
|
||||||
|
|
||||||
|
return t(name);
|
||||||
}, [t, location.pathname]);
|
}, [t, location.pathname]);
|
||||||
|
|
||||||
const getRoutes = (r: RouteProps[]) =>
|
const routeInstances = React.useMemo(() => {
|
||||||
// @ts-ignore
|
const instances = [];
|
||||||
r.map((route: RouteProps) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
|
||||||
|
for (const route of routes) {
|
||||||
|
// @ts-ignore
|
||||||
|
if (!route.children) instances.push(<Route path={route.path} element={<route.component />} key={route.id} />);
|
||||||
|
else {
|
||||||
|
for (const child of route.children) {
|
||||||
|
// @ts-ignore
|
||||||
|
instances.push(<Route path={child.path} element={<child.component />} key={child.id} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances;
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -59,9 +98,7 @@ const Layout = () => {
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
||||||
<PageContainer waitForUser>
|
<PageContainer waitForUser>
|
||||||
<Routes>
|
<Routes>{[...routeInstances, <Route path="*" element={<NotFoundPage />} key={uuid()} />]}</Routes>
|
||||||
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
|
||||||
</Routes>
|
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface GatewayDevice {
|
|||||||
entity: string;
|
entity: string;
|
||||||
firmware: string;
|
firmware: string;
|
||||||
fwUpdatePolicy: string;
|
fwUpdatePolicy: string;
|
||||||
|
hasGPS: boolean;
|
||||||
|
hasRADIUSSessions: number;
|
||||||
lastConfigurationChange: number;
|
lastConfigurationChange: number;
|
||||||
lastConfigurationDownload: number;
|
lastConfigurationDownload: number;
|
||||||
lastFWUpdate: number;
|
lastFWUpdate: number;
|
||||||
@@ -37,6 +39,7 @@ export interface GatewayDevice {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
|
simulated: boolean;
|
||||||
subscriber: string;
|
subscriber: string;
|
||||||
venue: string;
|
venue: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,53 @@
|
|||||||
import { ReactNode } from 'react';
|
import React, { LazyExoticComponent } from 'react';
|
||||||
|
|
||||||
export interface Route {
|
export type RouteName = string | ((t: (s: string) => string) => string);
|
||||||
|
|
||||||
|
export type SubRoute = {
|
||||||
|
id: string;
|
||||||
authorized: string[];
|
authorized: string[];
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: RouteName;
|
||||||
navName?: string;
|
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||||
icon: (active: boolean) => ReactNode;
|
navName?: RouteName;
|
||||||
navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode;
|
hidden?: boolean;
|
||||||
|
icon?: undefined;
|
||||||
|
navButton?: undefined;
|
||||||
|
isEntity?: undefined;
|
||||||
|
isCustom?: undefined;
|
||||||
|
children?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RouteGroup = {
|
||||||
|
id: string;
|
||||||
|
authorized: string[];
|
||||||
|
name: RouteName;
|
||||||
|
icon: (active: boolean) => React.ReactElement;
|
||||||
|
children: SubRoute[];
|
||||||
|
hidden?: boolean;
|
||||||
|
path?: undefined;
|
||||||
|
navName?: undefined;
|
||||||
|
navButton?: undefined;
|
||||||
|
isEntity?: undefined;
|
||||||
|
isCustom?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SingleRoute = {
|
||||||
|
id: string;
|
||||||
|
authorized: string[];
|
||||||
|
path: string;
|
||||||
|
name: RouteName;
|
||||||
|
navName?: RouteName;
|
||||||
|
icon: (active: boolean) => React.ReactElement;
|
||||||
|
navButton?: (
|
||||||
|
isActive: boolean,
|
||||||
|
toggleSidebar: () => void,
|
||||||
|
route: Route,
|
||||||
|
) => React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||||
isEntity?: boolean;
|
isEntity?: boolean;
|
||||||
component: unknown;
|
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
}
|
children?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Route = SingleRoute | RouteGroup;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface Column<T> {
|
|||||||
alwaysShow?: boolean;
|
alwaysShow?: boolean;
|
||||||
Footer?: string;
|
Footer?: string;
|
||||||
accessor?: string;
|
accessor?: string;
|
||||||
|
stopPropagation?: boolean;
|
||||||
disableSortBy?: boolean;
|
disableSortBy?: boolean;
|
||||||
hasPopover?: boolean;
|
hasPopover?: boolean;
|
||||||
customMaxWidth?: string;
|
customMaxWidth?: string;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Note } from './Note';
|
import { Note } from './Note';
|
||||||
|
|
||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
| 'root'
|
| 'root'
|
||||||
| 'admin'
|
| 'admin'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user