mirror of
https://github.com/Telecominfraproject/wlan-cloud-ucentralgw-ui.git
synced 2025-10-30 02:12:33 +00:00
Compare commits
46 Commits
release/v2
...
v2.10.0
| 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 |
@@ -3,3 +3,4 @@ build
|
||||
dist
|
||||
node_modules
|
||||
.github
|
||||
/helm
|
||||
|
||||
@@ -8,7 +8,7 @@ fullnameOverride: ""
|
||||
images:
|
||||
owgwui:
|
||||
repository: tip-tip-wlan-cloud-ucentral.jfrog.io/owgw-ui
|
||||
tag: main
|
||||
tag: v2.10.0
|
||||
pullPolicy: Always
|
||||
|
||||
services:
|
||||
|
||||
9303
package-lock.json
generated
9303
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
94
package.json
94
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ucentral-client",
|
||||
"version": "2.9.0(23)",
|
||||
"version": "2.10.0(49)",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "index.tsx",
|
||||
@@ -15,82 +15,84 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.11",
|
||||
"@chakra-ui/icons": "^2.0.18",
|
||||
"@chakra-ui/react": "^2.3.6",
|
||||
"@chakra-ui/theme-tools": "^2.0.12",
|
||||
"@chakra-ui/utils": "^2.0.11",
|
||||
"@emotion/react": "^11.10.4",
|
||||
"@emotion/styled": "^11.10.4",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@chakra-ui/utils": "^2.0.14",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@googlemaps/react-wrapper": "^1.1.35",
|
||||
"@googlemaps/typescript-guards": "^2.0.3",
|
||||
"@react-spring/web": "^9.5.5",
|
||||
"axios": "^1.1.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",
|
||||
"chakra-react-select": "^4.3.0",
|
||||
"chakra-react-select": "^4.6.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"dagre": "^0.8.5",
|
||||
"fast-equals": "^5.0.1",
|
||||
"formik": "^2.2.9",
|
||||
"fast-equals": "^4.0.3",
|
||||
"framer-motion": "^7.6.1",
|
||||
"i18next": "^22.0.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"i18next-http-backend": "^1.4.4",
|
||||
"libphonenumber-js": "^1.10.14",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"framer-motion": "^10.12.2",
|
||||
"i18next": "^22.4.14",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.0",
|
||||
"libphonenumber-js": "^1.10.26",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"chart.js": "^3.9.1",
|
||||
"react-country-flag": "^3.0.2",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-datepicker": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"@textea/json-viewer": "^2.10.0",
|
||||
"react-fast-compare": "^3.2.0",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-fast-compare": "^3.2.1",
|
||||
"react-i18next": "^12.2.0",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"@tanstack/react-query": "^4.12.0",
|
||||
"react-router-dom": "^6.4.2",
|
||||
"react-router-dom": "^6.10.0",
|
||||
"react-table": "^7.8.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-window": "^1.8.8",
|
||||
"react-virtualized-auto-sizer": "^1.0.15",
|
||||
"react-window": "^1.8.9",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^3.1.8",
|
||||
"typescript": "^4.8.4",
|
||||
"uuid": "^9.0.0",
|
||||
"vite": "^4.2.1",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.1.2"
|
||||
"zustand": "^4.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google.maps": "^3.51.0",
|
||||
"@types/node": "^18.11.2",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/google.maps": "^3.52.5",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/react": "^18.0.37",
|
||||
"@types/react-csv": "^1.1.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/react-datepicker": "4.8.0",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/react-datepicker": "4.10.0",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-table": "^7.7.14",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"eslint": "8.25.0",
|
||||
"vite-tsconfig-paths": "^3.5.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"prettier": "^2.7.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-airbnb-typescript-prettier": "^5.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-no-inline-styles": "^1.0.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
"eslint-plugin-react": "^7.32.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"lint-staged": "^13.2.1",
|
||||
"prettier": "^2.8.7",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
},
|
||||
"browserslist": {
|
||||
"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",
|
||||
"inactive": "Inaktiv",
|
||||
"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_contact": "Letzter Kontakt",
|
||||
"last_disconnection": "Letzte Trennung",
|
||||
@@ -241,6 +243,7 @@
|
||||
"error_download": "Fehler beim Downloadversuch: {{e}}",
|
||||
"errors": "Fehler",
|
||||
"exit_fullscreen": "Ausgang",
|
||||
"export": "Export",
|
||||
"finished": "Fertig",
|
||||
"fullscreen": "Vollbildschirm",
|
||||
"general_error": "Fehler beim Verbinden mit dem Server. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
@@ -356,6 +359,7 @@
|
||||
"error_pushes_one": "Fehler (könnte an einer fehlerhaften Konfiguration liegen): {{count}}",
|
||||
"error_pushes_other": "Fehler (können auf eine fehlerhafte Konfiguration zurückzuführen sein): {{count}}",
|
||||
"expert_name": "Expertenmodus",
|
||||
"expert_name_explanation": "Sie können den Expertenmodus verwenden, um Ihre Konfiguration direkt zu ändern, einschließlich des Hinzufügens von Werten, die derzeit nicht von der Benutzeroberfläche unterstützt werden. Bitte verwenden Sie dieses Format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Erläuterung",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Firmware-Aktualisierung",
|
||||
@@ -522,6 +526,7 @@
|
||||
"ouis_explanation": "OUIs von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||
"outdated_one": "Firmware {{count}} Tag alt",
|
||||
"outdated_other": "Firmware {{count}} Tage alt",
|
||||
"outdated_unknown": "Firmware unbekannten Alters",
|
||||
"release": "Veröffentlichung",
|
||||
"show_dev_releases": "Entwicklerversionen",
|
||||
"status_explanation": "Verbindungsstatus von Geräten, die sich mit diesem Firmware-Server verbunden haben",
|
||||
@@ -537,6 +542,16 @@
|
||||
"queue": {
|
||||
"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": {
|
||||
"load": "Belastung (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Vor {{s}} Sekunden",
|
||||
@@ -629,6 +644,7 @@
|
||||
"notifications": "Gerätebenachrichtigungen",
|
||||
"one": "Gerät",
|
||||
"reassign_already_owned": "Geräte neu zuweisen, die bereits vorhanden sind und einem anderen Unternehmen/Veranstaltungsort/Abonnenten gehören?",
|
||||
"reboot_logs": "Neustartprotokolle",
|
||||
"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",
|
||||
@@ -712,7 +728,7 @@
|
||||
"invalid_ipv6": "Ungültige IPv6-Adresse (Bsp.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Ungültige JSON-Zeichenfolge",
|
||||
"invalid_lease_time": "Ungültiger Lease-Time-Wert! Sie müssen im digitUnit-Format vorliegen. Zum Beispiel: 6d2h5m, was 6 Tage, 2 Stunden und 5 Minuten bedeutet. Hier sind die akzeptierten Einheiten: m, h, d. Wenn Sie eine Einheit nicht verwenden möchten, lassen Sie sie vollständig weg. Anstatt also 0d2h0m zu sagen, verwenden Sie 2h",
|
||||
"invalid_mac_uc": "Ungültiger UC-MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Ungültiger MAC-Wert, zum Beispiel: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Ungültiges Passwort, bitte sehen Sie sich die Passwortrichtlinie an",
|
||||
"invalid_phone_number": "Ungültige Telefonnummer",
|
||||
"invalid_phone_numbers": "Mindestens eine der Telefonnummern ist ungültig. Bitte geben Sie sie ohne Symbole und Leerzeichen oder in diesem Format an: +1(123)123-1234",
|
||||
@@ -990,6 +1006,8 @@
|
||||
"concurrent_devices": "Gleichzeitige Geräte",
|
||||
"controller": "Regler",
|
||||
"current_live_devices": "Aktuelle Live-Geräte",
|
||||
"currently_running_one": "Derzeit wird {{count}} Simulation ausgeführt",
|
||||
"currently_running_other": "Derzeit laufen {{count}} Simulationen",
|
||||
"delete_success": "Gelöschte Simulation!",
|
||||
"duration": "Dauer",
|
||||
"error_devices": "Fehler Geräte",
|
||||
@@ -997,6 +1015,7 @@
|
||||
"infinite": "Unendlich",
|
||||
"keep_alive": "Bleib am Leben",
|
||||
"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_clients": "Max. Kunden",
|
||||
"min_associations": "Mindest. Verbände",
|
||||
@@ -1013,6 +1032,7 @@
|
||||
"rx_messages": "Rx-Meldungen",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" läuft",
|
||||
"sim_history": "{{sim}} Vorherige Läufe",
|
||||
"simulated": "Simuliert",
|
||||
"start": "Simulation starten",
|
||||
"start_success": "Simulationslauf gestartet!",
|
||||
"state_interval": "Zustandsintervall",
|
||||
@@ -1026,6 +1046,7 @@
|
||||
},
|
||||
"statistics": {
|
||||
"last_stats": "Letzte Statistik",
|
||||
"latest": "Neueste Statistiken",
|
||||
"memory": "Erinnerung"
|
||||
},
|
||||
"subscribers": {
|
||||
@@ -1072,14 +1093,24 @@
|
||||
"version": "Ausführung"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Säulen",
|
||||
"columns_hidden_one": "{{count}} Spalte ausgeblendet",
|
||||
"columns_hidden_other": "{{count}} Spalten ausgeblendet",
|
||||
"display_column": "Anzeige",
|
||||
"drag_always_show": "Sie können diese Spalte nicht ausblenden, aber ihre Position ändern",
|
||||
"drag_explanation": "Ziehen und Ablegen zum Neuordnen und Ändern der Spaltensichtbarkeit",
|
||||
"drag_locked": "Diese Säule ist in ihrer Position arretiert",
|
||||
"export_current_page": "Nur aktuelle Seite",
|
||||
"first_page": "Erste Seite",
|
||||
"go_to_page": "Zur Seite gehen",
|
||||
"hide_column": "verbergen",
|
||||
"last_page": "Letzte Seite",
|
||||
"next_page": "Nächste Seite",
|
||||
"page": "Seite",
|
||||
"previous_page": "Vorherige Seite"
|
||||
"preferences": "Tabelleneinstellungen",
|
||||
"previous_page": "Vorherige Seite",
|
||||
"reset": "Einstellungen zurücksetzen",
|
||||
"settings": "die Einstellungen"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "E-Mail nicht validiert",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"health": "Health",
|
||||
"inactive": "Inactive",
|
||||
"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_contact": "Last Contact",
|
||||
"last_disconnection": "Last Disconnection",
|
||||
@@ -241,6 +243,7 @@
|
||||
"error_download": "Error while trying to download: {{e}}",
|
||||
"errors": "Errors",
|
||||
"exit_fullscreen": "Exit",
|
||||
"export": "Export",
|
||||
"finished": "Finished",
|
||||
"fullscreen": "Fullscreen",
|
||||
"general_error": "Error connecting to the server. Please consult your administrator.",
|
||||
@@ -356,6 +359,7 @@
|
||||
"error_pushes_one": "Error (could be because of bad configuration): {{count}}",
|
||||
"error_pushes_other": "Errors (could be because of bad configuration): {{count}}",
|
||||
"expert_name": "Expert Mode",
|
||||
"expert_name_explanation": "You can use expert mode to directly modify your configuration, including adding values that are not currently supported by the UI. Please use this format: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explanation",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Firmware Upgrade",
|
||||
@@ -522,6 +526,7 @@
|
||||
"ouis_explanation": "OUIs of devices that have connected to this firmware server",
|
||||
"outdated_one": "Firmware {{count}} day old",
|
||||
"outdated_other": "Firmware {{count}} days old",
|
||||
"outdated_unknown": "Firmware of unknown age",
|
||||
"release": "Release",
|
||||
"show_dev_releases": "Dev Releases",
|
||||
"status_explanation": "Connection status of devices that have connected to this firmware server",
|
||||
@@ -537,6 +542,16 @@
|
||||
"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": {
|
||||
"load": "Load (1 | 5 | 15 m.)",
|
||||
"seconds_ago": "{{s}} seconds ago",
|
||||
@@ -629,6 +644,7 @@
|
||||
"notifications": "Device Notifications",
|
||||
"one": "Device",
|
||||
"reassign_already_owned": "Reassign devices which already exist and are owned by another entity/venue/subscriber?",
|
||||
"reboot_logs": "Reboot Logs",
|
||||
"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",
|
||||
@@ -712,7 +728,7 @@
|
||||
"invalid_ipv6": "Invalid IPv6 address (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Invalid JSON string",
|
||||
"invalid_lease_time": "Invalid lease time value! They need to be in the digitUnit format. For example: 6d2h5m, which means 6 days, 2 hours and 5 minutes. Here are the accepted units: m, h, d. If you do not want to use a unit, omit it completely. So instead of saying 0d2h0m, use 2h",
|
||||
"invalid_mac_uc": "Invalid UC-MAC value, for example: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Invalid MAC value, for example: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Invalid password, please look at the password policy",
|
||||
"invalid_phone_number": "Invalid Phone Number",
|
||||
"invalid_phone_numbers": "One or more of the phone numbers are invalid. Please provide them without symbols and spaces, or in this format: +1(123)123-1234",
|
||||
@@ -753,8 +769,8 @@
|
||||
"success_remove_claim": "Successfully removed claim on: {{serial}}",
|
||||
"successful_reboots": "Started Rebooting: {{count}}",
|
||||
"successful_upgrades": "Successful upgrades: {{count}}",
|
||||
"tag_one": "Tag",
|
||||
"tags": "Inventory Tags",
|
||||
"tag_one": "Device",
|
||||
"tags": "Inventory Devices",
|
||||
"title": "Inventory",
|
||||
"warning_reboots": "Not connected: {{count}}",
|
||||
"warning_upgrades": "Devices not connected: {{count}}"
|
||||
@@ -990,6 +1006,8 @@
|
||||
"concurrent_devices": "Concurrent Devices",
|
||||
"controller": "Controller",
|
||||
"current_live_devices": "Current Live Devices",
|
||||
"currently_running_one": "There is currently {{count}} simulation running",
|
||||
"currently_running_other": "There are currently {{count}} simulations running",
|
||||
"delete_success": "Deleted Simulation!",
|
||||
"duration": "Duration",
|
||||
"error_devices": "Error Devices",
|
||||
@@ -997,6 +1015,7 @@
|
||||
"infinite": "Infinite",
|
||||
"keep_alive": "Keep Alive",
|
||||
"mac_prefix": "MAC Prefix",
|
||||
"mac_prefix_length": "Your MAC prefix needs to be valid 6 HEX digits (ex.: 00112233)",
|
||||
"max_associations": "Max. Associations",
|
||||
"max_clients": "Max. Clients",
|
||||
"min_associations": "Min. Associations",
|
||||
@@ -1013,6 +1032,7 @@
|
||||
"rx_messages": "Rx Messages",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" in progress",
|
||||
"sim_history": "{{sim}} Previous Runs",
|
||||
"simulated": "Simulated",
|
||||
"start": "Start Simulation",
|
||||
"start_success": "Started simulation run!",
|
||||
"state_interval": "State Interval",
|
||||
@@ -1026,6 +1046,7 @@
|
||||
},
|
||||
"statistics": {
|
||||
"last_stats": "Last Statistics",
|
||||
"latest": "Latest Statistics",
|
||||
"memory": "Memory"
|
||||
},
|
||||
"subscribers": {
|
||||
@@ -1072,14 +1093,24 @@
|
||||
"version": "Version"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Columns",
|
||||
"columns_hidden_one": "{{count}} Column Hidden",
|
||||
"columns_hidden_other": "{{count}} Columns Hidden",
|
||||
"display_column": "Display",
|
||||
"drag_always_show": "You cannot hide this column but can change its position ",
|
||||
"drag_explanation": "Drag and drop to reorder and change column visibility",
|
||||
"drag_locked": "This column is locked in its position",
|
||||
"export_current_page": "Current Page Only",
|
||||
"first_page": "First Page",
|
||||
"go_to_page": "Go to page",
|
||||
"hide_column": "Hide",
|
||||
"last_page": "Last Page",
|
||||
"next_page": "Next Page",
|
||||
"page": "Page",
|
||||
"previous_page": "Previous Page"
|
||||
"preferences": "Table Preferences",
|
||||
"previous_page": "Previous Page",
|
||||
"reset": "Reset Preferences",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "email not validated",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"health": "salud",
|
||||
"inactive": "inactivo",
|
||||
"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_contact": "Último contacto",
|
||||
"last_disconnection": "Última desconexión",
|
||||
@@ -241,6 +243,7 @@
|
||||
"error_download": "Error al intentar descargar: {{e}}",
|
||||
"errors": "Los errores",
|
||||
"exit_fullscreen": "salida",
|
||||
"export": "Exportar",
|
||||
"finished": "terminado",
|
||||
"fullscreen": "Pantalla Completa",
|
||||
"general_error": "Error al conectar con el servidor. Consulte a su administrador.",
|
||||
@@ -356,6 +359,7 @@
|
||||
"error_pushes_one": "Error (podría deberse a una mala configuración): {{count}}",
|
||||
"error_pushes_other": "Errores (pueden deberse a una mala configuración): {{count}}",
|
||||
"expert_name": "Modo experto",
|
||||
"expert_name_explanation": "Puede usar el modo experto para modificar directamente su configuración, incluida la adición de valores que actualmente no son compatibles con la interfaz de usuario. Utilice este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explicación",
|
||||
"firewall": "cortafuegos",
|
||||
"firmware_upgrade": "Actualización de firmware",
|
||||
@@ -522,6 +526,7 @@
|
||||
"ouis_explanation": "OUI de dispositivos que se han conectado a este servidor de firmware",
|
||||
"outdated_one": "Firmware {{count}} día de antigüedad",
|
||||
"outdated_other": "Firmware de {{count}} días de antigüedad",
|
||||
"outdated_unknown": "Firmware de antigüedad desconocida",
|
||||
"release": "Lanzamiento",
|
||||
"show_dev_releases": "Lanzamientos de desarrollo",
|
||||
"status_explanation": "Estado de conexión de los dispositivos que se han conectado a este servidor de firmware",
|
||||
@@ -537,6 +542,16 @@
|
||||
"queue": {
|
||||
"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": {
|
||||
"load": "Carga (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Hace {{s}} segundos",
|
||||
@@ -629,6 +644,7 @@
|
||||
"notifications": "notificaciones de dispositivos",
|
||||
"one": "Dispositivo",
|
||||
"reassign_already_owned": "¿Reasignar dispositivos que ya existen y son propiedad de otra entidad/lugar/suscriptor?",
|
||||
"reboot_logs": "Reiniciar registros",
|
||||
"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",
|
||||
@@ -712,7 +728,7 @@
|
||||
"invalid_ipv6": "Dirección IPv6 no válida (ej.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Cadena JSON no válida",
|
||||
"invalid_lease_time": "¡Valor de tiempo de arrendamiento no válido! Deben estar en el formato digitUnit. Por ejemplo: 6d2h5m, lo que significa 6 días, 2 horas y 5 minutos. Estas son las unidades aceptadas: m, h, d. Si no desea utilizar una unidad, omítala por completo. Entonces, en lugar de decir 0d2h0m, usa 2h",
|
||||
"invalid_mac_uc": "Valor de UC-MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valor de MAC no válido, por ejemplo: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Contraseña no válida, consulte la política de contraseñas",
|
||||
"invalid_phone_number": "Numero de telefono invalido",
|
||||
"invalid_phone_numbers": "Uno o más de los números de teléfono no son válidos. Proporciónelos sin símbolos ni espacios, o en este formato: +1(123)123-1234",
|
||||
@@ -990,6 +1006,8 @@
|
||||
"concurrent_devices": "Dispositivos concurrentes",
|
||||
"controller": "Controlador",
|
||||
"current_live_devices": "Dispositivos activos actuales",
|
||||
"currently_running_one": "Actualmente hay {{count}} simulación en ejecución",
|
||||
"currently_running_other": "Actualmente hay {{count}} simulaciones ejecutándose",
|
||||
"delete_success": "¡Simulación eliminada!",
|
||||
"duration": "Duración",
|
||||
"error_devices": "Dispositivos de error",
|
||||
@@ -997,6 +1015,7 @@
|
||||
"infinite": "infinito",
|
||||
"keep_alive": "Mantener viva",
|
||||
"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_clients": "Max. Clientela",
|
||||
"min_associations": "Min. Asociaciones",
|
||||
@@ -1013,6 +1032,7 @@
|
||||
"rx_messages": "Mensajes prescritos",
|
||||
"sim_currently_running": "Simulación \"{{sim}}\" en curso",
|
||||
"sim_history": "{{sim}} ejecuciones anteriores",
|
||||
"simulated": "Simulado",
|
||||
"start": "Iniciar simulación",
|
||||
"start_success": "¡Ejecución de simulación iniciada!",
|
||||
"state_interval": "Intervalo de estado",
|
||||
@@ -1026,6 +1046,7 @@
|
||||
},
|
||||
"statistics": {
|
||||
"last_stats": "Últimas estadísticas",
|
||||
"latest": "Últimas estadísticas",
|
||||
"memory": "Memoria"
|
||||
},
|
||||
"subscribers": {
|
||||
@@ -1072,14 +1093,24 @@
|
||||
"version": "Versión"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Columnas",
|
||||
"columns_hidden_one": "{{count}} columna oculta",
|
||||
"columns_hidden_other": "{{count}} columnas ocultas",
|
||||
"display_column": "Monitor",
|
||||
"drag_always_show": "No puede ocultar esta columna pero puede cambiar su posición",
|
||||
"drag_explanation": "Arrastre y suelte para reordenar y cambiar la visibilidad de las columnas",
|
||||
"drag_locked": "Esta columna está bloqueada en su posición.",
|
||||
"export_current_page": "Solo página actual",
|
||||
"first_page": "Primera pagina",
|
||||
"go_to_page": "Ir a la página",
|
||||
"hide_column": "Esconder",
|
||||
"last_page": "Ultima pagina",
|
||||
"next_page": "Siguiente página",
|
||||
"page": "Página",
|
||||
"previous_page": "Página anterior"
|
||||
"preferences": "Preferencias de mesa",
|
||||
"previous_page": "Página anterior",
|
||||
"reset": "Reiniciar preferencias",
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "correo electrónico no validado",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"health": "santé",
|
||||
"inactive": "Inactif",
|
||||
"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_contact": "Dernier contact",
|
||||
"last_disconnection": "Dernière déconnexion",
|
||||
@@ -241,6 +243,7 @@
|
||||
"error_download": "Erreur lors de la tentative de téléchargement : {{e}}",
|
||||
"errors": "les erreurs",
|
||||
"exit_fullscreen": "Sortie",
|
||||
"export": "Exportation",
|
||||
"finished": "fini",
|
||||
"fullscreen": "Plein écran",
|
||||
"general_error": "Erreur de connexion au serveur. Veuillez consulter votre administrateur.",
|
||||
@@ -356,6 +359,7 @@
|
||||
"error_pushes_one": "Erreur (peut être due à une mauvaise configuration) : {{count}}",
|
||||
"error_pushes_other": "Erreurs (peut-être dues à une mauvaise configuration) : {{count}}",
|
||||
"expert_name": "Mode expert",
|
||||
"expert_name_explanation": "Vous pouvez utiliser le mode expert pour modifier directement votre configuration, notamment en ajoutant des valeurs qui ne sont pas actuellement prises en charge par l'interface utilisateur. Veuillez utiliser ce format : { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explication",
|
||||
"firewall": "Pare-feu",
|
||||
"firmware_upgrade": "Mise à jour du firmware",
|
||||
@@ -522,6 +526,7 @@
|
||||
"ouis_explanation": "OUI des appareils qui se sont connectés à ce serveur de firmware",
|
||||
"outdated_one": "Micrologiciel vieux de {{count}} jours",
|
||||
"outdated_other": "Micrologiciel vieux de {{count}} jours",
|
||||
"outdated_unknown": "Firmware d'âge inconnu",
|
||||
"release": "libération",
|
||||
"show_dev_releases": "Versions de développement",
|
||||
"status_explanation": "État de connexion des appareils qui se sont connectés à ce serveur de micrologiciel",
|
||||
@@ -537,6 +542,16 @@
|
||||
"queue": {
|
||||
"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": {
|
||||
"load": "Charge (1 | 5 | 15 m.)",
|
||||
"seconds_ago": " Il y a {{s}} secondes",
|
||||
@@ -629,6 +644,7 @@
|
||||
"notifications": "notifications de l'appareil",
|
||||
"one": "Dispositif",
|
||||
"reassign_already_owned": "Réattribuer des appareils qui existent déjà et qui appartiennent à une autre entité/salle/abonné ?",
|
||||
"reboot_logs": "Journaux de redémarrage",
|
||||
"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",
|
||||
@@ -712,7 +728,7 @@
|
||||
"invalid_ipv6": "Adresse IPv6 invalide (ex. : 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Chaîne JSON non valide",
|
||||
"invalid_lease_time": "Valeur de durée de bail non valide ! Ils doivent être au format digitUnit. Par exemple : 6d2h5m, ce qui signifie 6 jours, 2 heures et 5 minutes. Voici les unités acceptées : m, h, d. Si vous ne voulez pas utiliser une unité, omettez-la complètement. Donc au lieu de dire 0d2h0m, utilisez 2h",
|
||||
"invalid_mac_uc": "Valeur UC-MAC non valide, par exemple : 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valeur MAC non valide, par exemple : 00:00:5e:00:53:af",
|
||||
"invalid_password": "Mot de passe invalide, veuillez consulter la politique de mot de passe",
|
||||
"invalid_phone_number": "Numéro de téléphone invalide",
|
||||
"invalid_phone_numbers": "Un ou plusieurs des numéros de téléphone sont invalides. Veuillez les fournir sans symboles ni espaces, ou dans ce format : +1(123)123-1234",
|
||||
@@ -990,6 +1006,8 @@
|
||||
"concurrent_devices": "Périphériques simultanés",
|
||||
"controller": "Manette",
|
||||
"current_live_devices": "Appareils en direct actuels",
|
||||
"currently_running_one": "Il y a actuellement {{count}} simulation en cours",
|
||||
"currently_running_other": "Il y a actuellement {{count}} simulations en cours d'exécution",
|
||||
"delete_success": "Simulation supprimée !",
|
||||
"duration": "Durée",
|
||||
"error_devices": "Périphériques d'erreur",
|
||||
@@ -997,6 +1015,7 @@
|
||||
"infinite": "Infini",
|
||||
"keep_alive": "Rester en vie",
|
||||
"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_clients": "Max. Clients",
|
||||
"min_associations": "Min. Les associations",
|
||||
@@ -1013,6 +1032,7 @@
|
||||
"rx_messages": "Messages reçus",
|
||||
"sim_currently_running": "Simulation \"{{sim}}\" en cours",
|
||||
"sim_history": "{{sim}} courses précédentes",
|
||||
"simulated": "Simulé",
|
||||
"start": "Démarrer la simulation",
|
||||
"start_success": "Lancement de la simulation !",
|
||||
"state_interval": "Intervalle d'état",
|
||||
@@ -1026,6 +1046,7 @@
|
||||
},
|
||||
"statistics": {
|
||||
"last_stats": "Dernières statistiques",
|
||||
"latest": "Dernières statistiques",
|
||||
"memory": "mémoire"
|
||||
},
|
||||
"subscribers": {
|
||||
@@ -1072,14 +1093,24 @@
|
||||
"version": "Version"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Les colonnes",
|
||||
"columns_hidden_one": "{{count}} Colonne masquée",
|
||||
"columns_hidden_other": "{{count}} colonnes masquées",
|
||||
"display_column": "Afficher",
|
||||
"drag_always_show": "Vous ne pouvez pas masquer cette colonne, mais vous pouvez modifier sa position",
|
||||
"drag_explanation": "Glisser-déposer pour réorganiser et modifier la visibilité des colonnes",
|
||||
"drag_locked": "Cette colonne est verrouillée dans sa position",
|
||||
"export_current_page": "Page actuelle uniquement",
|
||||
"first_page": "Première page",
|
||||
"go_to_page": "Aller à la page",
|
||||
"hide_column": "Cacher",
|
||||
"last_page": "Dernière page",
|
||||
"next_page": "Page suivante",
|
||||
"page": "Page",
|
||||
"previous_page": "Page précédente"
|
||||
"preferences": "Préférences de tableau",
|
||||
"previous_page": "Page précédente",
|
||||
"reset": "Remettre à zéro les préférences",
|
||||
"settings": "Réglages"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "Mail non valide",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"health": "Saúde",
|
||||
"inactive": "Inativo",
|
||||
"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_contact": "Último contato",
|
||||
"last_disconnection": "Última desconexão",
|
||||
@@ -241,6 +243,7 @@
|
||||
"error_download": "Erro ao tentar fazer o download: {{e}}",
|
||||
"errors": "Erros",
|
||||
"exit_fullscreen": "Saída",
|
||||
"export": "Exportar",
|
||||
"finished": "acabado",
|
||||
"fullscreen": "Tela cheia",
|
||||
"general_error": "Erro ao se conectar ao servidor. Consulte seu administrador.",
|
||||
@@ -356,6 +359,7 @@
|
||||
"error_pushes_one": "Erro (pode ser devido à configuração incorreta): {{count}}",
|
||||
"error_pushes_other": "Erros (podem ser devido à configuração incorreta): {{count}}",
|
||||
"expert_name": "MODO EXPERT",
|
||||
"expert_name_explanation": "Você pode usar o modo especialista para modificar diretamente sua configuração, incluindo a adição de valores que não são atualmente suportados pela interface do usuário. Use este formato: { \"interfaces\": [ ... ], \"globals\": { ... }, ...etc }",
|
||||
"explanation": "Explicação",
|
||||
"firewall": "Firewall",
|
||||
"firmware_upgrade": "Atualização de firmware",
|
||||
@@ -522,6 +526,7 @@
|
||||
"ouis_explanation": "OUIs de dispositivos que se conectaram a este servidor de firmware",
|
||||
"outdated_one": "Firmware com {{count}} dias",
|
||||
"outdated_other": "Firmware com {{count}} dias",
|
||||
"outdated_unknown": "Firmware de idade desconhecida",
|
||||
"release": "LANÇAMENTO",
|
||||
"show_dev_releases": "Lançamentos do desenvolvedor",
|
||||
"status_explanation": "Status da conexão dos dispositivos que se conectaram a este servidor de firmware",
|
||||
@@ -537,6 +542,16 @@
|
||||
"queue": {
|
||||
"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": {
|
||||
"load": "Carga (1 | 5 | 15 m.)",
|
||||
"seconds_ago": "{{s}} segundos atrás",
|
||||
@@ -629,6 +644,7 @@
|
||||
"notifications": "Notificações do dispositivo",
|
||||
"one": "Dispositivo",
|
||||
"reassign_already_owned": "Reatribuir dispositivos que já existem e são de propriedade de outra entidade/local/assinante?",
|
||||
"reboot_logs": "Registros de reinicialização",
|
||||
"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",
|
||||
@@ -712,7 +728,7 @@
|
||||
"invalid_ipv6": "Endereço IPv6 inválido (ex.: 2001:db8:3333:4444:5555:6666:7777:8888)",
|
||||
"invalid_json": "Sequência JSON inválida",
|
||||
"invalid_lease_time": "Valor de tempo de locação inválido! Eles precisam estar no formato digitUnit. Por exemplo: 6d2h5m, que significa 6 dias, 2 horas e 5 minutos. Aqui estão as unidades aceitas: m, h, d. Se você não quiser usar uma unidade, omita-a completamente. Então, em vez de dizer 0d2h0m, use 2h",
|
||||
"invalid_mac_uc": "Valor UC-MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
||||
"invalid_mac_uc": "Valor MAC inválido, por exemplo: 00:00:5e:00:53:af",
|
||||
"invalid_password": "Senha inválida, consulte a política de senha",
|
||||
"invalid_phone_number": "Número de telefone inválido",
|
||||
"invalid_phone_numbers": "Um ou mais números de telefone são inválidos. Forneça-os sem símbolos e espaços ou neste formato: +1(123)123-1234",
|
||||
@@ -990,6 +1006,8 @@
|
||||
"concurrent_devices": "Dispositivos Simultâneos",
|
||||
"controller": "Controlador",
|
||||
"current_live_devices": "Dispositivos ativos atuais",
|
||||
"currently_running_one": "Atualmente, há {{count}} simulação em execução",
|
||||
"currently_running_other": "Existem atualmente {{count}} simulações em execução",
|
||||
"delete_success": "Simulação excluída!",
|
||||
"duration": "Duração",
|
||||
"error_devices": "Dispositivos de Erro",
|
||||
@@ -997,6 +1015,7 @@
|
||||
"infinite": "Infinito",
|
||||
"keep_alive": "Mantenha vivo",
|
||||
"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_clients": "Máx. Clientes",
|
||||
"min_associations": "Min. Associações",
|
||||
@@ -1013,6 +1032,7 @@
|
||||
"rx_messages": "Mensagens Rx",
|
||||
"sim_currently_running": "Simulação \"{{sim}}\" em andamento",
|
||||
"sim_history": "{{sim}} execuções anteriores",
|
||||
"simulated": "Simulado",
|
||||
"start": "Iniciar simulação",
|
||||
"start_success": "Corrida de simulação iniciada!",
|
||||
"state_interval": "Intervalo de estado",
|
||||
@@ -1026,6 +1046,7 @@
|
||||
},
|
||||
"statistics": {
|
||||
"last_stats": "Últimas estatísticas",
|
||||
"latest": "Estatísticas mais recentes",
|
||||
"memory": "Memória"
|
||||
},
|
||||
"subscribers": {
|
||||
@@ -1072,14 +1093,24 @@
|
||||
"version": "Versão"
|
||||
},
|
||||
"table": {
|
||||
"columns": "Colunas",
|
||||
"columns_hidden_one": "{{count}} Coluna oculta",
|
||||
"columns_hidden_other": "{{count}} Colunas ocultas",
|
||||
"display_column": "Exibição",
|
||||
"drag_always_show": "Você não pode ocultar esta coluna, mas pode alterar sua posição",
|
||||
"drag_explanation": "Arraste e solte para reordenar e alterar a visibilidade da coluna",
|
||||
"drag_locked": "Esta coluna está travada em sua posição",
|
||||
"export_current_page": "Somente página atual",
|
||||
"first_page": "Primeira Página",
|
||||
"go_to_page": "Vá para página",
|
||||
"hide_column": "Ocultar",
|
||||
"last_page": "Última Página",
|
||||
"next_page": "Próxima página",
|
||||
"page": "Página",
|
||||
"previous_page": "Página anterior"
|
||||
"preferences": "Preferências de Tabela",
|
||||
"previous_page": "Página anterior",
|
||||
"reset": "Reiniciar preferências",
|
||||
"settings": "Definições"
|
||||
},
|
||||
"user": {
|
||||
"email_not_validated": "e-mail não validado",
|
||||
|
||||
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 { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, SpaceProps } from '@chakra-ui/react';
|
||||
import { X } from 'phosphor-react';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface CloseButtonProps extends SpaceProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, SpaceProps } from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface CreateButtonProps extends SpaceProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface DeleteButtonProps {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Portal,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
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 { useControllerStore } from 'contexts/ControllerSocketProvider/useStore';
|
||||
import { useBlinkDevice, useGetDeviceRtty } from 'hooks/Network/Devices';
|
||||
@@ -51,12 +50,13 @@ const DeviceActionDropdown = ({
|
||||
onOpenScriptModal,
|
||||
onOpenRebootModal,
|
||||
size,
|
||||
isCompact = true,
|
||||
isCompact,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const connectColor = useColorModeValue('blackAlpha', 'gray');
|
||||
const addEventListeners = useControllerStore((state) => state.addEventListeners);
|
||||
const { refetch: getRtty, isInitialLoading: isRtty } = useGetDeviceRtty({
|
||||
const { refetch: getRtty, isFetching: isRtty } = useGetDeviceRtty({
|
||||
serialNumber: device.serialNumber,
|
||||
extraId: 'inventory-modal',
|
||||
});
|
||||
@@ -162,49 +162,90 @@ const DeviceActionDropdown = ({
|
||||
const handleRebootClick = () => onOpenRebootModal(device.serialNumber);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Tooltip label={t('common.actions')}>
|
||||
{size === undefined || isCompact ? (
|
||||
<>
|
||||
<Tooltip label={t('commands.connect')}>
|
||||
<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
|
||||
as={IconButton}
|
||||
aria-label="Commands"
|
||||
icon={isRtty ? <Spinner /> : <Wrench size={20} />}
|
||||
icon={<Wrench size={20} />}
|
||||
size={size ?? 'sm'}
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
/>
|
||||
) : (
|
||||
<MenuButton
|
||||
as={Button}
|
||||
aria-label="Commands"
|
||||
rightIcon={isRtty ? <Spinner /> : <Wrench size={20} />}
|
||||
size={size ?? 'sm'}
|
||||
isDisabled={isDisabled}
|
||||
ml={2}
|
||||
>
|
||||
{t('common.actions')}
|
||||
</MenuButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
<Portal>
|
||||
<MenuList>
|
||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenConfigure}>{t('controller.configure.title')}</MenuItem>
|
||||
<MenuItem onClick={handleConnectClick}>{t('commands.connect')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
||||
<MenuItem onClick={handleRebootClick}>{t('commands.reboot')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||
{t('premium.toolbox.upgrade_to_latest')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenScan}>{t('commands.wifiscan')}</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</Tooltip>
|
||||
<Portal>
|
||||
<MenuList maxH="315px" overflowY="auto">
|
||||
<MenuItem onClick={handleBlinkClick}>{t('commands.blink')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenConfigure} hidden={!isCompact}>
|
||||
{t('controller.configure.title')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleConnectClick} hidden={!isCompact}>
|
||||
{t('commands.connect')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenQueue}>{t('controller.queue.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenFactoryReset}>{t('commands.factory_reset')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenUpgrade}>{t('commands.firmware_upgrade')}</MenuItem>
|
||||
<MenuItem onClick={handleRebootClick} hidden={!isCompact}>
|
||||
{t('commands.reboot')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenTelemetry}>{t('controller.telemetry.title')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenScript}>{t('script.one')}</MenuItem>
|
||||
<MenuItem onClick={handleOpenTrace}>{t('controller.devices.trace')}</MenuItem>
|
||||
<MenuItem onClick={handleUpdateToLatest} hidden>
|
||||
{t('premium.toolbox.upgrade_to_latest')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleOpenScan} hidden={!isCompact}>
|
||||
{t('commands.wifiscan')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Portal>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { IconButton, Button, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Pen } from 'phosphor-react';
|
||||
import { Pen } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface EditButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from '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';
|
||||
|
||||
export interface RefreshButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { FloppyDisk } from 'phosphor-react';
|
||||
import { FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface SaveButtonProps
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { ArrowRight, FloppyDisk } from 'phosphor-react';
|
||||
import { ArrowRight, FloppyDisk } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface StepButtonProps {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint, useDisclosure } from '@chakra-ui/react';
|
||||
import { Pencil, X } from 'phosphor-react';
|
||||
import { Pencil, X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConfirmCloseAlertModal } from '../../Modals/ConfirmCloseAlert';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button, IconButton, Tooltip, useBreakpoint } from '@chakra-ui/react';
|
||||
import { Warning } from 'phosphor-react';
|
||||
import { Warning } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeProps } from 'models/Theme';
|
||||
|
||||
|
||||
@@ -1,17 +1,57 @@
|
||||
import React from 'react';
|
||||
import { Box, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
import React, { DOMAttributes } from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
Box,
|
||||
EffectProps,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
useColorModeValue,
|
||||
useStyleConfig,
|
||||
useToken,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
export interface CardHeaderProps extends LayoutProps, SpaceProps {
|
||||
variant?: string;
|
||||
export interface CardHeaderProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps,
|
||||
DOMAttributes<HTMLDivElement> {
|
||||
variant?: 'panel' | 'unstyled';
|
||||
children: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
headerStyle?: {
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
const _CardHeader: React.FC<CardHeaderProps> = ({ variant, children, ...rest }) => {
|
||||
// @ts-ignore
|
||||
const _CardHeader: React.FC<CardHeaderProps> = ({
|
||||
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 });
|
||||
|
||||
// Pass the computed styles into the `__css` prop
|
||||
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}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,7 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
BackgroundProps,
|
||||
Box,
|
||||
EffectProps,
|
||||
InteractivityProps,
|
||||
LayoutProps,
|
||||
PositionProps,
|
||||
SpaceProps,
|
||||
useStyleConfig,
|
||||
} from '@chakra-ui/react';
|
||||
import { BackgroundProps, Box, InteractivityProps, LayoutProps, SpaceProps, useStyleConfig } from '@chakra-ui/react';
|
||||
|
||||
export interface CardProps
|
||||
extends LayoutProps,
|
||||
SpaceProps,
|
||||
BackgroundProps,
|
||||
InteractivityProps,
|
||||
PositionProps,
|
||||
EffectProps {
|
||||
export interface CardProps extends LayoutProps, SpaceProps, BackgroundProps, InteractivityProps {
|
||||
variant?: string;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from '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 { Card } from '../Card';
|
||||
import { CardBody } from '../Card/CardBody';
|
||||
@@ -18,7 +18,7 @@ const GraphStatDisplay = ({ chart, title, explanation }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card variant="widget" w="100%">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Heading mr={2} my="auto" size="md">
|
||||
{title}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
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;
|
||||
right?: React.ReactNode;
|
||||
left?: React.ReactNode;
|
||||
right: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ModalHeader = ({ title, right }: ModalHeaderProps) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
|
||||
const bg = useColorModeValue('blue.50', 'blue.700');
|
||||
|
||||
return (
|
||||
<Header bg={bg}>
|
||||
{title}
|
||||
{left ? (
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left}
|
||||
</HStack>
|
||||
) : null}
|
||||
<Spacer />
|
||||
{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, 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';
|
||||
|
||||
type Props = {
|
||||
@@ -15,19 +15,19 @@ const SimpleIconStatDisplay = ({ title, description, icon, value, color }: Props
|
||||
const bgColor = useColorModeValue(color[0], color[1]);
|
||||
|
||||
return (
|
||||
<Card variant="widget" w="100%" p={3}>
|
||||
<Flex h="70px" w="100%">
|
||||
<Card p={3} bgColor={bgColor}>
|
||||
<Flex h="70px" w="100%" color="white">
|
||||
<Flex direction="column" justifyContent="center">
|
||||
<Heading size="lg">{value}</Heading>
|
||||
<Heading size="sm" display="flex">
|
||||
<Text opacity={0.8}>{title}</Text>
|
||||
<Text>{title}</Text>
|
||||
<Tooltip label={description} hasArrow>
|
||||
<Info style={{ marginLeft: '4px', marginTop: '2px' }} />
|
||||
</Tooltip>
|
||||
</Heading>
|
||||
</Flex>
|
||||
<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>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, Checkbox, IconButton, Menu, MenuButton, MenuItem, MenuList, useBreakpoint } from '@chakra-ui/react';
|
||||
import { FunnelSimple } from 'phosphor-react';
|
||||
import { FunnelSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
|
||||
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 { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
isSorted: boolean;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Icon } from '@chakra-ui/react';
|
||||
import { ArrowDown, ArrowUp, Circle } from 'phosphor-react';
|
||||
import { ArrowDown, ArrowUp, Circle } from '@phosphor-icons/react';
|
||||
|
||||
interface Props {
|
||||
isSorted: boolean;
|
||||
|
||||
@@ -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 { AddIcon } from '@chakra-ui/icons';
|
||||
import { IconButton, Input, InputGroup, InputRightElement, Tooltip } from '@chakra-ui/react';
|
||||
import { Trash } from 'phosphor-react';
|
||||
import { Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DataTable } from '../../../DataTables/DataTable';
|
||||
|
||||
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 { 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 (isCompact)
|
||||
return hidePrefix ? formatDaysAgoCompact(date).split(' ').slice(1).join(' ') : formatDaysAgoCompact(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)}>
|
||||
{getDaysAgo({ date, hidePrefix })}
|
||||
{getDaysAgo({ date, hidePrefix, isCompact })}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import { Question } from 'phosphor-react';
|
||||
import { Question } from '@phosphor-icons/react';
|
||||
|
||||
export type InfoPopoverProps = {
|
||||
title: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { InfoIcon } from '@chakra-ui/icons';
|
||||
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 { Card } from 'components/Containers/Card';
|
||||
|
||||
|
||||
@@ -33,7 +33,14 @@ const LanguageSwitcher = () => {
|
||||
return (
|
||||
<Menu>
|
||||
<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>
|
||||
<MenuList>
|
||||
<MenuItem onClick={changeLanguage('de')}>Deutsche</MenuItem>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Textarea,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { ClipboardText } from 'phosphor-react';
|
||||
import { ClipboardText } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SaveButton } from '../../Buttons/SaveButton';
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
const FirmwareList: React.FC<Props> = ({ firmware, upgrade, isLoading }) => {
|
||||
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 (
|
||||
<Box h="600px" w="100%" overflowY="auto" px={0}>
|
||||
@@ -34,10 +34,10 @@ const FirmwareList: React.FC<Props> = ({ firmware, upgrade, isLoading }) => {
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{sortedFirmware.map(({ created, size, revision, uri }) => (
|
||||
{sortedFirmware.map(({ imageDate, size, revision, uri }) => (
|
||||
<Tr key={uuid()}>
|
||||
<Td px={0} py={1} w="200px">
|
||||
{compactDate(created)}
|
||||
{compactDate(imageDate)}
|
||||
</Td>
|
||||
<Td px={0} py={1} width="160px">
|
||||
{bytesString(size)}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
FormLabel,
|
||||
Switch,
|
||||
Heading,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { Formik, FormikProps } from 'formik';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -32,6 +33,7 @@ export type FirmwareUpgradeModalProps = {
|
||||
|
||||
export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNumber }: FirmwareUpgradeModalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDevFirmware, { toggle: toggleDev }] = useBoolean();
|
||||
const [formKey, setFormKey] = React.useState(uuid());
|
||||
const ref = useRef<
|
||||
| FormikProps<{
|
||||
@@ -72,7 +74,13 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
||||
<ModalContent maxWidth={{ sm: '90%', md: '900px', lg: '1000px', xl: '80%' }}>
|
||||
<ModalHeader
|
||||
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>
|
||||
{isUpgrading || isFetchingDevice || isFetchingFirmware ? (
|
||||
@@ -104,7 +112,11 @@ export const FirmwareUpgradeModal = ({ modalProps: { isOpen, onClose }, serialNu
|
||||
</Formik>
|
||||
)}
|
||||
{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,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Flex, HStack, ModalHeader as Header, Spacer } from '@chakra-ui/react';
|
||||
import { HStack, ModalHeader as Header, Spacer, useColorModeValue } from '@chakra-ui/react';
|
||||
|
||||
export interface ModalHeaderProps {
|
||||
title: string;
|
||||
@@ -7,17 +7,20 @@ export interface ModalHeaderProps {
|
||||
right: React.ReactNode;
|
||||
}
|
||||
|
||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => (
|
||||
<Header>
|
||||
<Flex justifyContent="center" alignItems="center" maxW="100%" px={1}>
|
||||
const _ModalHeader: React.FC<ModalHeaderProps> = ({ title, left, right }) => {
|
||||
const bg = useColorModeValue('blue.50', 'blue.700');
|
||||
|
||||
return (
|
||||
<Header bg={bg}>
|
||||
{title}
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left ?? null}
|
||||
</HStack>
|
||||
{left ? (
|
||||
<HStack spacing={2} ml={2}>
|
||||
{left}
|
||||
</HStack>
|
||||
) : null}
|
||||
<Spacer />
|
||||
{right}
|
||||
</Flex>
|
||||
</Header>
|
||||
);
|
||||
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
export const ModalHeader = React.memo(_ModalHeader);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Textarea,
|
||||
useBoolean,
|
||||
} from '@chakra-ui/react';
|
||||
import { UploadSimple } from 'phosphor-react';
|
||||
import { UploadSimple } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { useFastField } from 'hooks/useFastField';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { FormikProps } from 'formik';
|
||||
import { ArrowLeft } from 'phosphor-react';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Spinner,
|
||||
Switch,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowLeft } from 'phosphor-react';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '../Modal';
|
||||
import { useDownloadTrace, useTrace } from 'hooks/Network/Trace';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { ArrowLeft } from 'phosphor-react';
|
||||
import { ArrowLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from '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 { useTranslation } from 'react-i18next';
|
||||
import ConfirmIgnoreCommand from '../ConfirmIgnoreCommand';
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
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(() => {
|
||||
if (bytes === undefined) return '-';
|
||||
|
||||
if (showZerosAs && bytes === 0) return showZerosAs;
|
||||
return bytesString(bytes);
|
||||
}, [bytes]);
|
||||
|
||||
return <div>{data}</div>;
|
||||
return <Box {...boxProps}>{data}</Box>;
|
||||
};
|
||||
|
||||
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 }) => {
|
||||
const data = useMemo(() => {
|
||||
type Props = {
|
||||
value?: number;
|
||||
boxProps?: BoxProps;
|
||||
showZerosAs?: string;
|
||||
};
|
||||
|
||||
const NumberCell = ({ value, boxProps, showZerosAs }: Props) => {
|
||||
const getData = () => {
|
||||
if (value === undefined) return '-';
|
||||
|
||||
if (value === 0 && showZerosAs) return showZerosAs;
|
||||
return value.toLocaleString();
|
||||
}, [value]);
|
||||
};
|
||||
|
||||
return <div>{data}</div>;
|
||||
return <Box {...boxProps}>{getData()}</Box>;
|
||||
};
|
||||
|
||||
export default React.memo(NumberCell);
|
||||
|
||||
@@ -89,7 +89,7 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const setPref = ({ preference, value }: { preference: string; value: string }) => {
|
||||
const setPref = async ({ preference, value }: { preference: string; value: string }) => {
|
||||
let updated = false;
|
||||
if (preferences) {
|
||||
const newPreferences: Preference[] = preferences.map((pref: Preference) => {
|
||||
@@ -102,15 +102,41 @@ export const AuthProvider = ({ token, children }: AuthProviderProps) => {
|
||||
|
||||
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) {
|
||||
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,
|
||||
getPref,
|
||||
setPref,
|
||||
setPrefs,
|
||||
deletePref,
|
||||
endpoints,
|
||||
configurationDescriptions,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import { axiosProv } from 'constants/axiosInstances';
|
||||
import { Preference } from 'models/Preference';
|
||||
import { User } from 'models/User';
|
||||
|
||||
const getConfigDescriptions = async (baseUrl: string) =>
|
||||
@@ -26,7 +27,8 @@ export interface AuthProviderReturn {
|
||||
logout: () => void;
|
||||
getPref: (preference: string) => string | null;
|
||||
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>;
|
||||
endpoints: { [key: string]: string } | null;
|
||||
configurationDescriptions: Record<string, unknown>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuid } from 'uuid';
|
||||
import create from 'zustand';
|
||||
import { ControllerSocketRawMessage, SocketEventCallback, SocketWebSocketNotificationData } from './utils';
|
||||
import { axiosGw } from 'constants/axiosInstances';
|
||||
import { randomIntId } from 'helpers/stringHelper';
|
||||
import { DevicesStats, DeviceWithStatus } from 'hooks/Network/Devices';
|
||||
import { NotificationType } from 'models/Socket';
|
||||
|
||||
@@ -122,6 +123,7 @@ export type ControllerStoreState = {
|
||||
lastSearchResults: string[];
|
||||
setLastSearchResults: (result: string[]) => void;
|
||||
errors: { str: string; timestamp: Date }[];
|
||||
searchSerialNumber: (serialNumber: string, timeout?: number) => Promise<string[]>;
|
||||
};
|
||||
|
||||
export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
@@ -169,13 +171,23 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
id: uuid(),
|
||||
};
|
||||
|
||||
const eventsToFire = get().eventListeners.filter(
|
||||
({ type, serialNumber }) => type === msg.type && serialNumber === msg.serialNumber,
|
||||
);
|
||||
const eventsToFire = get().eventListeners.filter((event) => {
|
||||
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) {
|
||||
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) => ({
|
||||
@@ -211,6 +223,37 @@ export const useControllerStore = create<ControllerStoreState>((set, get) => ({
|
||||
const ws = get().webSocket;
|
||||
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) => {
|
||||
const newTries = tries + 1;
|
||||
if (tries <= 10) {
|
||||
|
||||
@@ -116,9 +116,16 @@ export type SocketWebSocketNotificationData =
|
||||
log?: undefined;
|
||||
message: InitialSocketMessage;
|
||||
};
|
||||
export type SocketEventCallback = {
|
||||
id: string;
|
||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
};
|
||||
export type SocketEventCallback =
|
||||
| {
|
||||
id: string;
|
||||
type: 'DEVICE_CONNECTION' | 'DEVICE_DISCONNECTION';
|
||||
serialNumber: string;
|
||||
callback: () => void;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: 'DEVICE_SEARCH_RESULTS';
|
||||
searchId: number;
|
||||
callback: (serialNumbers: string[]) => void;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { TOptions } from 'i18next';
|
||||
import { X } from 'phosphor-react';
|
||||
import { X } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ProvisioningVenueNotificationMessage } from '../../utils';
|
||||
|
||||
@@ -122,7 +122,44 @@ export const getHoursAgo = (hoursAgo = 1, date = new Date()) => {
|
||||
export const dateForFilename = (dateString: number) => {
|
||||
const convertedTimestamp = unixToDateString(dateString);
|
||||
const date = new Date(convertedTimestamp);
|
||||
return `${date.getFullYear()}_${twoDigitNumber(date.getMonth() + 1)}_${twoDigitNumber(
|
||||
return `${date.getFullYear()}.${twoDigitNumber(date.getMonth() + 1)}.${twoDigitNumber(
|
||||
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 '-';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ export type DeviceLog = {
|
||||
severity: number;
|
||||
};
|
||||
|
||||
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1) => async () =>
|
||||
const getDeviceLogs = (limit: number, serialNumber?: string, logType?: 0 | 1 | 2) => async () =>
|
||||
axiosGw
|
||||
.get(`device/${serialNumber}/logs?newest=true&limit=${limit}&logType=${logType}`)
|
||||
.then((response) => response.data) as Promise<{
|
||||
@@ -28,7 +28,7 @@ export const useGetDeviceLogs = ({
|
||||
serialNumber?: string;
|
||||
limit: number;
|
||||
onError?: (e: AxiosError) => void;
|
||||
logType?: 0 | 1;
|
||||
logType?: 0 | 1 | 2;
|
||||
}) =>
|
||||
useQuery(['devicelogs', serialNumber, { limit, logType }], getDeviceLogs(limit, serialNumber, logType ?? 0), {
|
||||
keepPreviousData: true,
|
||||
@@ -44,7 +44,7 @@ const deleteLogs = async ({
|
||||
}: {
|
||||
serialNumber: string;
|
||||
endDate: number;
|
||||
logType: 0 | 1;
|
||||
logType: 0 | 1 | 2;
|
||||
}) => axiosGw.delete(`device/${serialNumber}/logs?endDate=${endDate}&logType=${logType}`);
|
||||
export const useDeleteLogs = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -62,7 +62,7 @@ const getLogsBatch = (
|
||||
end?: number,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
logType?: 0 | 1,
|
||||
logType?: 0 | 1 | 2,
|
||||
) =>
|
||||
axiosGw
|
||||
.get(
|
||||
@@ -74,7 +74,7 @@ const getLogsBatch = (
|
||||
}>;
|
||||
|
||||
const getDeviceLogsWithTimestamps =
|
||||
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1) => async () => {
|
||||
(serialNumber?: string, start?: number, end?: number, logType?: 0 | 1 | 2) => async () => {
|
||||
let offset = 0;
|
||||
const limit = 100;
|
||||
let logs: DeviceLog[] = [];
|
||||
@@ -104,7 +104,7 @@ export const useGetDeviceLogsWithTimestamps = ({
|
||||
start?: number;
|
||||
end?: number;
|
||||
onError?: (e: AxiosError) => void;
|
||||
logType?: 0 | 1;
|
||||
logType?: 0 | 1 | 2;
|
||||
}) =>
|
||||
useQuery(
|
||||
['devicelogs', serialNumber, { start, end, logType }],
|
||||
|
||||
@@ -44,6 +44,7 @@ export type DeviceWithStatus = {
|
||||
associations_6G: number;
|
||||
compatible: string;
|
||||
connected: boolean;
|
||||
connectReason?: string;
|
||||
certificateExpiryDate?: number;
|
||||
createdTimestamp: number;
|
||||
devicePassword: string;
|
||||
@@ -51,28 +52,49 @@ export type DeviceWithStatus = {
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
hasGPS: boolean;
|
||||
hasRADIUSSessions: number | boolean;
|
||||
ipAddress: string;
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastContact: number | string;
|
||||
lastFWUpdate: number;
|
||||
lastRecordedContact: number;
|
||||
load: number;
|
||||
locale: string;
|
||||
location: string;
|
||||
macAddress: string;
|
||||
manufacturer: string;
|
||||
memoryUsed: number;
|
||||
messageCount: number;
|
||||
modified: number;
|
||||
notes: Note[];
|
||||
owner: string;
|
||||
sanity: number;
|
||||
started: number;
|
||||
restrictedDevice: boolean;
|
||||
rxBytes: number;
|
||||
serialNumber: string;
|
||||
simulated: boolean;
|
||||
subscriber: string;
|
||||
temperature: number;
|
||||
txBytes: number;
|
||||
venue: 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) =>
|
||||
axiosGw
|
||||
.get(`devices?deviceWithStatus=true&limit=${limit}&offset=${offset}`)
|
||||
@@ -127,6 +149,7 @@ export type DeviceStatus = {
|
||||
associations_2G: number;
|
||||
associations_5G: number;
|
||||
connected: boolean;
|
||||
connectReason?: string;
|
||||
certificateExpiryDate: number;
|
||||
connectionCompletionTime: number;
|
||||
firmware: string;
|
||||
|
||||
@@ -55,4 +55,5 @@ export const useGetTag = ({ serialNumber, onError }: { serialNumber?: string; on
|
||||
useQuery(['tag', serialNumber], () => getTag(serialNumber), {
|
||||
enabled: serialNumber !== undefined && serialNumber !== '',
|
||||
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]);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -39,6 +39,7 @@ type DeviceInterfaceStatistics = {
|
||||
ack_signal_avg: number;
|
||||
bssid: string;
|
||||
connected: number;
|
||||
dynamic_vlan?: number;
|
||||
inactive: number;
|
||||
ipaddr_v4: string;
|
||||
rssi: number;
|
||||
@@ -117,6 +118,13 @@ export type DeviceStatistics = {
|
||||
transmit_ms: number;
|
||||
tx_power: number;
|
||||
}[];
|
||||
dynamic_vlans?: {
|
||||
vid: number;
|
||||
rx_bytes: number;
|
||||
rx_packets: number;
|
||||
tx_bytes: number;
|
||||
tx_packets: number;
|
||||
}[];
|
||||
unit?: {
|
||||
load: [number, number, number];
|
||||
localtime: number;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Tooltip,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowSquareDown, ArrowSquareUp, Clock } from 'phosphor-react';
|
||||
import { ArrowSquareDown, ArrowSquareUp, Clock } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { compactSecondsToDetailed, minimalSecondsToDetailed } from 'helpers/dateFormatting';
|
||||
@@ -54,7 +54,7 @@ const SidebarDevices = () => {
|
||||
if (!getStats.data) return null;
|
||||
|
||||
return (
|
||||
<Card borderWidth="2px">
|
||||
<Card p={4}>
|
||||
<Tooltip hasArrow label={t('controller.stats.seconds_ago', { s: time })}>
|
||||
<CircularProgress
|
||||
isIndeterminate
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useBreakpoint,
|
||||
Portal,
|
||||
} from '@chakra-ui/react';
|
||||
import { ArrowCircleLeft } from 'phosphor-react';
|
||||
import { ArrowCircleLeft } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
@@ -97,14 +97,22 @@ export const Navbar = ({ toggleSidebar, activeRoute, languageSwitcher }: NavbarP
|
||||
ps="12px"
|
||||
pt="8px"
|
||||
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} />}
|
||||
<Heading>{activeRoute}</Heading>
|
||||
<Heading size="lg">{activeRoute}</Heading>
|
||||
<Tooltip label={t('common.go_back')}>
|
||||
<IconButton
|
||||
mt={2}
|
||||
mt={1}
|
||||
ml={4}
|
||||
colorScheme="blue"
|
||||
aria-label={t('common.go_back')}
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
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 { NavLink } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import IconBox from 'components/Containers/IconBox';
|
||||
import { Route } from 'models/Routes';
|
||||
import { SingleRoute } from 'models/Routes';
|
||||
|
||||
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 = {
|
||||
isActive: boolean;
|
||||
route: Route;
|
||||
route: SingleRoute;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
@@ -39,42 +17,64 @@ export const NavLinkButton = ({ isActive, route, toggleSidebar }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const activeTextColor = useColorModeValue('gray.700', 'white');
|
||||
const inactiveTextColor = useColorModeValue('gray.600', 'gray.200');
|
||||
const inactiveIconColor = useColorModeValue('gray.100', 'gray.600');
|
||||
const activeBg = useColorModeValue('blue.50', 'blue.900');
|
||||
const hoverBg = useColorModeValue('blue.100', 'blue.800');
|
||||
|
||||
if (route.navButton) {
|
||||
return route.navButton(isActive, toggleSidebar, route) as JSX.Element;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink to={route.path.replace(':id', '0')} key={uuid()} style={{ width: '100%' }}>
|
||||
<NavLink to={route.path.replace(':id', '0')} style={{ width: '100%' }}>
|
||||
{isActive ? (
|
||||
<Button {...commonStyle} boxShadow="none">
|
||||
<Flex>
|
||||
<IconBox bg="blue.300" color="white" h="38px" w="38px" me="6px" transition={variantChange}>
|
||||
{route.icon(true)}
|
||||
</IconBox>
|
||||
<Text color={activeTextColor} my="auto" fontSize="md">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||
<AccordionButton
|
||||
px={1}
|
||||
h={{
|
||||
md: '40px',
|
||||
lg: '50px',
|
||||
}}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
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
|
||||
{...commonStyle}
|
||||
ps="6px"
|
||||
_focus={{
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
<IconBox bg={inactiveIconColor} color="blue.300" h="34px" w="34px" me="6px" transition={variantChange}>
|
||||
{route.icon(false)}
|
||||
</IconBox>
|
||||
<Text color={inactiveTextColor} my="auto" fontSize="sm">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Button>
|
||||
<AccordionItem w="152px" borderTop="0px" borderBottom="0px">
|
||||
<AccordionButton
|
||||
px={1}
|
||||
h={{
|
||||
md: '40px',
|
||||
lg: '50px',
|
||||
}}
|
||||
borderRadius="15px"
|
||||
w="100%"
|
||||
_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={inactiveTextColor} fontSize="md" fontWeight="bold">
|
||||
{t(route.name)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</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,
|
||||
useBreakpoint,
|
||||
VStack,
|
||||
Accordion,
|
||||
} from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { NavLinkButton } from './NavLinkButton';
|
||||
import NestedNavButton from './NestedNavButton';
|
||||
import { useAuth } from 'contexts/AuthProvider';
|
||||
import { Route } from 'models/Routes';
|
||||
|
||||
@@ -60,14 +61,25 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
const sidebarContent = React.useMemo(
|
||||
() => (
|
||||
<>
|
||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||
{routes
|
||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||
.map((route) => (
|
||||
<NavLinkButton key={uuid()} isActive={isRouteActive(route.path)} route={route} toggleSidebar={toggle} />
|
||||
))}
|
||||
</VStack>
|
||||
<Accordion allowToggle>
|
||||
<VStack spacing={2} alignItems="start" w="100%" px={4}>
|
||||
{topNav ? topNav(isRouteActive, toggle) : null}
|
||||
{routes
|
||||
.filter(({ hidden, authorized }) => !hidden && authorized.includes(user?.userRole ?? ''))
|
||||
.map((route) =>
|
||||
route.children ? (
|
||||
<NestedNavButton key={route.id} isActive={isRouteActive} route={route} />
|
||||
) : (
|
||||
<NavLinkButton
|
||||
key={route.id}
|
||||
isActive={isRouteActive(route.path)}
|
||||
route={route}
|
||||
toggleSidebar={toggle}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</VStack>
|
||||
</Accordion>
|
||||
<Spacer />
|
||||
<Box mb={2}>{children}</Box>
|
||||
<Box>
|
||||
@@ -117,7 +129,8 @@ export const Sidebar = ({ routes, isOpen, toggle, logo, version, topNav, childre
|
||||
h="calc(100vh - 32px)"
|
||||
my="16px"
|
||||
ml="16px"
|
||||
borderRadius="16px"
|
||||
borderRadius="15px"
|
||||
border="0.5px solid"
|
||||
>
|
||||
{brand}
|
||||
<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 lightLogo from 'assets/Logo_Light_Mode.svg';
|
||||
import LanguageSwitcher from 'components/LanguageSwitcher';
|
||||
import { Route as RouteProps } from 'models/Routes';
|
||||
import { RouteName } from 'models/Routes';
|
||||
import NotFoundPage from 'pages/NotFound';
|
||||
import routes from 'router/routes';
|
||||
|
||||
@@ -22,18 +22,57 @@ const Layout = () => {
|
||||
document.documentElement.dir = 'ltr';
|
||||
|
||||
const activeRoute = React.useMemo(() => {
|
||||
const route = routes.find(
|
||||
(r) => r.path === location.pathname || location.pathname.split('/')[1] === r.path.split('/')[1],
|
||||
);
|
||||
let name: RouteName = '';
|
||||
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]);
|
||||
|
||||
const getRoutes = (r: RouteProps[]) =>
|
||||
// @ts-ignore
|
||||
r.map((route: RouteProps) => <Route path={route.path} element={<route.component />} key={uuid()} />);
|
||||
const routeInstances = React.useMemo(() => {
|
||||
const instances = [];
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -59,9 +98,7 @@ const Layout = () => {
|
||||
</Sidebar>
|
||||
<Navbar toggleSidebar={toggleSidebar} languageSwitcher={<LanguageSwitcher />} activeRoute={activeRoute} />
|
||||
<PageContainer waitForUser>
|
||||
<Routes>
|
||||
{[...getRoutes(routes as RouteProps[]), <Route path="*" element={<NotFoundPage />} key={uuid()} />]}
|
||||
</Routes>
|
||||
<Routes>{[...routeInstances, <Route path="*" element={<NotFoundPage />} key={uuid()} />]}</Routes>
|
||||
</PageContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface GatewayDevice {
|
||||
entity: string;
|
||||
firmware: string;
|
||||
fwUpdatePolicy: string;
|
||||
hasGPS: boolean;
|
||||
hasRADIUSSessions: number;
|
||||
lastConfigurationChange: number;
|
||||
lastConfigurationDownload: number;
|
||||
lastFWUpdate: number;
|
||||
@@ -37,6 +39,7 @@ export interface GatewayDevice {
|
||||
};
|
||||
};
|
||||
serialNumber: string;
|
||||
simulated: boolean;
|
||||
subscriber: 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[];
|
||||
path: string;
|
||||
name: string;
|
||||
navName?: string;
|
||||
icon: (active: boolean) => ReactNode;
|
||||
navButton?: (isActive: boolean, toggleSidebar: () => void, route: Route) => React.ReactNode;
|
||||
name: RouteName;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
navName?: RouteName;
|
||||
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;
|
||||
component: unknown;
|
||||
component: React.ReactElement | LazyExoticComponent<React.ComponentType<unknown>>;
|
||||
hidden?: boolean;
|
||||
isCustom?: boolean;
|
||||
}
|
||||
children?: undefined;
|
||||
};
|
||||
|
||||
export type Route = SingleRoute | RouteGroup;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Note } from './Note';
|
||||
|
||||
|
||||
export type UserRole =
|
||||
| 'root'
|
||||
| 'admin'
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DefaultConfigurationResponse, useDeleteDefaultConfig } from 'hooks/Network/DefaultConfigurations';
|
||||
import { AxiosError } from 'models/Axios';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Grid, GridItem, Heading, Link, Spacer, useClipboard, useDisclosure } from '@chakra-ui/react';
|
||||
import { Eye, EyeSlash } from 'phosphor-react';
|
||||
import { Eye, EyeSlash, ListBullets } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ViewCapabilitiesModal from './ViewCapabilitiesModal';
|
||||
import ViewConfigurationModal from './ViewConfigurationModal';
|
||||
@@ -28,7 +28,6 @@ const DeviceDetails = ({ serialNumber }: Props) => {
|
||||
? getDevice.data?.devicePassword
|
||||
: 'openwifi',
|
||||
);
|
||||
|
||||
const getPassword = () => {
|
||||
if (!getDevice.data) return '-';
|
||||
if (isShowingPassword) {
|
||||
@@ -51,7 +50,13 @@ const DeviceDetails = ({ serialNumber }: Props) => {
|
||||
|
||||
return (
|
||||
<Card mb={4}>
|
||||
<CardHeader mb={2}>
|
||||
<CardHeader
|
||||
mb={2}
|
||||
headerStyle={{
|
||||
color: 'purple',
|
||||
}}
|
||||
icon={<ListBullets weight="bold" size={20} />}
|
||||
>
|
||||
<Heading size="md">{t('common.details')}</Heading>
|
||||
<Spacer />
|
||||
<ViewCapabilitiesModal serialNumber={serialNumber} />
|
||||
@@ -107,7 +112,7 @@ const DeviceDetails = ({ serialNumber }: Props) => {
|
||||
</GridItem>
|
||||
<GridItem colSpan={1}>
|
||||
{getTag.data?.extendedInfo?.entity?.name ? (
|
||||
<Link isExternal href={goToProvUi(`entity/${getTag.data?.subscriber}`)}>
|
||||
<Link isExternal href={goToProvUi(`entity/${getTag.data?.entity}`)}>
|
||||
{getTag.data?.extendedInfo?.entity?.name}
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Flex, FormControl, FormLabel, useDisclosure } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Icon,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { Wrapper } from '@googlemaps/react-wrapper';
|
||||
import { Globe } from 'phosphor-react';
|
||||
import { Globe } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GoogleMap } from 'components/Maps/GoogleMap';
|
||||
import { GoogleMapMarker } from 'components/Maps/GoogleMap/Marker';
|
||||
@@ -11,13 +21,15 @@ import { useGetDeviceLastStats } from 'hooks/Network/Statistics';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
isCompact?: boolean;
|
||||
};
|
||||
|
||||
const LocationDisplayButton = ({ serialNumber }: Props) => {
|
||||
const LocationDisplayButton = ({ serialNumber, isCompact }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const getGoogleApiKey = useGetSystemSecret({ secret: 'google.maps.apikey' });
|
||||
const getLastStats = useGetDeviceLastStats({ serialNumber });
|
||||
const iconColor = useColorModeValue('blue.500', 'blue.200');
|
||||
|
||||
const location: google.maps.LatLngLiteral | undefined = React.useMemo(() => {
|
||||
if (!getLastStats.data?.gps) return undefined;
|
||||
@@ -38,9 +50,15 @@ const LocationDisplayButton = ({ serialNumber }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
|
||||
{t('locations.view_gps')}
|
||||
</Button>
|
||||
{isCompact ? (
|
||||
<Tooltip label={t('locations.view_gps')}>
|
||||
<Icon as={Globe} boxSize={6} onClick={onOpen} color={iconColor} cursor="pointer" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button variant="link" onClick={onOpen} rightIcon={<Globe size={20} />} colorScheme="blue">
|
||||
{t('locations.view_gps')}
|
||||
</Button>
|
||||
)}
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={t('locations.one')}>
|
||||
<Box w="100%" h="100%">
|
||||
<Flex mb={4}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { Download } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
|
||||
import { DeviceCommandHistory, useDownloadScriptResult } from 'hooks/Network/Commands';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { Download } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
|
||||
import { DeviceCommandHistory } from 'hooks/Network/Commands';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Download } from 'phosphor-react';
|
||||
import { Download } from '@phosphor-icons/react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResponsiveButton } from 'components/Buttons/ResponsiveButton';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Badge, Box, HStack, IconButton, Tooltip, useDisclosure, useToast } from '@chakra-ui/react';
|
||||
import { MagnifyingGlass, Trash } from 'phosphor-react';
|
||||
import { MagnifyingGlass, Trash } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { uppercaseFirstLetter } from 'helpers/stringHelper';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Tooltip,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { Clock, Prohibit } from 'phosphor-react';
|
||||
import { Clock, Prohibit } from '@phosphor-icons/react';
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloseButton } from 'components/Buttons/CloseButton';
|
||||
|
||||
@@ -52,7 +52,7 @@ const CrashLogs = ({ serialNumber }: Props) => {
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="gateway.device.logs.hiddenColumns"
|
||||
/>
|
||||
<DeleteLogModal serialNumber={serialNumber} logType={0} />
|
||||
<DeleteLogModal serialNumber={serialNumber} logType={1} />
|
||||
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
@@ -16,7 +16,7 @@ const CustomInputButton = React.forwardRef(
|
||||
),
|
||||
);
|
||||
|
||||
type Props = { serialNumber: string; logType: 0 | 1 };
|
||||
type Props = { serialNumber: string; logType: 0 | 1 | 2 };
|
||||
const DeleteLogModal = ({ serialNumber, logType }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
|
||||
@@ -18,11 +18,26 @@ const DetailedLogViewModal = ({ modalProps, log }: Props) => {
|
||||
const { hasCopied, onCopy, setValue } = useClipboard(JSON.stringify(log?.log ?? {}, null, 2));
|
||||
|
||||
React.useEffect(() => {
|
||||
setValue(JSON.stringify(log?.log ?? {}, null, 2));
|
||||
if (log?.logType === 2) {
|
||||
setValue(JSON.stringify(log.data ?? {}, null, 2));
|
||||
} else {
|
||||
setValue(JSON.stringify(log?.log ?? {}, null, 2));
|
||||
}
|
||||
}, [log]);
|
||||
|
||||
if (!log) return null;
|
||||
|
||||
const getCodeContent = () => {
|
||||
if (log.logType === 2) {
|
||||
if (log.data.info !== undefined && Array.isArray(log.data.info)) {
|
||||
return log.data.info.map((v) => v).join('\n');
|
||||
}
|
||||
return JSON.stringify(log.data, null, 2);
|
||||
}
|
||||
|
||||
return log.log;
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={modalProps.isOpen}
|
||||
@@ -45,7 +60,7 @@ const DetailedLogViewModal = ({ modalProps, log }: Props) => {
|
||||
{t('controller.devices.config_id')}: {log.UUID}
|
||||
</Heading>
|
||||
<Code whiteSpace="pre-line" mt={2}>
|
||||
{log.log}
|
||||
{getCodeContent()}
|
||||
</Code>
|
||||
</Box>
|
||||
</Modal>
|
||||
|
||||
94
src/pages/Device/LogsCard/LogHistory/RebootLogs.tsx
Normal file
94
src/pages/Device/LogsCard/LogHistory/RebootLogs.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Button, Center, Flex, Heading, HStack, Spacer } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import HistoryDatePickers from '../DatePickers';
|
||||
import DeleteLogModal from './DeleteModal';
|
||||
import useDeviceLogsTable from './useDeviceLogsTable';
|
||||
import { RefreshButton } from 'components/Buttons/RefreshButton';
|
||||
import { ColumnPicker } from 'components/DataTables/ColumnPicker';
|
||||
import { DataTable } from 'components/DataTables/DataTable';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
};
|
||||
const RebootLogs = ({ serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [limit, setLimit] = React.useState(25);
|
||||
const [hiddenColumns, setHiddenColumns] = React.useState<string[]>([]);
|
||||
const { time, setTime, getCustomLogs, getLogs, columns, modal } = useDeviceLogsTable({
|
||||
serialNumber,
|
||||
limit,
|
||||
logType: 2,
|
||||
});
|
||||
|
||||
const setNewTime = (start: Date, end: Date) => {
|
||||
setTime({ start, end });
|
||||
};
|
||||
const onClear = () => {
|
||||
setTime(undefined);
|
||||
};
|
||||
const raiseLimit = () => {
|
||||
setLimit(limit + 25);
|
||||
};
|
||||
|
||||
const noMoreAvailable = getLogs.data !== undefined && getLogs.data.values.length < limit;
|
||||
|
||||
const data = React.useMemo(() => {
|
||||
if (getCustomLogs.data) return getCustomLogs.data.values.sort((a, b) => b.recorded - a.recorded);
|
||||
if (getLogs.data) return getLogs.data.values;
|
||||
return [];
|
||||
}, [getLogs.data, getCustomLogs.data]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex>
|
||||
<Spacer />
|
||||
<HStack>
|
||||
<HistoryDatePickers defaults={time} setTime={setNewTime} onClear={onClear} />
|
||||
<ColumnPicker
|
||||
columns={columns as Column<unknown>[]}
|
||||
hiddenColumns={hiddenColumns}
|
||||
setHiddenColumns={setHiddenColumns}
|
||||
preference="gateway.device.logs.hiddenColumns"
|
||||
/>
|
||||
<DeleteLogModal serialNumber={serialNumber} logType={2} />
|
||||
<RefreshButton isCompact isFetching={getLogs.isFetching} onClick={getLogs.refetch} colorScheme="blue" />
|
||||
</HStack>
|
||||
</Flex>
|
||||
<Box overflowY="auto" h="300px">
|
||||
<DataTable
|
||||
columns={
|
||||
columns as {
|
||||
id: string;
|
||||
Header: string;
|
||||
Footer: string;
|
||||
accessor: string;
|
||||
}[]
|
||||
}
|
||||
data={data}
|
||||
isLoading={getLogs.isFetching || getCustomLogs.isFetching}
|
||||
hiddenColumns={hiddenColumns}
|
||||
obj={t('controller.devices.logs')}
|
||||
// @ts-ignore
|
||||
hideControls
|
||||
showAllRows
|
||||
/>
|
||||
{getLogs.data !== undefined && (
|
||||
<Center mt={1} hidden={getCustomLogs.data !== undefined}>
|
||||
{!noMoreAvailable || getLogs.isFetching ? (
|
||||
<Button colorScheme="blue" onClick={raiseLimit} isLoading={getLogs.isFetching}>
|
||||
{t('controller.devices.show_more')}
|
||||
</Button>
|
||||
) : (
|
||||
<Heading size="sm">{t('controller.devices.no_more_available')}!</Heading>
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
{modal}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default RebootLogs;
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Box, IconButton, Text, useDisclosure } from '@chakra-ui/react';
|
||||
import { MagnifyingGlass } from 'phosphor-react';
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DetailedLogViewModal from './DetailedLogViewModal';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
@@ -10,7 +10,7 @@ import { Column } from 'models/Table';
|
||||
type Props = {
|
||||
serialNumber: string;
|
||||
limit: number;
|
||||
logType: 0 | 1;
|
||||
logType: 0 | 1 | 2;
|
||||
};
|
||||
|
||||
const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
@@ -53,6 +53,28 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
[onOpen],
|
||||
);
|
||||
|
||||
const detailsCell = React.useCallback((v: DeviceLog) => {
|
||||
if (logType === 2) {
|
||||
return (
|
||||
<Box display="flex">
|
||||
<IconButton
|
||||
aria-label="Open Log Details"
|
||||
onClick={() => onOpen(v)}
|
||||
colorScheme="blue"
|
||||
icon={<MagnifyingGlass size={16} />}
|
||||
size="xs"
|
||||
mr={2}
|
||||
/>
|
||||
<Text my="auto" textOverflow="ellipsis" overflow="hidden" whiteSpace="nowrap">
|
||||
{JSON.stringify(v.data, null, 0)}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <pre>{JSON.stringify(v.data, null, 0)}</pre>;
|
||||
}, []);
|
||||
|
||||
const dateCell = React.useCallback(
|
||||
(v: number) => (
|
||||
<Box>
|
||||
@@ -62,8 +84,6 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
[],
|
||||
);
|
||||
|
||||
const jsonCell = React.useCallback((v: Record<string, unknown>) => <pre>{JSON.stringify(v, null, 0)}</pre>, []);
|
||||
|
||||
const columns: Column<DeviceLog>[] = React.useMemo(
|
||||
(): Column<DeviceLog>[] => [
|
||||
{
|
||||
@@ -106,7 +126,7 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
Header: t('common.details'),
|
||||
Footer: '',
|
||||
accessor: 'data',
|
||||
Cell: (v) => jsonCell(v.cell.row.original.data),
|
||||
Cell: (v) => detailsCell(v.cell.row.original),
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
@@ -114,7 +134,19 @@ const useDeviceLogsTable = ({ serialNumber, limit, logType }: Props) => {
|
||||
);
|
||||
|
||||
return {
|
||||
columns,
|
||||
columns:
|
||||
logType === 2
|
||||
? columns
|
||||
.filter((c) => c.id !== 'severity')
|
||||
.map((col) =>
|
||||
col.id === 'log'
|
||||
? {
|
||||
...col,
|
||||
Header: 'Type',
|
||||
}
|
||||
: col,
|
||||
)
|
||||
: columns,
|
||||
getLogs,
|
||||
getCustomLogs,
|
||||
time,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import { Box, Tab, TabList, TabPanel, TabPanels, Tabs, useBreakpoint } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import CommandHistory from './CommandHistory';
|
||||
import HealthCheckHistory from './HealthCheckHistory';
|
||||
import LogHistory from './LogHistory';
|
||||
import CrashLogs from './LogHistory/CrashLogs';
|
||||
import RebootLogs from './LogHistory/RebootLogs';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
|
||||
@@ -13,12 +14,15 @@ type Props = {
|
||||
};
|
||||
const DeviceLogsCard = ({ serialNumber }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const breakpoint = useBreakpoint();
|
||||
const [tabIndex, setTabIndex] = React.useState(0);
|
||||
|
||||
const handleTabsChange = React.useCallback((index: number) => {
|
||||
setTabIndex(index);
|
||||
}, []);
|
||||
|
||||
const isCompact = breakpoint === 'base' || breakpoint === 'sm' || breakpoint === 'md' || breakpoint === 'lg';
|
||||
|
||||
return (
|
||||
<Card p={0} mb={4}>
|
||||
<CardBody p={0}>
|
||||
@@ -34,7 +38,10 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
|
||||
{t('controller.devices.logs')}
|
||||
</Tab>
|
||||
<Tab fontSize="lg" fontWeight="bold">
|
||||
{t('devices.crash_logs')}
|
||||
{isCompact ? 'Crashes' : t('devices.crash_logs')}
|
||||
</Tab>
|
||||
<Tab fontSize="lg" fontWeight="bold">
|
||||
{isCompact ? 'Reboots' : t('devices.reboot_logs')}
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
@@ -61,6 +68,9 @@ const DeviceLogsCard = ({ serialNumber }: Props) => {
|
||||
<TabPanel>
|
||||
<CrashLogs serialNumber={serialNumber} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<RebootLogs serialNumber={serialNumber} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</CardBody>
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useToast,
|
||||
useBreakpoint,
|
||||
} from '@chakra-ui/react';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { Note, Plus } from '@phosphor-icons/react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card } from 'components/Containers/Card';
|
||||
import { CardBody } from 'components/Containers/Card/CardBody';
|
||||
@@ -26,7 +26,7 @@ import { CardHeader } from 'components/Containers/Card/CardHeader';
|
||||
import { DataTable } from 'components/DataTables/DataTable';
|
||||
import FormattedDate from 'components/InformationDisplays/FormattedDate';
|
||||
import { useGetDevice, useUpdateDevice } from 'hooks/Network/Devices';
|
||||
import { Note } from 'models/Note';
|
||||
import { Note as TNote } from 'models/Note';
|
||||
import { Column } from 'models/Table';
|
||||
|
||||
type Props = {
|
||||
@@ -87,7 +87,7 @@ const DeviceNotes = ({ serialNumber }: Props) => {
|
||||
[],
|
||||
);
|
||||
|
||||
const columns: Column<Note>[] = React.useMemo(
|
||||
const columns: Column<TNote>[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 'created',
|
||||
@@ -116,8 +116,8 @@ const DeviceNotes = ({ serialNumber }: Props) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Card mb={4} p={4}>
|
||||
<CardHeader mb={2}>
|
||||
<Card mb={4}>
|
||||
<CardHeader icon={<Note weight="bold" size={20} />}>
|
||||
<Heading size="md">{t('common.notes')}</Heading>
|
||||
<Spacer />
|
||||
<Popover trigger="click" placement="auto">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user